aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.codeclimate.yml36
-rw-r--r--.github/autolabeler.yml28
-rw-r--r--.github/issue_template.md9
-rw-r--r--.github/no-response.yml12
-rw-r--r--.github/pull_request_template.md13
-rw-r--r--.github/stale.yml28
-rw-r--r--.gitignore29
-rw-r--r--.rubocop.yml250
-rw-r--r--.travis.yml167
-rw-r--r--.yarnrc2
-rw-r--r--Brewfile16
-rw-r--r--CODE_OF_CONDUCT.md4
-rw-r--r--CONTRIBUTING.md16
-rw-r--r--Gemfile159
-rw-r--r--Gemfile.lock737
-rw-r--r--MIT-LICENSE20
-rw-r--r--RAILS_VERSION2
-rw-r--r--README.md73
-rw-r--r--RELEASING_RAILS.md117
-rw-r--r--Rakefile43
-rw-r--r--actioncable/.babelrc8
-rw-r--r--actioncable/.eslintrc19
-rw-r--r--actioncable/.gitignore6
-rw-r--r--actioncable/CHANGELOG.md167
-rw-r--r--actioncable/MIT-LICENSE2
-rw-r--r--actioncable/README.md459
-rw-r--r--actioncable/Rakefile65
-rw-r--r--actioncable/actioncable.gemspec38
-rw-r--r--actioncable/app/assets/javascripts/action_cable.coffee.erb35
-rw-r--r--actioncable/app/assets/javascripts/action_cable.js517
-rw-r--r--actioncable/app/assets/javascripts/action_cable/connection.coffee107
-rw-r--r--actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee95
-rw-r--r--actioncable/app/assets/javascripts/action_cable/consumer.coffee27
-rw-r--r--actioncable/app/assets/javascripts/action_cable/subscription.coffee66
-rw-r--r--actioncable/app/assets/javascripts/action_cable/subscriptions.coffee66
-rw-r--r--actioncable/app/javascript/action_cable/adapters.js4
-rw-r--r--actioncable/app/javascript/action_cable/connection.js165
-rw-r--r--actioncable/app/javascript/action_cable/connection_monitor.js126
-rw-r--r--actioncable/app/javascript/action_cable/consumer.js75
-rw-r--r--actioncable/app/javascript/action_cable/index.js31
-rw-r--r--actioncable/app/javascript/action_cable/logger.js10
-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/karma.conf.js64
-rw-r--r--actioncable/lib/action_cable.rb29
-rw-r--r--actioncable/lib/action_cable/channel.rb3
-rw-r--r--actioncable/lib/action_cable/channel/base.rb86
-rw-r--r--actioncable/lib/action_cable/channel/broadcasting.rb32
-rw-r--r--actioncable/lib/action_cable/channel/callbacks.rb6
-rw-r--r--actioncable/lib/action_cable/channel/naming.rb7
-rw-r--r--actioncable/lib/action_cable/channel/periodic_timers.rb57
-rw-r--r--actioncable/lib/action_cable/channel/streams.rb79
-rw-r--r--actioncable/lib/action_cable/channel/test_case.rb310
-rw-r--r--actioncable/lib/action_cable/connection.rb5
-rw-r--r--actioncable/lib/action_cable/connection/authorization.rb14
-rw-r--r--actioncable/lib/action_cable/connection/base.rb68
-rw-r--r--actioncable/lib/action_cable/connection/client_socket.rb46
-rw-r--r--actioncable/lib/action_cable/connection/faye_client_socket.rb43
-rw-r--r--actioncable/lib/action_cable/connection/faye_event_loop.rb44
-rw-r--r--actioncable/lib/action_cable/connection/identification.rb9
-rw-r--r--actioncable/lib/action_cable/connection/internal_channel.rb6
-rw-r--r--actioncable/lib/action_cable/connection/message_buffer.rb5
-rw-r--r--actioncable/lib/action_cable/connection/stream.rb72
-rw-r--r--actioncable/lib/action_cable/connection/stream_event_loop.rb53
-rw-r--r--actioncable/lib/action_cable/connection/subscriptions.rb38
-rw-r--r--actioncable/lib/action_cable/connection/tagged_logger_proxy.rb6
-rw-r--r--actioncable/lib/action_cable/connection/test_case.rb234
-rw-r--r--actioncable/lib/action_cable/connection/web_socket.rb16
-rw-r--r--actioncable/lib/action_cable/engine.rb15
-rw-r--r--actioncable/lib/action_cable/gem_version.rb4
-rw-r--r--actioncable/lib/action_cable/helpers/action_cable_helper.rb4
-rw-r--r--actioncable/lib/action_cable/remote_connections.rb15
-rw-r--r--actioncable/lib/action_cable/server.rb4
-rw-r--r--actioncable/lib/action_cable/server/base.rb49
-rw-r--r--actioncable/lib/action_cable/server/broadcasting.rb12
-rw-r--r--actioncable/lib/action_cable/server/configuration.rb56
-rw-r--r--actioncable/lib/action_cable/server/connections.rb2
-rw-r--r--actioncable/lib/action_cable/server/worker.rb50
-rw-r--r--actioncable/lib/action_cable/server/worker/active_record_connection_management.rb2
-rw-r--r--actioncable/lib/action_cable/subscription_adapter.rb4
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/async.rb4
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/base.rb2
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb28
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/evented_redis.rb75
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/inline.rb2
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/postgresql.rb53
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/redis.rb27
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb10
-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.rb4
-rw-r--r--actioncable/lib/rails/generators/channel/USAGE9
-rw-r--r--actioncable/lib/rails/generators/channel/channel_generator.rb27
-rw-r--r--actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb5
-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.rb5
-rw-r--r--actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb.tt4
-rw-r--r--actioncable/lib/rails/generators/channel/templates/assets/channel.coffee14
-rw-r--r--actioncable/lib/rails/generators/channel/templates/channel.rb17
-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/lib/rails/generators/test_unit/channel_generator.rb20
-rw-r--r--actioncable/lib/rails/generators/test_unit/templates/channel_test.rb.tt8
-rw-r--r--actioncable/package.json51
-rw-r--r--actioncable/rollup.config.js24
-rw-r--r--actioncable/rollup.config.test.js18
-rw-r--r--actioncable/test/channel/base_test.rb171
-rw-r--r--actioncable/test/channel/broadcasting_test.rb37
-rw-r--r--actioncable/test/channel/naming_test.rb6
-rw-r--r--actioncable/test/channel/periodic_timers_test.rb77
-rw-r--r--actioncable/test/channel/rejection_test.rb47
-rw-r--r--actioncable/test/channel/stream_test.rb142
-rw-r--r--actioncable/test/channel/test_case_test.rb214
-rw-r--r--actioncable/test/client/echo_channel.rb22
-rw-r--r--actioncable/test/client_test.rb281
-rw-r--r--actioncable/test/connection/authorization_test.rb17
-rw-r--r--actioncable/test/connection/base_test.rb48
-rw-r--r--actioncable/test/connection/client_socket_test.rb60
-rw-r--r--actioncable/test/connection/cross_site_forgery_test.rb51
-rw-r--r--actioncable/test/connection/identifier_test.rb52
-rw-r--r--actioncable/test/connection/multiple_identifiers_test.rb27
-rw-r--r--actioncable/test/connection/stream_test.rb30
-rw-r--r--actioncable/test/connection/string_identifier_test.rb27
-rw-r--r--actioncable/test/connection/subscriptions_test.rb44
-rw-r--r--actioncable/test/connection/test_case_test.rb197
-rw-r--r--actioncable/test/javascript/src/test.js6
-rw-r--r--actioncable/test/javascript/src/test_helpers/consumer_test_helper.js58
-rw-r--r--actioncable/test/javascript/src/test_helpers/index.js10
-rw-r--r--actioncable/test/javascript/src/unit/action_cable_test.js57
-rw-r--r--actioncable/test/javascript/src/unit/connection_test.js28
-rw-r--r--actioncable/test/javascript/src/unit/consumer_test.js19
-rw-r--r--actioncable/test/javascript/src/unit/subscription_test.js54
-rw-r--r--actioncable/test/javascript/src/unit/subscriptions_test.js31
-rw-r--r--actioncable/test/server/base_test.rb38
-rw-r--r--actioncable/test/server/broadcasting_test.rb53
-rw-r--r--actioncable/test/stubs/global_id.rb2
-rw-r--r--actioncable/test/stubs/room.rb4
-rw-r--r--actioncable/test/stubs/test_adapter.rb4
-rw-r--r--actioncable/test/stubs/test_connection.rb6
-rw-r--r--actioncable/test/stubs/test_server.rb22
-rw-r--r--actioncable/test/stubs/user.rb2
-rw-r--r--actioncable/test/subscription_adapter/async_test.rb8
-rw-r--r--actioncable/test/subscription_adapter/base_test.rb40
-rw-r--r--actioncable/test/subscription_adapter/channel_prefix.rb30
-rw-r--r--actioncable/test/subscription_adapter/common.rb86
-rw-r--r--actioncable/test/subscription_adapter/evented_redis_test.rb21
-rw-r--r--actioncable/test/subscription_adapter/inline_test.rb8
-rw-r--r--actioncable/test/subscription_adapter/postgresql_test.rb45
-rw-r--r--actioncable/test/subscription_adapter/redis_test.rb48
-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.rb73
-rw-r--r--actioncable/test/test_helper_test.rb116
-rw-r--r--actioncable/test/worker_test.rb22
-rw-r--r--actionmailbox/.gitignore5
-rw-r--r--actionmailbox/CHANGELOG.md19
-rw-r--r--actionmailbox/MIT-LICENSE21
-rw-r--r--actionmailbox/README.md13
-rw-r--r--actionmailbox/Rakefile15
-rw-r--r--actionmailbox/actionmailbox.gemspec38
-rw-r--r--actionmailbox/app/controllers/action_mailbox/base_controller.rb38
-rw-r--r--actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb54
-rw-r--r--actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb103
-rw-r--r--actionmailbox/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb82
-rw-r--r--actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb62
-rw-r--r--actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb65
-rw-r--r--actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb54
-rw-r--r--actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb34
-rw-r--r--actionmailbox/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb19
-rw-r--r--actionmailbox/app/controllers/rails/conductor/base_controller.rb14
-rw-r--r--actionmailbox/app/jobs/action_mailbox/incineration_job.rb25
-rw-r--r--actionmailbox/app/jobs/action_mailbox/routing_job.rb13
-rw-r--r--actionmailbox/app/models/action_mailbox/inbound_email.rb49
-rw-r--r--actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb20
-rw-r--r--actionmailbox/app/models/action_mailbox/inbound_email/incineratable/incineration.rb26
-rw-r--r--actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb38
-rw-r--r--actionmailbox/app/models/action_mailbox/inbound_email/routable.rb24
-rw-r--r--actionmailbox/app/views/layouts/rails/conductor.html.erb8
-rw-r--r--actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb15
-rw-r--r--actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb47
-rw-r--r--actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb15
-rwxr-xr-xactionmailbox/bin/test5
-rw-r--r--actionmailbox/config/routes.rb20
-rw-r--r--actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb13
-rw-r--r--actionmailbox/lib/action_mailbox.rb17
-rw-r--r--actionmailbox/lib/action_mailbox/base.rb118
-rw-r--r--actionmailbox/lib/action_mailbox/callbacks.rb34
-rw-r--r--actionmailbox/lib/action_mailbox/engine.rb42
-rw-r--r--actionmailbox/lib/action_mailbox/gem_version.rb17
-rw-r--r--actionmailbox/lib/action_mailbox/mail_ext.rb6
-rw-r--r--actionmailbox/lib/action_mailbox/mail_ext/address_equality.rb9
-rw-r--r--actionmailbox/lib/action_mailbox/mail_ext/address_wrapping.rb9
-rw-r--r--actionmailbox/lib/action_mailbox/mail_ext/addresses.rb29
-rw-r--r--actionmailbox/lib/action_mailbox/mail_ext/from_source.rb7
-rw-r--r--actionmailbox/lib/action_mailbox/mail_ext/recipients.rb9
-rw-r--r--actionmailbox/lib/action_mailbox/relayer.rb75
-rw-r--r--actionmailbox/lib/action_mailbox/router.rb42
-rw-r--r--actionmailbox/lib/action_mailbox/router/route.rb42
-rw-r--r--actionmailbox/lib/action_mailbox/routing.rb22
-rw-r--r--actionmailbox/lib/action_mailbox/test_case.rb12
-rw-r--r--actionmailbox/lib/action_mailbox/test_helper.rb44
-rw-r--r--actionmailbox/lib/action_mailbox/version.rb10
-rw-r--r--actionmailbox/lib/rails/generators/installer.rb10
-rw-r--r--actionmailbox/lib/rails/generators/mailbox/USAGE12
-rw-r--r--actionmailbox/lib/rails/generators/mailbox/mailbox_generator.rb32
-rw-r--r--actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt3
-rw-r--r--actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt4
-rw-r--r--actionmailbox/lib/rails/generators/test_unit/mailbox_generator.rb20
-rw-r--r--actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt13
-rw-r--r--actionmailbox/lib/tasks/ingress.rake72
-rw-r--r--actionmailbox/lib/tasks/install.rake20
-rw-r--r--actionmailbox/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb22
-rw-r--r--actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb91
-rw-r--r--actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb60
-rw-r--r--actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb55
-rw-r--r--actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb56
-rw-r--r--actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb46
-rw-r--r--actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb57
-rw-r--r--actionmailbox/test/dummy/.babelrc18
-rw-r--r--actionmailbox/test/dummy/.postcssrc.yml3
-rw-r--r--actionmailbox/test/dummy/Rakefile (renamed from railties/lib/rails/generators/rails/app/templates/Rakefile)0
-rw-r--r--actionmailbox/test/dummy/app/assets/config/manifest.js3
-rw-r--r--actionmailbox/test/dummy/app/assets/images/.keep (renamed from actionpack/test/tmp/.gitignore)0
-rw-r--r--actionmailbox/test/dummy/app/assets/stylesheets/application.css (renamed from railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css)0
-rw-r--r--actionmailbox/test/dummy/app/assets/stylesheets/scaffold.css80
-rw-r--r--actionmailbox/test/dummy/app/channels/application_cable/channel.rb4
-rw-r--r--actionmailbox/test/dummy/app/channels/application_cable/connection.rb4
-rw-r--r--actionmailbox/test/dummy/app/controllers/application_controller.rb2
-rw-r--r--actionmailbox/test/dummy/app/controllers/concerns/.keep (renamed from actionview/test/tmp/.gitkeep)0
-rw-r--r--actionmailbox/test/dummy/app/helpers/application_helper.rb (renamed from railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb)0
-rw-r--r--actionmailbox/test/dummy/app/javascript/packs/application.js (renamed from activerecord/test/migrations/empty/.gitkeep)0
-rw-r--r--actionmailbox/test/dummy/app/jobs/application_job.rb (renamed from railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb)0
-rw-r--r--actionmailbox/test/dummy/app/mailboxes/application_mailbox.rb5
-rw-r--r--actionmailbox/test/dummy/app/mailboxes/messages_mailbox.rb4
-rw-r--r--actionmailbox/test/dummy/app/mailers/application_mailer.rb (renamed from railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb)0
-rw-r--r--actionmailbox/test/dummy/app/models/application_record.rb (renamed from railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb)0
-rw-r--r--actionmailbox/test/dummy/app/models/concerns/.keep0
-rw-r--r--actionmailbox/test/dummy/app/views/layouts/application.html.erb14
-rw-r--r--actionmailbox/test/dummy/app/views/layouts/mailer.html.erb13
-rw-r--r--actionmailbox/test/dummy/app/views/layouts/mailer.text.erb1
-rwxr-xr-xactionmailbox/test/dummy/bin/bundle3
-rwxr-xr-xactionmailbox/test/dummy/bin/rails4
-rwxr-xr-xactionmailbox/test/dummy/bin/rake4
-rwxr-xr-xactionmailbox/test/dummy/bin/setup36
-rwxr-xr-xactionmailbox/test/dummy/bin/update31
-rwxr-xr-xactionmailbox/test/dummy/bin/yarn11
-rw-r--r--actionmailbox/test/dummy/config.ru (renamed from railties/lib/rails/generators/rails/app/templates/config.ru)0
-rw-r--r--actionmailbox/test/dummy/config/application.rb17
-rw-r--r--actionmailbox/test/dummy/config/boot.rb (renamed from railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb)0
-rw-r--r--actionmailbox/test/dummy/config/cable.yml10
-rw-r--r--actionmailbox/test/dummy/config/database.yml25
-rw-r--r--actionmailbox/test/dummy/config/environment.rb (renamed from railties/lib/rails/generators/rails/app/templates/config/environment.rb)0
-rw-r--r--actionmailbox/test/dummy/config/environments/development.rb63
-rw-r--r--actionmailbox/test/dummy/config/environments/production.rb99
-rw-r--r--actionmailbox/test/dummy/config/environments/test.rb46
-rw-r--r--actionmailbox/test/dummy/config/initializers/application_controller_renderer.rb8
-rw-r--r--actionmailbox/test/dummy/config/initializers/assets.rb14
-rw-r--r--actionmailbox/test/dummy/config/initializers/backtrace_silencers.rb (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb)0
-rw-r--r--actionmailbox/test/dummy/config/initializers/content_security_policy.rb22
-rw-r--r--actionmailbox/test/dummy/config/initializers/cookies_serializer.rb (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb)0
-rw-r--r--actionmailbox/test/dummy/config/initializers/filter_parameter_logging.rb (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb)0
-rw-r--r--actionmailbox/test/dummy/config/initializers/inflections.rb (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb)0
-rw-r--r--actionmailbox/test/dummy/config/initializers/mime_types.rb (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb)0
-rw-r--r--actionmailbox/test/dummy/config/initializers/secret_token.rb1
-rw-r--r--actionmailbox/test/dummy/config/initializers/wrap_parameters.rb14
-rw-r--r--actionmailbox/test/dummy/config/locales/en.yml33
-rw-r--r--actionmailbox/test/dummy/config/puma.rb34
-rw-r--r--actionmailbox/test/dummy/config/routes.rb4
-rw-r--r--actionmailbox/test/dummy/config/spring.rb6
-rw-r--r--actionmailbox/test/dummy/config/storage.yml35
-rw-r--r--actionmailbox/test/dummy/config/webpack/development.js3
-rw-r--r--actionmailbox/test/dummy/config/webpack/environment.js3
-rw-r--r--actionmailbox/test/dummy/config/webpack/production.js3
-rw-r--r--actionmailbox/test/dummy/config/webpack/test.js3
-rw-r--r--actionmailbox/test/dummy/config/webpacker.yml65
-rw-r--r--actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb13
-rw-r--r--actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb27
-rw-r--r--actionmailbox/test/dummy/db/schema.rb46
-rw-r--r--actionmailbox/test/dummy/lib/assets/.keep0
-rw-r--r--actionmailbox/test/dummy/log/.keep0
-rw-r--r--actionmailbox/test/dummy/package.json11
-rw-r--r--actionmailbox/test/dummy/public/404.html67
-rw-r--r--actionmailbox/test/dummy/public/422.html67
-rw-r--r--actionmailbox/test/dummy/public/500.html66
-rw-r--r--actionmailbox/test/dummy/public/apple-touch-icon-precomposed.png0
-rw-r--r--actionmailbox/test/dummy/public/apple-touch-icon.png0
-rw-r--r--actionmailbox/test/dummy/public/favicon.ico0
-rw-r--r--actionmailbox/test/dummy/storage/.keep0
-rw-r--r--actionmailbox/test/dummy/yarn.lock7578
-rw-r--r--actionmailbox/test/fixtures/files/avatar1.jpegbin0 -> 20535 bytes
-rw-r--r--actionmailbox/test/fixtures/files/avatar2.jpegbin0 -> 12686 bytes
-rw-r--r--actionmailbox/test/fixtures/files/welcome.eml631
-rw-r--r--actionmailbox/test/generators/mailbox_generator_test.rb85
-rw-r--r--actionmailbox/test/jobs/incineration_job_test.rb17
-rw-r--r--actionmailbox/test/test_helper.rb56
-rw-r--r--actionmailbox/test/unit/inbound_email/incineration_test.rb57
-rw-r--r--actionmailbox/test/unit/inbound_email/message_id_test.rb15
-rw-r--r--actionmailbox/test/unit/inbound_email_test.rb37
-rw-r--r--actionmailbox/test/unit/mail_ext/address_equality_test.rb11
-rw-r--r--actionmailbox/test/unit/mail_ext/address_wrapping_test.rb13
-rw-r--r--actionmailbox/test/unit/mail_ext/recipients_test.rb35
-rw-r--r--actionmailbox/test/unit/mailbox/bouncing_test.rb31
-rw-r--r--actionmailbox/test/unit/mailbox/callbacks_test.rb77
-rw-r--r--actionmailbox/test/unit/mailbox/routing_test.rb31
-rw-r--r--actionmailbox/test/unit/mailbox/state_test.rb51
-rw-r--r--actionmailbox/test/unit/relayer_test.rb100
-rw-r--r--actionmailbox/test/unit/router_test.rb139
-rw-r--r--actionmailer/CHANGELOG.md120
-rw-r--r--actionmailer/MIT-LICENSE2
-rw-r--r--actionmailer/README.rdoc46
-rw-r--r--actionmailer/Rakefile11
-rw-r--r--actionmailer/actionmailer.gemspec44
-rwxr-xr-xactionmailer/bin/test7
-rw-r--r--actionmailer/lib/action_mailer.rb32
-rw-r--r--actionmailer/lib/action_mailer/base.rb375
-rw-r--r--actionmailer/lib/action_mailer/collector.rb8
-rw-r--r--actionmailer/lib/action_mailer/delivery_job.rb33
-rw-r--r--actionmailer/lib/action_mailer/delivery_methods.rb35
-rw-r--r--actionmailer/lib/action_mailer/gem_version.rb4
-rw-r--r--actionmailer/lib/action_mailer/inline_preview_interceptor.rb20
-rw-r--r--actionmailer/lib/action_mailer/log_subscriber.rb12
-rw-r--r--actionmailer/lib/action_mailer/mail_delivery_job.rb38
-rw-r--r--actionmailer/lib/action_mailer/mail_helper.rb4
-rw-r--r--actionmailer/lib/action_mailer/message_delivery.rb98
-rw-r--r--actionmailer/lib/action_mailer/parameterized.rb171
-rw-r--r--actionmailer/lib/action_mailer/preview.rb71
-rw-r--r--actionmailer/lib/action_mailer/railtie.rb41
-rw-r--r--actionmailer/lib/action_mailer/rescuable.rb29
-rw-r--r--actionmailer/lib/action_mailer/test_case.rb46
-rw-r--r--actionmailer/lib/action_mailer/test_helper.rb63
-rw-r--r--actionmailer/lib/action_mailer/version.rb4
-rw-r--r--actionmailer/lib/rails/generators/mailer/mailer_generator.rb25
-rw-r--r--actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb6
-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.rb42
-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.rb408
-rw-r--r--actionmailer/test/caching_test.rb170
-rw-r--r--actionmailer/test/delivery_methods_test.rb28
-rw-r--r--actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.html.erb1
-rw-r--r--actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.text.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/i18n_with_controller_test.rb34
-rw-r--r--actionmailer/test/legacy_delivery_job_test.rb83
-rw-r--r--actionmailer/test/log_subscriber_test.rb34
-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.rb58
-rw-r--r--actionmailer/test/mailers/caching_mailer.rb10
-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.rb127
-rw-r--r--actionmailer/test/parameterized_test.rb92
-rw-r--r--actionmailer/test/test_case_test.rb44
-rw-r--r--actionmailer/test/test_helper_test.rb168
-rw-r--r--actionmailer/test/url_test.rb70
-rw-r--r--actionpack/CHANGELOG.md833
-rw-r--r--actionpack/MIT-LICENSE2
-rw-r--r--actionpack/README.rdoc11
-rw-r--r--actionpack/Rakefile21
-rw-r--r--actionpack/actionpack.gemspec48
-rwxr-xr-xactionpack/bin/test7
-rw-r--r--actionpack/lib/abstract_controller.rb9
-rw-r--r--actionpack/lib/abstract_controller/asset_paths.rb2
-rw-r--r--actionpack/lib/abstract_controller/base.rb47
-rw-r--r--actionpack/lib/abstract_controller/caching.rb14
-rw-r--r--actionpack/lib/abstract_controller/caching/fragments.rb35
-rw-r--r--actionpack/lib/abstract_controller/callbacks.rb83
-rw-r--r--actionpack/lib/abstract_controller/collector.rb8
-rw-r--r--actionpack/lib/abstract_controller/error.rb2
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb49
-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.rb47
-rw-r--r--actionpack/lib/abstract_controller/translation.rb9
-rw-r--r--actionpack/lib/abstract_controller/url_for.rb2
-rw-r--r--actionpack/lib/action_controller.rb32
-rw-r--r--actionpack/lib/action_controller/api.rb50
-rw-r--r--actionpack/lib/action_controller/api/api_rendering.rb2
-rw-r--r--actionpack/lib/action_controller/base.rb37
-rw-r--r--actionpack/lib/action_controller/caching.rb2
-rw-r--r--actionpack/lib/action_controller/form_builder.rb2
-rw-r--r--actionpack/lib/action_controller/log_subscriber.rb21
-rw-r--r--actionpack/lib/action_controller/metal.rb108
-rw-r--r--actionpack/lib/action_controller/metal/basic_implicit_render.rb4
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb32
-rw-r--r--actionpack/lib/action_controller/metal/content_security_policy.rb52
-rw-r--r--actionpack/lib/action_controller/metal/cookies.rb4
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb54
-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.rb31
-rw-r--r--actionpack/lib/action_controller/metal/exceptions.rb46
-rw-r--r--actionpack/lib/action_controller/metal/flash.rb17
-rw-r--r--actionpack/lib/action_controller/metal/force_ssl.rb85
-rw-r--r--actionpack/lib/action_controller/metal/head.rb36
-rw-r--r--actionpack/lib/action_controller/metal/helpers.rb31
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb96
-rw-r--r--actionpack/lib/action_controller/metal/implicit_render.rb31
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb48
-rw-r--r--actionpack/lib/action_controller/metal/live.rb118
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb27
-rw-r--r--actionpack/lib/action_controller/metal/parameter_encoding.rb51
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb120
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb80
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb16
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb114
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb189
-rw-r--r--actionpack/lib/action_controller/metal/rescue.rb27
-rw-r--r--actionpack/lib/action_controller/metal/streaming.rb14
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb573
-rw-r--r--actionpack/lib/action_controller/metal/testing.rb8
-rw-r--r--actionpack/lib/action_controller/metal/url_for.rb14
-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.rb63
-rw-r--r--actionpack/lib/action_controller/template_assertions.rb2
-rw-r--r--actionpack/lib/action_controller/test_case.rb314
-rw-r--r--actionpack/lib/action_dispatch.rb45
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb67
-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.rb32
-rw-r--r--actionpack/lib/action_dispatch/http/filter_redirect.rb8
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb24
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb67
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb113
-rw-r--r--actionpack/lib/action_dispatch/http/mime_types.rb21
-rw-r--r--actionpack/lib/action_dispatch/http/parameter_filter.rb86
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb90
-rw-r--r--actionpack/lib/action_dispatch/http/rack_cache.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb156
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb121
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb28
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb279
-rw-r--r--actionpack/lib/action_dispatch/journey.rb12
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb33
-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.rb25
-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.rb12
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb21
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb73
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb47
-rw-r--r--actionpack/lib/action_dispatch/journey/router/utils.rb41
-rw-r--r--actionpack/lib/action_dispatch/journey/routes.rb5
-rw-r--r--actionpack/lib/action_dispatch/journey/scanner.rb44
-rw-r--r--actionpack/lib/action_dispatch/journey/visitors.rb54
-rw-r--r--actionpack/lib/action_dispatch/middleware/callbacks.rb18
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb341
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb256
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_locks.rb124
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_view.rb56
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb161
-rw-r--r--actionpack/lib/action_dispatch/middleware/executor.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb39
-rw-r--r--actionpack/lib/action_dispatch/middleware/host_authorization.rb103
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb46
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb56
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb52
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb36
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb15
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb44
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb114
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb42
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb135
-rw-r--r--actionpack/lib/action_dispatch/middleware/stack.rb62
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb64
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb80
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb7
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb5
-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.erb24
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb15
-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.erb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb11
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb31
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb71
-rw-r--r--actionpack/lib/action_dispatch/request/utils.rb23
-rw-r--r--actionpack/lib/action_dispatch/routing.rb55
-rw-r--r--actionpack/lib/action_dispatch/routing/endpoint.rb11
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb221
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb667
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb301
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb56
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb344
-rw-r--r--actionpack/lib/action_dispatch/routing/routes_proxy.rb37
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb76
-rw-r--r--actionpack/lib/action_dispatch/system_test_case.rb167
-rw-r--r--actionpack/lib/action_dispatch/system_testing/browser.rb57
-rw-r--r--actionpack/lib/action_dispatch/system_testing/driver.rb66
-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.rb30
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions.rb10
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb18
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb55
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb492
-rw-r--r--actionpack/lib/action_dispatch/testing/request_encoder.rb55
-rw-r--r--actionpack/lib/action_dispatch/testing/test_process.rb41
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.rb38
-rw-r--r--actionpack/lib/action_dispatch/testing/test_response.rb21
-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.rb8
-rw-r--r--actionpack/test/abstract/translation_test.rb26
-rw-r--r--actionpack/test/abstract_unit.rb265
-rw-r--r--actionpack/test/assertions/response_assertions_test.rb38
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb204
-rw-r--r--actionpack/test/controller/api/conditional_get_test.rb20
-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.rb10
-rw-r--r--actionpack/test/controller/api/redirect_to_test.rb4
-rw-r--r--actionpack/test/controller/api/renderers_test.rb30
-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.rb136
-rw-r--r--actionpack/test/controller/caching_test.rb228
-rw-r--r--actionpack/test/controller/content_type_test.rb22
-rw-r--r--actionpack/test/controller/default_url_options_with_before_action_test.rb10
-rw-r--r--actionpack/test/controller/filters_test.rb330
-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.rb96
-rw-r--r--actionpack/test/controller/form_builder_test.rb4
-rw-r--r--actionpack/test/controller/helper_test.rb91
-rw-r--r--actionpack/test/controller/http_basic_authentication_test.rb100
-rw-r--r--actionpack/test/controller/http_digest_authentication_test.rb192
-rw-r--r--actionpack/test/controller/http_token_authentication_test.rb77
-rw-r--r--actionpack/test/controller/integration_test.rb739
-rw-r--r--actionpack/test/controller/live_stream_test.rb140
-rw-r--r--actionpack/test/controller/localized_templates_test.rb4
-rw-r--r--actionpack/test/controller/log_subscriber_test.rb60
-rw-r--r--actionpack/test/controller/metal/renderers_test.rb22
-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.rb197
-rw-r--r--actionpack/test/controller/new_base/bare_metal_test.rb30
-rw-r--r--actionpack/test/controller/new_base/base_test.rb33
-rw-r--r--actionpack/test/controller/new_base/content_negotiation_test.rb21
-rw-r--r--actionpack/test/controller/new_base/content_type_test.rb6
-rw-r--r--actionpack/test/controller/new_base/middleware_test.rb10
-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.rb54
-rw-r--r--actionpack/test/controller/new_base/render_file_test.rb52
-rw-r--r--actionpack/test/controller/new_base/render_html_test.rb8
-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.rb8
-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.rb18
-rw-r--r--actionpack/test/controller/new_base/render_text_test.rb188
-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.rb239
-rw-r--r--actionpack/test/controller/parameters/always_permitted_parameters_test.rb15
-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.rb369
-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.rb234
-rw-r--r--actionpack/test/controller/permitted_params_test.rb4
-rw-r--r--actionpack/test/controller/redirect_test.rb180
-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_test.rb371
-rw-r--r--actionpack/test/controller/render_xml_test.rb26
-rw-r--r--actionpack/test/controller/renderer_test.rb106
-rw-r--r--actionpack/test/controller/renderers_test.rb23
-rw-r--r--actionpack/test/controller/request/test_request_test.rb43
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb425
-rw-r--r--actionpack/test/controller/required_params_test.rb35
-rw-r--r--actionpack/test/controller/rescue_test.rb142
-rw-r--r--actionpack/test/controller/resources_test.rb769
-rw-r--r--actionpack/test/controller/routing_test.rb1362
-rw-r--r--actionpack/test/controller/runner_test.rb10
-rw-r--r--actionpack/test/controller/send_file_test.rb135
-rw-r--r--actionpack/test/controller/show_exceptions_test.rb67
-rw-r--r--actionpack/test/controller/streaming_test.rb6
-rw-r--r--actionpack/test/controller/test_case_test.rb668
-rw-r--r--actionpack/test/controller/url_for_integration_test.rb265
-rw-r--r--actionpack/test/controller/url_for_test.rb333
-rw-r--r--actionpack/test/controller/url_rewriter_test.rb61
-rw-r--r--actionpack/test/controller/webservice_test.rb50
-rw-r--r--actionpack/test/dispatch/callbacks_test.rb27
-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.rb877
-rw-r--r--actionpack/test/dispatch/debug_exceptions_test.rb380
-rw-r--r--actionpack/test/dispatch/debug_locks_test.rb38
-rw-r--r--actionpack/test/dispatch/exception_wrapper_test.rb71
-rw-r--r--actionpack/test/dispatch/executor_test.rb14
-rw-r--r--actionpack/test/dispatch/header_test.rb62
-rw-r--r--actionpack/test/dispatch/host_authorization_test.rb161
-rw-r--r--actionpack/test/dispatch/live_response_test.rb52
-rw-r--r--actionpack/test/dispatch/mapper_test.rb79
-rw-r--r--actionpack/test/dispatch/middleware_stack_test.rb94
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb161
-rw-r--r--actionpack/test/dispatch/mount_test.rb30
-rw-r--r--actionpack/test/dispatch/prefix_generation_test.rb238
-rw-r--r--actionpack/test/dispatch/rack_cache_test.rb8
-rw-r--r--actionpack/test/dispatch/reloader_test.rb134
-rw-r--r--actionpack/test/dispatch/request/json_params_parsing_test.rb112
-rw-r--r--actionpack/test/dispatch/request/multipart_params_parsing_test.rb116
-rw-r--r--actionpack/test/dispatch/request/query_string_parsing_test.rb68
-rw-r--r--actionpack/test/dispatch/request/session_test.rb119
-rw-r--r--actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb14
-rw-r--r--actionpack/test/dispatch/request_id_test.rb50
-rw-r--r--actionpack/test/dispatch/request_test.rb901
-rw-r--r--actionpack/test/dispatch/response_test.rb349
-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.rb184
-rw-r--r--actionpack/test/dispatch/routing/ipv6_redirect_test.rb25
-rw-r--r--actionpack/test/dispatch/routing/non_dispatch_routed_app_test.rb27
-rw-r--r--actionpack/test/dispatch/routing/route_set_test.rb89
-rw-r--r--actionpack/test/dispatch/routing_assertions_test.rb150
-rw-r--r--actionpack/test/dispatch/routing_test.rb3991
-rw-r--r--actionpack/test/dispatch/runner_test.rb19
-rw-r--r--actionpack/test/dispatch/session/abstract_store_test.rb16
-rw-r--r--actionpack/test/dispatch/session/cache_store_test.rb92
-rw-r--r--actionpack/test/dispatch/session/cookie_store_test.rb253
-rw-r--r--actionpack/test/dispatch/session/mem_cache_store_test.rb97
-rw-r--r--actionpack/test/dispatch/session/test_session_test.rb46
-rw-r--r--actionpack/test/dispatch/show_exceptions_test.rb45
-rw-r--r--actionpack/test/dispatch/ssl_test.rb200
-rw-r--r--actionpack/test/dispatch/static_test.rb118
-rw-r--r--actionpack/test/dispatch/system_testing/driver_test.rb123
-rw-r--r--actionpack/test/dispatch/system_testing/screenshot_helper_test.rb80
-rw-r--r--actionpack/test/dispatch/system_testing/server_test.rb32
-rw-r--r--actionpack/test/dispatch/system_testing/system_test_case_test.rb85
-rw-r--r--actionpack/test/dispatch/test_request_test.rb70
-rw-r--r--actionpack/test/dispatch/test_response_test.rb12
-rw-r--r--actionpack/test/dispatch/uploaded_file_test.rb129
-rw-r--r--actionpack/test/dispatch/url_generation_test.rb26
-rw-r--r--actionpack/test/fixtures/alternate_helpers/foo_helper.rb4
-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.erb2
-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/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/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/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.rb8
-rw-r--r--actionpack/test/journey/path/pattern_test.rb194
-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.rb74
-rw-r--r--actionpack/test/journey/router/utils_test.rb15
-rw-r--r--actionpack/test/journey/router_test.rb329
-rw-r--r--actionpack/test/journey/routes_test.rb30
-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--actiontext/.gitignore5
-rw-r--r--actiontext/CHANGELOG.md15
-rw-r--r--actiontext/MIT-LICENSE21
-rw-r--r--actiontext/README.md9
-rw-r--r--actiontext/Rakefile15
-rw-r--r--actiontext/actiontext.gemspec37
-rw-r--r--actiontext/app/helpers/action_text/content_helper.rb32
-rw-r--r--actiontext/app/helpers/action_text/tag_helper.rb79
-rw-r--r--actiontext/app/javascript/actiontext/attachment_upload.js45
-rw-r--r--actiontext/app/javascript/actiontext/index.js10
-rw-r--r--actiontext/app/models/action_text/rich_text.rb29
-rw-r--r--actiontext/app/views/action_text/attachables/_missing_attachable.html.erb1
-rw-r--r--actiontext/app/views/action_text/attachables/_remote_image.html.erb8
-rw-r--r--actiontext/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb3
-rw-r--r--actiontext/app/views/action_text/content/_layout.html.erb3
-rw-r--r--actiontext/app/views/active_storage/blobs/_blob.html.erb14
-rwxr-xr-xactiontext/bin/test5
-rwxr-xr-xactiontext/bin/webpack29
-rwxr-xr-xactiontext/bin/webpack-dev-server29
-rw-r--r--actiontext/db/migrate/20180528164100_create_action_text_tables.rb13
-rw-r--r--actiontext/lib/action_text.rb37
-rw-r--r--actiontext/lib/action_text/attachable.rb86
-rw-r--r--actiontext/lib/action_text/attachables/content_attachment.rb38
-rw-r--r--actiontext/lib/action_text/attachables/missing_attachable.rb13
-rw-r--r--actiontext/lib/action_text/attachables/remote_image.rb46
-rw-r--r--actiontext/lib/action_text/attachment.rb103
-rw-r--r--actiontext/lib/action_text/attachment_gallery.rb65
-rw-r--r--actiontext/lib/action_text/attachments/caching.rb16
-rw-r--r--actiontext/lib/action_text/attachments/minification.rb17
-rw-r--r--actiontext/lib/action_text/attachments/trix_conversion.rb34
-rw-r--r--actiontext/lib/action_text/attribute.rb45
-rw-r--r--actiontext/lib/action_text/content.rb132
-rw-r--r--actiontext/lib/action_text/engine.rb50
-rw-r--r--actiontext/lib/action_text/fragment.rb57
-rw-r--r--actiontext/lib/action_text/gem_version.rb17
-rw-r--r--actiontext/lib/action_text/html_conversion.rb24
-rw-r--r--actiontext/lib/action_text/plain_text_conversion.rb81
-rw-r--r--actiontext/lib/action_text/serialization.rb34
-rw-r--r--actiontext/lib/action_text/trix_attachment.rb92
-rw-r--r--actiontext/lib/action_text/version.rb10
-rw-r--r--actiontext/lib/tasks/actiontext.rake20
-rw-r--r--actiontext/lib/templates/actiontext.scss36
-rw-r--r--actiontext/lib/templates/fixtures.yml4
-rw-r--r--actiontext/lib/templates/installer.rb32
-rw-r--r--actiontext/package.json29
-rw-r--r--actiontext/test/dummy/.babelrc18
-rw-r--r--actiontext/test/dummy/.postcssrc.yml3
-rw-r--r--actiontext/test/dummy/Rakefile6
-rw-r--r--actiontext/test/dummy/app/assets/config/manifest.js3
-rw-r--r--actiontext/test/dummy/app/assets/images/.keep0
-rw-r--r--actiontext/test/dummy/app/assets/stylesheets/application.css16
-rw-r--r--actiontext/test/dummy/app/assets/stylesheets/messages.css4
-rw-r--r--actiontext/test/dummy/app/assets/stylesheets/scaffold.css80
-rw-r--r--actiontext/test/dummy/app/channels/application_cable/channel.rb4
-rw-r--r--actiontext/test/dummy/app/channels/application_cable/connection.rb4
-rw-r--r--actiontext/test/dummy/app/controllers/application_controller.rb2
-rw-r--r--actiontext/test/dummy/app/controllers/concerns/.keep0
-rw-r--r--actiontext/test/dummy/app/controllers/messages_controller.rb58
-rw-r--r--actiontext/test/dummy/app/helpers/application_helper.rb2
-rw-r--r--actiontext/test/dummy/app/helpers/messages_helper.rb2
-rw-r--r--actiontext/test/dummy/app/javascript/packs/application.js1
-rw-r--r--actiontext/test/dummy/app/jobs/application_job.rb2
-rw-r--r--actiontext/test/dummy/app/mailers/application_mailer.rb4
-rw-r--r--actiontext/test/dummy/app/models/application_record.rb3
-rw-r--r--actiontext/test/dummy/app/models/concerns/.keep0
-rw-r--r--actiontext/test/dummy/app/models/message.rb7
-rw-r--r--actiontext/test/dummy/app/models/page.rb4
-rw-r--r--actiontext/test/dummy/app/models/person.rb7
-rw-r--r--actiontext/test/dummy/app/models/review.rb5
-rw-r--r--actiontext/test/dummy/app/views/layouts/application.html.erb14
-rw-r--r--actiontext/test/dummy/app/views/layouts/mailer.html.erb13
-rw-r--r--actiontext/test/dummy/app/views/layouts/mailer.text.erb1
-rw-r--r--actiontext/test/dummy/app/views/messages/_form.html.erb27
-rw-r--r--actiontext/test/dummy/app/views/messages/edit.html.erb6
-rw-r--r--actiontext/test/dummy/app/views/messages/index.html.erb29
-rw-r--r--actiontext/test/dummy/app/views/messages/new.html.erb5
-rw-r--r--actiontext/test/dummy/app/views/messages/show.html.erb13
-rw-r--r--actiontext/test/dummy/app/views/people/_trix_content_attachment.html.erb3
-rwxr-xr-xactiontext/test/dummy/bin/bundle3
-rwxr-xr-xactiontext/test/dummy/bin/rails4
-rwxr-xr-xactiontext/test/dummy/bin/rake4
-rwxr-xr-xactiontext/test/dummy/bin/setup36
-rwxr-xr-xactiontext/test/dummy/bin/update31
-rwxr-xr-xactiontext/test/dummy/bin/yarn11
-rw-r--r--actiontext/test/dummy/config.ru5
-rw-r--r--actiontext/test/dummy/config/application.rb19
-rw-r--r--actiontext/test/dummy/config/boot.rb5
-rw-r--r--actiontext/test/dummy/config/cable.yml10
-rw-r--r--actiontext/test/dummy/config/database.yml25
-rw-r--r--actiontext/test/dummy/config/environment.rb5
-rw-r--r--actiontext/test/dummy/config/environments/development.rb63
-rw-r--r--actiontext/test/dummy/config/environments/production.rb96
-rw-r--r--actiontext/test/dummy/config/environments/test.rb46
-rw-r--r--actiontext/test/dummy/config/initializers/application_controller_renderer.rb8
-rw-r--r--actiontext/test/dummy/config/initializers/assets.rb14
-rw-r--r--actiontext/test/dummy/config/initializers/backtrace_silencers.rb7
-rw-r--r--actiontext/test/dummy/config/initializers/content_security_policy.rb22
-rw-r--r--actiontext/test/dummy/config/initializers/cookies_serializer.rb5
-rw-r--r--actiontext/test/dummy/config/initializers/filter_parameter_logging.rb4
-rw-r--r--actiontext/test/dummy/config/initializers/inflections.rb16
-rw-r--r--actiontext/test/dummy/config/initializers/mime_types.rb4
-rw-r--r--actiontext/test/dummy/config/initializers/wrap_parameters.rb14
-rw-r--r--actiontext/test/dummy/config/locales/en.yml33
-rw-r--r--actiontext/test/dummy/config/puma.rb34
-rw-r--r--actiontext/test/dummy/config/routes.rb4
-rw-r--r--actiontext/test/dummy/config/spring.rb6
-rw-r--r--actiontext/test/dummy/config/storage.yml35
-rw-r--r--actiontext/test/dummy/config/webpack/development.js3
-rw-r--r--actiontext/test/dummy/config/webpack/environment.js3
-rw-r--r--actiontext/test/dummy/config/webpack/production.js3
-rw-r--r--actiontext/test/dummy/config/webpack/test.js3
-rw-r--r--actiontext/test/dummy/config/webpacker.yml65
-rw-r--r--actiontext/test/dummy/db/migrate/20180208205311_create_messages.rb8
-rw-r--r--actiontext/test/dummy/db/migrate/20180212164506_create_active_storage_tables.rb27
-rw-r--r--actiontext/test/dummy/db/migrate/20180528164100_create_action_text_tables.rb13
-rw-r--r--actiontext/test/dummy/db/migrate/20181003185713_create_people.rb9
-rw-r--r--actiontext/test/dummy/db/migrate/20190305172303_create_pages.rb9
-rw-r--r--actiontext/test/dummy/db/migrate/20190317200724_create_reviews.rb8
-rw-r--r--actiontext/test/dummy/db/schema.rb71
-rw-r--r--actiontext/test/dummy/lib/assets/.keep0
-rw-r--r--actiontext/test/dummy/log/.keep0
-rw-r--r--actiontext/test/dummy/package.json11
-rw-r--r--actiontext/test/dummy/public/404.html67
-rw-r--r--actiontext/test/dummy/public/422.html67
-rw-r--r--actiontext/test/dummy/public/500.html66
-rw-r--r--actiontext/test/dummy/public/apple-touch-icon-precomposed.png0
-rw-r--r--actiontext/test/dummy/public/apple-touch-icon.png0
-rw-r--r--actiontext/test/dummy/public/favicon.ico0
-rw-r--r--actiontext/test/dummy/storage/.keep0
-rw-r--r--actiontext/test/dummy/tmp/.keep0
-rw-r--r--actiontext/test/dummy/tmp/storage/.keep0
-rw-r--r--actiontext/test/dummy/yarn.lock7592
-rw-r--r--actiontext/test/fixtures/files/racecar.jpgbin0 -> 1124062 bytes
-rw-r--r--actiontext/test/template/form_helper_test.rb113
-rw-r--r--actiontext/test/test_helper.rb33
-rw-r--r--actiontext/test/unit/attachment_test.rb66
-rw-r--r--actiontext/test/unit/content_test.rb116
-rw-r--r--actiontext/test/unit/model_test.rb78
-rw-r--r--actiontext/test/unit/plain_text_conversion_test.rb94
-rw-r--r--actiontext/test/unit/trix_attachment_test.rb83
-rw-r--r--actionview/.gitignore5
-rw-r--r--actionview/CHANGELOG.md349
-rw-r--r--actionview/MIT-LICENSE2
-rw-r--r--actionview/README.rdoc10
-rw-r--r--actionview/RUNNING_UJS_TESTS.rdoc8
-rw-r--r--actionview/RUNNING_UNIT_TESTS.rdoc15
-rw-r--r--actionview/Rakefile132
-rw-r--r--actionview/actionview.gemspec48
-rw-r--r--actionview/app/assets/javascripts/MIT-LICENSE20
-rw-r--r--actionview/app/assets/javascripts/README.md56
-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.coffee93
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/features/method.coffee34
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/features/remote.coffee93
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/start.coffee75
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee97
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee8
-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.rb18
-rw-r--r--actionview/lib/action_view/base.rb169
-rw-r--r--actionview/lib/action_view/buffers.rb19
-rw-r--r--actionview/lib/action_view/cache_expiry.rb49
-rw-r--r--actionview/lib/action_view/context.rb20
-rw-r--r--actionview/lib/action_view/dependency_tracker.rb11
-rw-r--r--actionview/lib/action_view/digestor.rb48
-rw-r--r--actionview/lib/action_view/file_template.rb33
-rw-r--r--actionview/lib/action_view/flows.rb22
-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.rb311
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb193
-rw-r--r--actionview/lib/action_view/helpers/atom_feed_helper.rb29
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb106
-rw-r--r--actionview/lib/action_view/helpers/capture_helper.rb24
-rw-r--r--actionview/lib/action_view/helpers/controller_helper.rb18
-rw-r--r--actionview/lib/action_view/helpers/csp_helper.rb26
-rw-r--r--actionview/lib/action_view/helpers/csrf_helper.rb14
-rw-r--r--actionview/lib/action_view/helpers/date_helper.rb321
-rw-r--r--actionview/lib/action_view/helpers/debug_helper.rb11
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb822
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb107
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb134
-rw-r--r--actionview/lib/action_view/helpers/javascript_helper.rb34
-rw-r--r--actionview/lib/action_view/helpers/number_helper.rb110
-rw-r--r--actionview/lib/action_view/helpers/output_safety_helper.rb38
-rw-r--r--actionview/lib/action_view/helpers/record_tag_helper.rb21
-rw-r--r--actionview/lib/action_view/helpers/rendering_helper.rb17
-rw-r--r--actionview/lib/action_view/helpers/sanitize_helper.rb38
-rw-r--r--actionview/lib/action_view/helpers/tag_helper.rb276
-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.rb19
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_helpers.rb122
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb7
-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.rb2
-rw-r--r--actionview/lib/action_view/helpers/text_helper.rb68
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb45
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb137
-rw-r--r--actionview/lib/action_view/layouts.rb118
-rw-r--r--actionview/lib/action_view/log_subscriber.rb44
-rw-r--r--actionview/lib/action_view/lookup_context.rb137
-rw-r--r--actionview/lib/action_view/model_naming.rb2
-rw-r--r--actionview/lib/action_view/path_set.rb40
-rw-r--r--actionview/lib/action_view/railtie.rb61
-rw-r--r--actionview/lib/action_view/record_identifier.rb14
-rw-r--r--actionview/lib/action_view/renderer/abstract_renderer.rb85
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb427
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb69
-rw-r--r--actionview/lib/action_view/renderer/renderer.rb24
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb102
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb150
-rw-r--r--actionview/lib/action_view/rendering.rb102
-rw-r--r--actionview/lib/action_view/routing_url_for.rb47
-rw-r--r--actionview/lib/action_view/tasks/cache_digests.rake16
-rw-r--r--actionview/lib/action_view/template.rb196
-rw-r--r--actionview/lib/action_view/template/error.rb47
-rw-r--r--actionview/lib/action_view/template/handlers.rb40
-rw-r--r--actionview/lib/action_view/template/handlers/builder.rb25
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb109
-rw-r--r--actionview/lib/action_view/template/handlers/erb/erubi.rb87
-rw-r--r--actionview/lib/action_view/template/handlers/html.rb4
-rw-r--r--actionview/lib/action_view/template/handlers/raw.rb6
-rw-r--r--actionview/lib/action_view/template/html.rb29
-rw-r--r--actionview/lib/action_view/template/inline.rb22
-rw-r--r--actionview/lib/action_view/template/raw_file.rb28
-rw-r--r--actionview/lib/action_view/template/resolver.rb372
-rw-r--r--actionview/lib/action_view/template/text.rb21
-rw-r--r--actionview/lib/action_view/template/types.rb6
-rw-r--r--actionview/lib/action_view/test_case.rb64
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb66
-rw-r--r--actionview/lib/action_view/version.rb4
-rw-r--r--actionview/lib/action_view/view_paths.rb56
-rw-r--r--actionview/package.json36
-rw-r--r--actionview/test/abstract_unit.rb216
-rw-r--r--actionview/test/actionpack/abstract/abstract_controller_test.rb64
-rw-r--r--actionview/test/actionpack/abstract/helper_test.rb17
-rw-r--r--actionview/test/actionpack/abstract/layouts_test.rb254
-rw-r--r--actionview/test/actionpack/abstract/render_test.rb12
-rw-r--r--actionview/test/actionpack/controller/capture_test.rb25
-rw-r--r--actionview/test/actionpack/controller/layout_test.rb99
-rw-r--r--actionview/test/actionpack/controller/render_test.rb504
-rw-r--r--actionview/test/actionpack/controller/view_paths_test.rb49
-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.rb37
-rw-r--r--actionview/test/activerecord/polymorphic_routes_test.rb133
-rw-r--r--actionview/test/activerecord/relation_cache_test.rb21
-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/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/show.js.erb1
-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/streaming_with_locale.erb2
-rw-r--r--actionview/test/fixtures/mascot.rb4
-rw-r--r--actionview/test/fixtures/project.rb2
-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/_first.html.erb1
-rw-r--r--actionview/test/fixtures/test/_first.xml.erb1
-rw-r--r--actionview/test/fixtures/test/_first_layer.html.erb4
-rw-r--r--actionview/test/fixtures/test/_first_layer.xml.erb4
-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/_second.html.erb1
-rw-r--r--actionview/test/fixtures/test/_second.xml.erb1
-rw-r--r--actionview/test/fixtures/test/_second_layer.html.erb4
-rw-r--r--actionview/test/fixtures/test/_second_layer.xml.erb4
-rw-r--r--actionview/test/fixtures/test/_ðŸ£.erb1
-rw-r--r--actionview/test/fixtures/test/hello.builder4
-rw-r--r--actionview/test/fixtures/test/layout_render_file.erb2
-rw-r--r--actionview/test/fixtures/test/mixing_formats.html.erb5
-rw-r--r--actionview/test/fixtures/test/mixing_formats_deep.html.erb5
-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/syntax_error.html.erb4
-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.rb89
-rw-r--r--actionview/test/template/asset_tag_helper_test.rb249
-rw-r--r--actionview/test/template/atom_feed_helper_test.rb64
-rw-r--r--actionview/test/template/capture_helper_test.rb144
-rw-r--r--actionview/test/template/compiled_templates_test.rb72
-rw-r--r--actionview/test/template/controller_helper_test.rb19
-rw-r--r--actionview/test/template/csp_helper_test.rb35
-rw-r--r--actionview/test/template/csrf_helper_test.rb46
-rw-r--r--actionview/test/template/date_helper_i18n_test.rb106
-rw-r--r--actionview/test/template/date_helper_test.rb1463
-rw-r--r--actionview/test/template/dependency_tracker_test.rb10
-rw-r--r--actionview/test/template/digestor_test.rb144
-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/fallback_file_system_resolver_test.rb16
-rw-r--r--actionview/test/template/form_collections_helper_test.rb451
-rw-r--r--actionview/test/template/form_helper/form_with_test.rb2360
-rw-r--r--actionview/test/template/form_helper_test.rb1222
-rw-r--r--actionview/test/template/form_options_helper_i18n_test.rb14
-rw-r--r--actionview/test/template/form_options_helper_test.rb690
-rw-r--r--actionview/test/template/form_tag_helper_test.rb272
-rw-r--r--actionview/test/template/html_test.rb18
-rw-r--r--actionview/test/template/javascript_helper_test.rb36
-rw-r--r--actionview/test/template/log_subscriber_test.rb146
-rw-r--r--actionview/test/template/lookup_context_test.rb132
-rw-r--r--actionview/test/template/number_helper_test.rb140
-rw-r--r--actionview/test/template/output_safety_helper_test.rb96
-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.rb438
-rw-r--r--actionview/test/template/resolver_cache_test.rb10
-rw-r--r--actionview/test/template/resolver_patterns_test.rb21
-rw-r--r--actionview/test/template/sanitize_helper_test.rb16
-rw-r--r--actionview/test/template/streaming_render_test.rb66
-rw-r--r--actionview/test/template/tag_helper_test.rb249
-rw-r--r--actionview/test/template/template_error_test.rb2
-rw-r--r--actionview/test/template/template_test.rb67
-rw-r--r--actionview/test/template/test_case_test.rb138
-rw-r--r--actionview/test/template/test_test.rb10
-rw-r--r--actionview/test/template/testing/fixture_resolver_test.rb10
-rw-r--r--actionview/test/template/testing/null_resolver_test.rb8
-rw-r--r--actionview/test/template/text_helper_test.rb250
-rw-r--r--actionview/test/template/text_test.rb26
-rw-r--r--actionview/test/template/translation_helper_test.rb120
-rw-r--r--actionview/test/template/url_helper_test.rb317
-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.js434
-rw-r--r--actionview/test/ujs/public/test/data-disable.js358
-rw-r--r--actionview/test/ujs/public/test/data-method.js85
-rw-r--r--actionview/test/ujs/public/test/data-remote.js479
-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.rb92
-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.md178
-rw-r--r--activejob/MIT-LICENSE2
-rw-r--r--activejob/README.md31
-rw-r--r--activejob/Rakefile55
-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.rb106
-rw-r--r--activejob/lib/active_job/base.rb26
-rw-r--r--activejob/lib/active_job/callbacks.rb15
-rw-r--r--activejob/lib/active_job/configured_job.rb4
-rw-r--r--activejob/lib/active_job/core.rb121
-rw-r--r--activejob/lib/active_job/enqueuing.rb72
-rw-r--r--activejob/lib/active_job/exceptions.rb150
-rw-r--r--activejob/lib/active_job/execution.rb13
-rw-r--r--activejob/lib/active_job/gem_version.rb4
-rw-r--r--activejob/lib/active_job/logging.rb160
-rw-r--r--activejob/lib/active_job/queue_adapter.rb53
-rw-r--r--activejob/lib/active_job/queue_adapters.rb25
-rw-r--r--activejob/lib/active_job/queue_adapters/async_adapter.rb12
-rw-r--r--activejob/lib/active_job/queue_adapters/backburner_adapter.rb8
-rw-r--r--activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb8
-rw-r--r--activejob/lib/active_job/queue_adapters/inline_adapter.rb4
-rw-r--r--activejob/lib/active_job/queue_adapters/qu_adapter.rb44
-rw-r--r--activejob/lib/active_job/queue_adapters/que_adapter.rb4
-rw-r--r--activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb10
-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.rb8
-rw-r--r--activejob/lib/active_job/queue_adapters/test_adapter.rb53
-rw-r--r--activejob/lib/active_job/queue_name.rb36
-rw-r--r--activejob/lib/active_job/queue_priority.rb11
-rw-r--r--activejob/lib/active_job/railtie.rb25
-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.rb517
-rw-r--r--activejob/lib/active_job/timezones.rb13
-rw-r--r--activejob/lib/active_job/translation.rb4
-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.rb2
-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.rb129
-rw-r--r--activejob/test/cases/callbacks_test.rb37
-rw-r--r--activejob/test/cases/exceptions_test.rb170
-rw-r--r--activejob/test/cases/job_serialization_test.rb68
-rw-r--r--activejob/test/cases/logging_test.rb154
-rw-r--r--activejob/test/cases/queue_adapter_test.rb27
-rw-r--r--activejob/test/cases/queue_naming_test.rb73
-rw-r--r--activejob/test/cases/queue_priority_test.rb24
-rw-r--r--activejob/test/cases/queuing_test.rb38
-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.rb1487
-rw-r--r--activejob/test/cases/timezones_test.rb24
-rw-r--r--activejob/test/cases/translation_test.rb10
-rw-r--r--activejob/test/helper.rb18
-rw-r--r--activejob/test/integration/queuing_test.rb117
-rw-r--r--activejob/test/jobs/abort_before_enqueue_job.rb9
-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.rb4
-rw-r--r--activejob/test/jobs/rescue_job.rb12
-rw-r--r--activejob/test/jobs/retry_job.rb48
-rw-r--r--activejob/test/jobs/timezone_dependent_job.rb22
-rw-r--r--activejob/test/jobs/translated_hello_job.rb6
-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.rb2
-rw-r--r--activejob/test/support/integration/adapters/backburner.rb11
-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.rb22
-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.rb18
-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.rb31
-rw-r--r--activejob/test/support/job_buffer.rb2
-rw-r--r--activejob/test/support/que/inline.rb6
-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.md186
-rw-r--r--activemodel/MIT-LICENSE2
-rw-r--r--activemodel/README.rdoc10
-rw-r--r--activemodel/Rakefile15
-rw-r--r--activemodel/activemodel.gemspec34
-rwxr-xr-xactivemodel/bin/test7
-rw-r--r--activemodel/lib/active_model.rb27
-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.rb86
-rw-r--r--activemodel/lib/active_model/attribute_mutation_tracker.rb119
-rw-r--r--activemodel/lib/active_model/attribute_set.rb106
-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.rb51
-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.rb335
-rw-r--r--activemodel/lib/active_model/forbidden_attributes_protection.rb4
-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.rb43
-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.rb8
-rw-r--r--activemodel/lib/active_model/serializers/json.rb28
-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.rb54
-rw-r--r--activemodel/lib/active_model/type/big_integer.rb10
-rw-r--r--activemodel/lib/active_model/type/binary.rb4
-rw-r--r--activemodel/lib/active_model/type/boolean.rb44
-rw-r--r--activemodel/lib/active_model/type/date.rb55
-rw-r--r--activemodel/lib/active_model/type/date_time.rb45
-rw-r--r--activemodel/lib/active_model/type/decimal.rb76
-rw-r--r--activemodel/lib/active_model/type/decimal_without_scale.rb11
-rw-r--r--activemodel/lib/active_model/type/float.rb27
-rw-r--r--activemodel/lib/active_model/type/helpers.rb11
-rw-r--r--activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb14
-rw-r--r--activemodel/lib/active_model/type/helpers/mutable.rb6
-rw-r--r--activemodel/lib/active_model/type/helpers/numeric.rb44
-rw-r--r--activemodel/lib/active_model/type/helpers/time_value.rb67
-rw-r--r--activemodel/lib/active_model/type/helpers/timezone.rb19
-rw-r--r--activemodel/lib/active_model/type/immutable_string.rb19
-rw-r--r--activemodel/lib/active_model/type/integer.rb55
-rw-r--r--activemodel/lib/active_model/type/registry.rb24
-rw-r--r--activemodel/lib/active_model/type/string.rb13
-rw-r--r--activemodel/lib/active_model/type/text.rb11
-rw-r--r--activemodel/lib/active_model/type/time.rb29
-rw-r--r--activemodel/lib/active_model/type/unsigned_integer.rb15
-rw-r--r--activemodel/lib/active_model/type/value.rb22
-rw-r--r--activemodel/lib/active_model/validations.rb33
-rw-r--r--activemodel/lib/active_model/validations/absence.rb4
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb87
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb37
-rw-r--r--activemodel/lib/active_model/validations/clusivity.rb18
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb35
-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.rb60
-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.rb27
-rw-r--r--activemodel/lib/active_model/version.rb4
-rw-r--r--activemodel/test/cases/attribute_assignment_test.rb19
-rw-r--r--activemodel/test/cases/attribute_methods_test.rb178
-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.rb344
-rw-r--r--activemodel/test/cases/forbidden_attributes_protection_test.rb14
-rw-r--r--activemodel/test/cases/helper.rb31
-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.rb151
-rw-r--r--activemodel/test/cases/serialization_test.rb89
-rw-r--r--activemodel/test/cases/serializers/json_serialization_test.rb76
-rw-r--r--activemodel/test/cases/translation_test.rb86
-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.rb54
-rw-r--r--activemodel/test/cases/type/date_test.rb34
-rw-r--r--activemodel/test/cases/type/date_time_test.rb50
-rw-r--r--activemodel/test/cases/type/decimal_test.rb30
-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.rb66
-rw-r--r--activemodel/test/cases/type/registry_test.rb55
-rw-r--r--activemodel/test/cases/type/string_test.rb57
-rw-r--r--activemodel/test/cases/type/time_test.rb38
-rw-r--r--activemodel/test/cases/type/unsigned_integer_test.rb18
-rw-r--r--activemodel/test/cases/type/value_test.rb15
-rw-r--r--activemodel/test/cases/types_test.rb125
-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.rb79
-rw-r--r--activemodel/test/cases/validations/exclusion_validation_test.rb47
-rw-r--r--activemodel/test/cases/validations/format_validation_test.rb51
-rw-r--r--activemodel/test/cases/validations/i18n_generate_message_validation_test.rb48
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb207
-rw-r--r--activemodel/test/cases/validations/inclusion_validation_test.rb103
-rw-r--r--activemodel/test/cases/validations/length_validation_test.rb296
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb193
-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.rb16
-rw-r--r--activemodel/test/cases/validations/with_validation_test.rb77
-rw-r--r--activemodel/test/cases/validations_test.rb128
-rw-r--r--activemodel/test/models/account.rb2
-rw-r--r--activemodel/test/models/blog_post.rb2
-rw-r--r--activemodel/test/models/contact.rb2
-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.md2118
-rw-r--r--activerecord/MIT-LICENSE4
-rw-r--r--activerecord/README.rdoc12
-rw-r--r--activerecord/RUNNING_UNIT_TESTS.rdoc6
-rw-r--r--activerecord/Rakefile200
-rw-r--r--activerecord/activerecord.gemspec38
-rwxr-xr-xactiverecord/bin/test18
-rw-r--r--activerecord/examples/.gitignore1
-rw-r--r--activerecord/examples/performance.rb60
-rw-r--r--activerecord/examples/simple.rb8
-rw-r--r--activerecord/lib/active_record.rb62
-rw-r--r--activerecord/lib/active_record/aggregations.rb494
-rw-r--r--activerecord/lib/active_record/association_relation.rb15
-rw-r--r--activerecord/lib/active_record/associations.rb3194
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb57
-rw-r--r--activerecord/lib/active_record/associations/association.rb169
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb190
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb99
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb20
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb11
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb90
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb11
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb105
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb13
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb414
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb321
-rw-r--r--activerecord/lib/active_record/associations/foreign_association.rb9
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb60
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb106
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb113
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb31
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb305
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb121
-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.rb192
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb205
-rw-r--r--activerecord/lib/active_record/associations/preloader/belongs_to.rb17
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb18
-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.rb15
-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.rb157
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb63
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb42
-rw-r--r--activerecord/lib/active_record/attribute.rb207
-rw-r--r--activerecord/lib/active_record/attribute/user_provided_default.rb28
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb114
-rw-r--r--activerecord/lib/active_record/attribute_decorators.rb53
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb237
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb16
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb271
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb166
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb5
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb76
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb37
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb103
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb72
-rw-r--r--activerecord/lib/active_record/attribute_mutation_tracker.rb70
-rw-r--r--activerecord/lib/active_record/attribute_set.rb108
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb108
-rw-r--r--activerecord/lib/active_record/attributes.rb73
-rw-r--r--activerecord/lib/active_record/autosave_association.rb93
-rw-r--r--activerecord/lib/active_record/base.rb52
-rw-r--r--activerecord/lib/active_record/callbacks.rb102
-rw-r--r--activerecord/lib/active_record/coders/json.rb4
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb22
-rw-r--r--activerecord/lib/active_record/collection_cache_key.rb37
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb852
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb316
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb100
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb138
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb76
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb336
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb146
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb574
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb231
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb558
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb1001
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb39
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb309
-rw-r--r--activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/column.rb39
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb201
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb46
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/quoting.rb31
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb103
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb114
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb112
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb261
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb125
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb110
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb45
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb46
-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.rb6
-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.rb46
-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.rb10
-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.rb8
-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.rb40
-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.rb64
-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.rb13
-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.rb118
-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.rb76
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb252
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb67
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb712
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/utils.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb701
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/sql_type_metadata.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb38
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb65
-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.rb137
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb530
-rw-r--r--activerecord/lib/active_record/connection_adapters/statement_pool.rb26
-rw-r--r--activerecord/lib/active_record/connection_handling.rb198
-rw-r--r--activerecord/lib/active_record/core.rb356
-rw-r--r--activerecord/lib/active_record/counter_cache.rb85
-rw-r--r--activerecord/lib/active_record/database_configurations.rb204
-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.rb79
-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.rb83
-rw-r--r--activerecord/lib/active_record/errors.rb136
-rw-r--r--activerecord/lib/active_record/explain.rb34
-rw-r--r--activerecord/lib/active_record/explain_registry.rb4
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb13
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb21
-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.rb692
-rw-r--r--activerecord/lib/active_record/gem_version.rb4
-rw-r--r--activerecord/lib/active_record/inheritance.rb248
-rw-r--r--activerecord/lib/active_record/insert_all.rb178
-rw-r--r--activerecord/lib/active_record/integration.rb136
-rw-r--r--activerecord/lib/active_record/internal_metadata.rb27
-rw-r--r--activerecord/lib/active_record/legacy_yaml_adapter.rb6
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb170
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb24
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb85
-rw-r--r--activerecord/lib/active_record/middleware/database_selector.rb75
-rw-r--r--activerecord/lib/active_record/middleware/database_selector/resolver.rb90
-rw-r--r--activerecord/lib/active_record/middleware/database_selector/resolver/session.rb45
-rw-r--r--activerecord/lib/active_record/migration.rb618
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb217
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb228
-rw-r--r--activerecord/lib/active_record/migration/join_table.rb14
-rw-r--r--activerecord/lib/active_record/model_schema.rb410
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb415
-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.rb538
-rw-r--r--activerecord/lib/active_record/query_cache.rb40
-rw-r--r--activerecord/lib/active_record/querying.rb63
-rw-r--r--activerecord/lib/active_record/railtie.rb165
-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.rake427
-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.rb456
-rw-r--r--activerecord/lib/active_record/relation.rb657
-rw-r--r--activerecord/lib/active_record/relation/batches.rb165
-rw-r--r--activerecord/lib/active_record/relation/batches/batch_enumerator.rb6
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb390
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb129
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb458
-rw-r--r--activerecord/lib/active_record/relation/from_clause.rb10
-rw-r--r--activerecord/lib/active_record/relation/merger.rb144
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb191
-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.rb88
-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_handler.rb57
-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.rb25
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb6
-rw-r--r--activerecord/lib/active_record/relation/query_attribute.rb35
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb720
-rw-r--r--activerecord/lib/active_record/relation/record_fetch_warning.rb8
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb15
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb197
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb18
-rw-r--r--activerecord/lib/active_record/result.rb116
-rw-r--r--activerecord/lib/active_record/runtime_registry.rb8
-rw-r--r--activerecord/lib/active_record/sanitization.rb150
-rw-r--r--activerecord/lib/active_record/schema.rb21
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb188
-rw-r--r--activerecord/lib/active_record/schema_migration.rb18
-rw-r--r--activerecord/lib/active_record/scoping.rb42
-rw-r--r--activerecord/lib/active_record/scoping/default.rb186
-rw-r--r--activerecord/lib/active_record/scoping/named.rb84
-rw-r--r--activerecord/lib/active_record/secure_token.rb6
-rw-r--r--activerecord/lib/active_record/serialization.rb4
-rw-r--r--activerecord/lib/active_record/statement_cache.rb93
-rw-r--r--activerecord/lib/active_record/store.rb162
-rw-r--r--activerecord/lib/active_record/suppressor.rb5
-rw-r--r--activerecord/lib/active_record/table_metadata.rb35
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb279
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb144
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb127
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb50
-rw-r--r--activerecord/lib/active_record/test_databases.rb38
-rw-r--r--activerecord/lib/active_record/test_fixtures.rb224
-rw-r--r--activerecord/lib/active_record/timestamp.rb95
-rw-r--r--activerecord/lib/active_record/touch_later.rb12
-rw-r--r--activerecord/lib/active_record/transactions.rb250
-rw-r--r--activerecord/lib/active_record/translation.rb4
-rw-r--r--activerecord/lib/active_record/type.rb40
-rw-r--r--activerecord/lib/active_record/type/adapter_specific_registry.rb89
-rw-r--r--activerecord/lib/active_record/type/date.rb2
-rw-r--r--activerecord/lib/active_record/type/date_time.rb2
-rw-r--r--activerecord/lib/active_record/type/decimal_without_scale.rb15
-rw-r--r--activerecord/lib/active_record/type/hash_lookup_type_map.rb8
-rw-r--r--activerecord/lib/active_record/type/internal/abstract_json.rb33
-rw-r--r--activerecord/lib/active_record/type/internal/timezone.rb2
-rw-r--r--activerecord/lib/active_record/type/json.rb30
-rw-r--r--activerecord/lib/active_record/type/serialized.rb24
-rw-r--r--activerecord/lib/active_record/type/text.rb11
-rw-r--r--activerecord/lib/active_record/type/time.rb15
-rw-r--r--activerecord/lib/active_record/type/type_map.rb28
-rw-r--r--activerecord/lib/active_record/type/unsigned_integer.rb17
-rw-r--r--activerecord/lib/active_record/type_caster.rb6
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb17
-rw-r--r--activerecord/lib/active_record/type_caster/map.rb9
-rw-r--r--activerecord/lib/active_record/validations.rb10
-rw-r--r--activerecord/lib/active_record/validations/absence.rb2
-rw-r--r--activerecord/lib/active_record/validations/associated.rb4
-rw-r--r--activerecord/lib/active_record/validations/length.rb2
-rw-r--r--activerecord/lib/active_record/validations/presence.rb6
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb81
-rw-r--r--activerecord/lib/active_record/version.rb4
-rw-r--r--activerecord/lib/arel.rb51
-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/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.rb68
-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.rb36
-rw-r--r--activerecord/lib/arel/nodes/case.rb55
-rw-r--r--activerecord/lib/arel/nodes/casted.rb50
-rw-r--r--activerecord/lib/arel/nodes/comment.rb29
-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.rb67
-rw-r--r--activerecord/lib/arel/nodes/select_statement.rb41
-rw-r--r--activerecord/lib/arel/nodes/sql_literal.rb16
-rw-r--r--activerecord/lib/arel/nodes/string_join.rb11
-rw-r--r--activerecord/lib/arel/nodes/table_alias.rb27
-rw-r--r--activerecord/lib/arel/nodes/terminal.rb16
-rw-r--r--activerecord/lib/arel/nodes/true.rb16
-rw-r--r--activerecord/lib/arel/nodes/unary.rb45
-rw-r--r--activerecord/lib/arel/nodes/unary_operation.rb20
-rw-r--r--activerecord/lib/arel/nodes/unqualified_column.rb22
-rw-r--r--activerecord/lib/arel/nodes/update_statement.rb41
-rw-r--r--activerecord/lib/arel/nodes/values_list.rb9
-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.rb257
-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.rb204
-rw-r--r--activerecord/lib/arel/visitors/dot.rb297
-rw-r--r--activerecord/lib/arel/visitors/ibm_db.rb34
-rw-r--r--activerecord/lib/arel/visitors/informix.rb62
-rw-r--r--activerecord/lib/arel/visitors/mssql.rb157
-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.rb110
-rw-r--r--activerecord/lib/arel/visitors/sqlite.rb39
-rw-r--r--activerecord/lib/arel/visitors/to_sql.rb860
-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.tt (renamed from activerecord/lib/rails/generators/active_record/model/templates/application_record.rb)0
-rw-r--r--activerecord/lib/rails/generators/active_record/migration.rb25
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb74
-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.rb46
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt48
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb48
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/model.rb13
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt22
-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.rb4
-rw-r--r--activerecord/test/assets/schema_dump_5_1.yml345
-rw-r--r--activerecord/test/cases/adapter_test.rb513
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb115
-rw-r--r--activerecord/test/cases/adapters/mysql2/annotate_test.rb37
-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.rb40
-rw-r--r--activerecord/test/cases/adapters/mysql2/charset_collation_test.rb34
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb123
-rw-r--r--activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb55
-rw-r--r--activerecord/test/cases/adapters/mysql2/enum_test.rb17
-rw-r--r--activerecord/test/cases/adapters/mysql2/explain_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql2/json_test.rb182
-rw-r--r--activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb180
-rw-r--r--activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb40
-rw-r--r--activerecord/test/cases/adapters/mysql2/quoting_test.rb21
-rw-r--r--activerecord/test/cases/adapters/mysql2/reserved_word_test.rb152
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb46
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_test.rb61
-rw-r--r--activerecord/test/cases/adapters/mysql2/sp_test.rb18
-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.rb23
-rw-r--r--activerecord/test/cases/adapters/mysql2/virtual_column_test.rb63
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb65
-rw-r--r--activerecord/test/cases/adapters/postgresql/annotate_test.rb37
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb207
-rw-r--r--activerecord/test/cases/adapters/postgresql/bit_string_test.rb27
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb45
-rw-r--r--activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb27
-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.rb48
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb163
-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.rb22
-rw-r--r--activerecord/test/cases/adapters/postgresql/explain_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/extension_migration_test.rb6
-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.rb117
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb548
-rw-r--r--activerecord/test/cases/adapters/postgresql/infinity_test.rb69
-rw-r--r--activerecord/test/cases/adapters/postgresql/integer_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb194
-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/optimizer_hints_test.rb44
-rw-r--r--activerecord/test/cases/adapters/postgresql/partitions_test.rb22
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb292
-rw-r--r--activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb27
-rw-r--r--activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb22
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb675
-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.rb37
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb265
-rw-r--r--activerecord/test/cases/adapters/postgresql/serial_test.rb80
-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.rb189
-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.rb325
-rw-r--r--activerecord/test/cases/adapters/postgresql/xml_test.rb18
-rw-r--r--activerecord/test/cases/adapters/sqlite3/annotate_test.rb37
-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.rb24
-rw-r--r--activerecord/test/cases/adapters/sqlite3/json_test.rb29
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb164
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb481
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb22
-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.rb233
-rw-r--r--activerecord/test/cases/arel/attributes/attribute_test.rb1080
-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.rb241
-rw-r--r--activerecord/test/cases/arel/nodes/and_test.rb30
-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.rb96
-rw-r--r--activerecord/test/cases/arel/nodes/casted_test.rb18
-rw-r--r--activerecord/test/cases/arel/nodes/comment_test.rb22
-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.rb79
-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.rb1248
-rw-r--r--activerecord/test/cases/arel/support/fake_record.rb133
-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.rb276
-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.rb560
-rw-r--r--activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb6
-rw-r--r--activerecord/test/cases/associations/callbacks_test.rb75
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb103
-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.rb763
-rw-r--r--activerecord/test/cases/associations/extension_test.rb33
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb454
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb1440
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb793
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb380
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb196
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb92
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb370
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb295
-rw-r--r--activerecord/test/cases/associations/left_outer_join_association_test.rb70
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb208
-rw-r--r--activerecord/test/cases/associations/required_test.rb44
-rw-r--r--activerecord/test/cases/associations_test.rb308
-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.rb697
-rw-r--r--activerecord/test/cases/attribute_set_test.rb253
-rw-r--r--activerecord/test/cases/attribute_test.rb246
-rw-r--r--activerecord/test/cases/attributes_test.rb126
-rw-r--r--activerecord/test/cases/autosave_association_test.rb979
-rw-r--r--activerecord/test/cases/base_test.rb753
-rw-r--r--activerecord/test/cases/batches_test.rb310
-rw-r--r--activerecord/test/cases/binary_test.rb22
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb242
-rw-r--r--activerecord/test/cases/boolean_test.rb52
-rw-r--r--activerecord/test/cases/cache_key_test.rb112
-rw-r--r--activerecord/test/cases/calculations_test.rb496
-rw-r--r--activerecord/test/cases/callbacks_test.rb276
-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.rb118
-rw-r--r--activerecord/test/cases/column_alias_test.rb16
-rw-r--r--activerecord/test/cases/column_definition_test.rb78
-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.rb346
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb391
-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.rb137
-rw-r--r--activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb113
-rw-r--r--activerecord/test/cases/connection_adapters/quoting_test.rb13
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb125
-rw-r--r--activerecord/test/cases/connection_adapters/type_lookup_test.rb186
-rw-r--r--activerecord/test/cases/connection_management_test.rb36
-rw-r--r--activerecord/test/cases/connection_pool_test.rb371
-rw-r--r--activerecord/test/cases/connection_specification/resolver_test.rb75
-rw-r--r--activerecord/test/cases/core_test.rb127
-rw-r--r--activerecord/test/cases/counter_cache_test.rb253
-rw-r--r--activerecord/test/cases/custom_locking_test.rb8
-rw-r--r--activerecord/test/cases/database_configurations_test.rb117
-rw-r--r--activerecord/test/cases/database_selector_test.rb166
-rw-r--r--activerecord/test/cases/database_statements_test.rb33
-rw-r--r--activerecord/test/cases/date_test.rb36
-rw-r--r--activerecord/test/cases/date_time_precision_test.rb167
-rw-r--r--activerecord/test/cases/date_time_test.rb31
-rw-r--r--activerecord/test/cases/defaults_test.rb144
-rw-r--r--activerecord/test/cases/dirty_test.rb608
-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.rb280
-rw-r--r--activerecord/test/cases/errors_test.rb14
-rw-r--r--activerecord/test/cases/explain_subscriber_test.rb32
-rw-r--r--activerecord/test/cases/explain_test.rb31
-rw-r--r--activerecord/test/cases/filter_attributes_test.rb132
-rw-r--r--activerecord/test/cases/finder_respond_to_test.rb27
-rw-r--r--activerecord/test/cases/finder_test.rb710
-rw-r--r--activerecord/test/cases/fixture_set/file_test.rb68
-rw-r--r--activerecord/test/cases/fixtures_test.rb740
-rw-r--r--activerecord/test/cases/forbidden_attributes_protection_test.rb99
-rw-r--r--activerecord/test/cases/habtm_destroy_order_test.rb50
-rw-r--r--activerecord/test/cases/helper.rb98
-rw-r--r--activerecord/test/cases/hot_compatibility_test.rb64
-rw-r--r--activerecord/test/cases/i18n_test.rb35
-rw-r--r--activerecord/test/cases/inheritance_test.rb268
-rw-r--r--activerecord/test/cases/insert_all_test.rb189
-rw-r--r--activerecord/test/cases/instrumentation_test.rb76
-rw-r--r--activerecord/test/cases/integration_test.rb146
-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.rb139
-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/locking_test.rb471
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb102
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb168
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb32
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb91
-rw-r--r--activerecord/test/cases/migration/column_positioning_test.rb24
-rw-r--r--activerecord/test/cases/migration/columns_test.rb170
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb75
-rw-r--r--activerecord/test/cases/migration/compatibility_test.rb389
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb66
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb681
-rw-r--r--activerecord/test/cases/migration/helper.rb4
-rw-r--r--activerecord/test/cases/migration/index_test.rb124
-rw-r--r--activerecord/test/cases/migration/logger_test.rb6
-rw-r--r--activerecord/test/cases/migration/pending_migrations_test.rb64
-rw-r--r--activerecord/test/cases/migration/references_foreign_key_test.rb292
-rw-r--r--activerecord/test/cases/migration/references_index_test.rb36
-rw-r--r--activerecord/test/cases/migration/references_statements_test.rb42
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb53
-rw-r--r--activerecord/test/cases/migration_test.rb562
-rw-r--r--activerecord/test/cases/migrator_test.rb314
-rw-r--r--activerecord/test/cases/mixin_test.rb18
-rw-r--r--activerecord/test/cases/modules_test.rb68
-rw-r--r--activerecord/test/cases/multiparameter_attributes_test.rb107
-rw-r--r--activerecord/test/cases/multiple_db_test.rb38
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb604
-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.rb625
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb42
-rw-r--r--activerecord/test/cases/primary_keys_test.rb316
-rw-r--r--activerecord/test/cases/query_cache_test.rb475
-rw-r--r--activerecord/test/cases/quoting_test.rb212
-rw-r--r--activerecord/test/cases/readonly_test.rb97
-rw-r--r--activerecord/test/cases/reaper_test.rb26
-rw-r--r--activerecord/test/cases/reflection_test.rb308
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb85
-rw-r--r--activerecord/test/cases/relation/delete_all_test.rb102
-rw-r--r--activerecord/test/cases/relation/merging_test.rb98
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb147
-rw-r--r--activerecord/test/cases/relation/or_test.rb104
-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.rb27
-rw-r--r--activerecord/test/cases/relation/update_all_test.rb292
-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.rb137
-rw-r--r--activerecord/test/cases/relation_test.rb320
-rw-r--r--activerecord/test/cases/relations_test.rb1527
-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.rb158
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb307
-rw-r--r--activerecord/test/cases/schema_loading_test.rb54
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb339
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb226
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb237
-rw-r--r--activerecord/test/cases/secure_token_test.rb8
-rw-r--r--activerecord/test/cases/serialization_test.rb50
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb167
-rw-r--r--activerecord/test/cases/statement_cache_test.rb76
-rw-r--r--activerecord/test/cases/statement_invalid_test.rb42
-rw-r--r--activerecord/test/cases/store_test.rb230
-rw-r--r--activerecord/test/cases/suppressor_test.rb38
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb1199
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb615
-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.rb51
-rw-r--r--activerecord/test/cases/test_fixtures_test.rb24
-rw-r--r--activerecord/test/cases/time_precision_test.rb163
-rw-r--r--activerecord/test/cases/timestamp_test.rb161
-rw-r--r--activerecord/test/cases/touch_later_test.rb29
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb255
-rw-r--r--activerecord/test/cases/transaction_isolation_test.rb22
-rw-r--r--activerecord/test/cases/transactions_test.rb593
-rw-r--r--activerecord/test/cases/type/adapter_specific_registry_test.rb2
-rw-r--r--activerecord/test/cases/type/date_time_test.rb4
-rw-r--r--activerecord/test/cases/type/integer_test.rb6
-rw-r--r--activerecord/test/cases/type/string_test.rb18
-rw-r--r--activerecord/test/cases/type/time_test.rb22
-rw-r--r--activerecord/test/cases/type/type_map_test.rb57
-rw-r--r--activerecord/test/cases/type/unsigned_integer_test.rb19
-rw-r--r--activerecord/test/cases/type_test.rb2
-rw-r--r--activerecord/test/cases/types_test.rb4
-rw-r--r--activerecord/test/cases/unconnected_test.rb18
-rw-r--r--activerecord/test/cases/unsafe_raw_sql_test.rb319
-rw-r--r--activerecord/test/cases/validations/absence_validation_test.rb28
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb48
-rw-r--r--activerecord/test/cases/validations/i18n_generate_message_validation_test.rb50
-rw-r--r--activerecord/test/cases/validations/i18n_validation_test.rb41
-rw-r--r--activerecord/test/cases/validations/length_validation_test.rb67
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb38
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb296
-rw-r--r--activerecord/test/cases/validations_repair_helper.rb2
-rw-r--r--activerecord/test/cases/validations_test.rb83
-rw-r--r--activerecord/test/cases/view_test.rb246
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb66
-rw-r--r--activerecord/test/config.example.yml18
-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/binaries.yml4
-rw-r--r--activerecord/test/fixtures/books.yml2
-rw-r--r--activerecord/test/fixtures/citations.yml5
-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.yml1
-rw-r--r--activerecord/test/fixtures/other_dogs.yml2
-rw-r--r--activerecord/test/fixtures/other_posts.yml1
-rw-r--r--activerecord/test/fixtures/posts.yml8
-rw-r--r--activerecord/test/fixtures/reserved_words/values.yml4
-rw-r--r--activerecord/test/fixtures/sponsors.yml3
-rw-r--r--activerecord/test/fixtures/subscribers.yml2
-rw-r--r--activerecord/test/fixtures/teapots.yml3
-rw-r--r--activerecord/test/migrations/10_urban/9_add_expressions.rb2
-rw-r--r--activerecord/test/migrations/decimal/1_give_me_big_numbers.rb10
-rw-r--r--activerecord/test/migrations/empty/.keep0
-rw-r--r--activerecord/test/migrations/magic/1_currencies_have_symbols.rb5
-rw-r--r--activerecord/test/migrations/missing/1000_people_have_middle_names.rb2
-rw-r--r--activerecord/test/migrations/missing/1_people_have_last_names.rb2
-rw-r--r--activerecord/test/migrations/missing/3_we_need_reminders.rb2
-rw-r--r--activerecord/test/migrations/missing/4_innocent_jointable.rb4
-rw-r--r--activerecord/test/migrations/rename/1_we_need_things.rb2
-rw-r--r--activerecord/test/migrations/rename/2_rename_things.rb2
-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.rb2
-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.rb2
-rw-r--r--activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb2
-rw-r--r--activerecord/test/migrations/valid/1_valid_people_have_last_names.rb2
-rw-r--r--activerecord/test/migrations/valid/2_we_need_reminders.rb2
-rw-r--r--activerecord/test/migrations/valid/3_innocent_jointable.rb4
-rw-r--r--activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb2
-rw-r--r--activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb2
-rw-r--r--activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb4
-rw-r--r--activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb2
-rw-r--r--activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb2
-rw-r--r--activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb4
-rw-r--r--activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb2
-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.rb4
-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.rb14
-rw-r--r--activerecord/test/models/book.rb11
-rw-r--r--activerecord/test/models/boolean.rb5
-rw-r--r--activerecord/test/models/bulb.rb14
-rw-r--r--activerecord/test/models/cake_designer.rb2
-rw-r--r--activerecord/test/models/car.rb22
-rw-r--r--activerecord/test/models/carrier.rb2
-rw-r--r--activerecord/test/models/cat.rb2
-rw-r--r--activerecord/test/models/categorization.rb14
-rw-r--r--activerecord/test/models/category.rb44
-rw-r--r--activerecord/test/models/chef.rb2
-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.rb52
-rw-r--r--activerecord/test/models/company.rb203
-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.rb10
-rw-r--r--activerecord/test/models/content.rb8
-rw-r--r--activerecord/test/models/contract.rb18
-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.rb2
-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.rb179
-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.rb11
-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.rb17
-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.rb2
-rw-r--r--activerecord/test/models/hotel.rb8
-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.rb44
-rw-r--r--activerecord/test/models/member_detail.rb3
-rw-r--r--activerecord/test/models/member_type.rb2
-rw-r--r--activerecord/test/models/membership.rb5
-rw-r--r--activerecord/test/models/mentor.rb4
-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/mocktail_designer.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.rb2
-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.rb21
-rw-r--r--activerecord/test/models/person.rb112
-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.rb108
-rw-r--r--activerecord/test/models/possession.rb4
-rw-r--r--activerecord/test/models/post.rb258
-rw-r--r--activerecord/test/models/price_estimate.rb12
-rw-r--r--activerecord/test/models/professor.rb4
-rw-r--r--activerecord/test/models/project.rb26
-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.rb5
-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.rb9
-rw-r--r--activerecord/test/models/reply.rb36
-rw-r--r--activerecord/test/models/ship.rb16
-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.rb2
-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.rb6
-rw-r--r--activerecord/test/models/tag.rb9
-rw-r--r--activerecord/test/models/tagging.rb19
-rw-r--r--activerecord/test/models/task.rb2
-rw-r--r--activerecord/test/models/topic.rb67
-rw-r--r--activerecord/test/models/toy.rb2
-rw-r--r--activerecord/test/models/traffic_light.rb2
-rw-r--r--activerecord/test/models/treasure.rb8
-rw-r--r--activerecord/test/models/treaty.rb6
-rw-r--r--activerecord/test/models/tree.rb2
-rw-r--r--activerecord/test/models/tuning_peg.rb2
-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.rb2
-rw-r--r--activerecord/test/models/uuid_parent.rb2
-rw-r--r--activerecord/test/models/vegetables.rb7
-rw-r--r--activerecord/test/models/vehicle.rb4
-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.rb86
-rw-r--r--activerecord/test/schema/oracle_specific_schema.rb28
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb77
-rw-r--r--activerecord/test/schema/schema.rb336
-rw-r--r--activerecord/test/schema/sqlite_specific_schema.rb27
-rw-r--r--activerecord/test/support/config.rb55
-rw-r--r--activerecord/test/support/connection.rb19
-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.rb2
-rw-r--r--activerecord/test/support/stubs/strong_parameters.rb40
-rw-r--r--activestorage/.babelrc8
-rw-r--r--activestorage/.eslintrc19
-rw-r--r--activestorage/.gitignore6
-rw-r--r--activestorage/CHANGELOG.md175
-rw-r--r--activestorage/MIT-LICENSE20
-rw-r--r--activestorage/README.md162
-rw-r--r--activestorage/Rakefile25
-rw-r--r--activestorage/activestorage.gemspec36
-rw-r--r--activestorage/app/assets/javascripts/activestorage.js942
-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.js73
-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.rb12
-rw-r--r--activestorage/app/jobs/active_storage/base_job.rb4
-rw-r--r--activestorage/app/jobs/active_storage/purge_job.rb13
-rw-r--r--activestorage/app/models/active_storage/attachment.rb50
-rw-r--r--activestorage/app/models/active_storage/blob.rb278
-rw-r--r--activestorage/app/models/active_storage/blob/analyzable.rb57
-rw-r--r--activestorage/app/models/active_storage/blob/identifiable.rb31
-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/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb9
-rw-r--r--activestorage/lib/active_storage.rb65
-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.rb69
-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.rb43
-rw-r--r--activestorage/lib/active_storage/downloading.rb47
-rw-r--r--activestorage/lib/active_storage/engine.rb145
-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.rb141
-rw-r--r--activestorage/lib/active_storage/service/azure_storage_service.rb165
-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.rb141
-rw-r--r--activestorage/lib/active_storage/service/mirror_service.rb55
-rw-r--r--activestorage/lib/active_storage/service/s3_service.rb116
-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.rake22
-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.rb47
-rw-r--r--activestorage/test/controllers/direct_uploads_controller_test.rb147
-rw-r--r--activestorage/test/controllers/disk_controller_test.rb99
-rw-r--r--activestorage/test/controllers/representations_controller_test.rb91
-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/.keep0
-rw-r--r--activestorage/test/dummy/app/assets/javascripts/application.js (renamed from railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js)0
-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/.keep0
-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/.keep0
-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/yarn11
-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.rb52
-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.tifbin0 -> 33705838 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.rb599
-rw-r--r--activestorage/test/models/attached/one_test.rb516
-rw-r--r--activestorage/test/models/blob_test.rb206
-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.rb180
-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.rb37
-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.rb82
-rw-r--r--activestorage/test/service/mirror_service_test.rb68
-rw-r--r--activestorage/test/service/s3_service_test.rb83
-rw-r--r--activestorage/test/service/shared_service_tests.rb140
-rw-r--r--activestorage/test/template/image_tag_test.rb44
-rw-r--r--activestorage/test/test_helper.rb103
-rw-r--r--activesupport/.gitignore1
-rw-r--r--activesupport/CHANGELOG.md694
-rw-r--r--activesupport/MIT-LICENSE2
-rw-r--r--activesupport/README.rdoc7
-rw-r--r--activesupport/Rakefile35
-rw-r--r--activesupport/activesupport.gemspec43
-rwxr-xr-xactivesupport/bin/generate_tables139
-rwxr-xr-xactivesupport/bin/test7
-rw-r--r--activesupport/lib/active_support.rb17
-rw-r--r--activesupport/lib/active_support/all.rb8
-rw-r--r--activesupport/lib/active_support/array_inquirer.rb14
-rw-r--r--activesupport/lib/active_support/backtrace_cleaner.rb33
-rw-r--r--activesupport/lib/active_support/benchmarkable.rb8
-rw-r--r--activesupport/lib/active_support/builder.rb4
-rw-r--r--activesupport/lib/active_support/cache.rb444
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb82
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb135
-rw-r--r--activesupport/lib/active_support/cache/memory_store.rb37
-rw-r--r--activesupport/lib/active_support/cache/null_store.rb15
-rw-r--r--activesupport/lib/active_support/cache/redis_cache_store.rb485
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb70
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb19
-rw-r--r--activesupport/lib/active_support/callbacks.rb1231
-rw-r--r--activesupport/lib/active_support/concern.rb14
-rw-r--r--activesupport/lib/active_support/concurrency/latch.rb19
-rw-r--r--activesupport/lib/active_support/concurrency/load_interlock_aware_monitor.rb17
-rw-r--r--activesupport/lib/active_support/concurrency/share_lock.rb83
-rw-r--r--activesupport/lib/active_support/configurable.rb26
-rw-r--r--activesupport/lib/active_support/core_ext.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/array.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/array/access.rb28
-rw-r--r--activesupport/lib/active_support/core_ext/array/conversions.rb32
-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.rb10
-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.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/class.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute.rb69
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute_accessors.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/class/subclasses.rb25
-rw-r--r--activesupport/lib/active_support/core_ext/date.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/date/acts_like.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/date/blank.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/date/calculations.rb29
-rw-r--r--activesupport/lib/active_support/core_ext/date/conversions.rb42
-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.rb136
-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.rb21
-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.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/calculations.rb65
-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.rb26
-rw-r--r--activesupport/lib/active_support/core_ext/digest/uuid.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/enumerable.rb120
-rw-r--r--activesupport/lib/active_support/core_ext/file.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/file/atomic.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/hash.rb19
-rw-r--r--activesupport/lib/active_support/core_ext/hash/compact.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/hash/conversions.rb84
-rw-r--r--activesupport/lib/active_support/core_ext/hash/deep_merge.rb20
-rw-r--r--activesupport/lib/active_support/core_ext/hash/deep_transform_values.rb46
-rw-r--r--activesupport/lib/active_support/core_ext/hash/except.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/hash/indifferent_access.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/hash/keys.rb43
-rw-r--r--activesupport/lib/active_support/core_ext/hash/reverse_merge.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/hash/slice.rb32
-rw-r--r--activesupport/lib/active_support/core_ext/hash/transform_values.rb32
-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.rb9
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/agnostics.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/concern.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/debugger.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/reporting.rb4
-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.rb8
-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.rb2
-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.rb72
-rw-r--r--activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb51
-rw-r--r--activesupport/lib/active_support/core_ext/module/concerning.rb17
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb147
-rw-r--r--activesupport/lib/active_support/core_ext/module/deprecation.rb2
-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.rb70
-rw-r--r--activesupport/lib/active_support/core_ext/module/reachable.rb12
-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.rb28
-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.rb257
-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.rb19
-rw-r--r--activesupport/lib/active_support/core_ext/object/conversions.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/object/deep_dup.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/object/duplicable.rb133
-rw-r--r--activesupport/lib/active_support/core_ext/object/inclusion.rb4
-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.rb28
-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.rb70
-rw-r--r--activesupport/lib/active_support/core_ext/range/conversions.rb60
-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.rb34
-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.rb8
-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.rb69
-rw-r--r--activesupport/lib/active_support/core_ext/string/inquiry.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/string/multibyte.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/string/output_safety.rb110
-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.rb6
-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.rb121
-rw-r--r--activesupport/lib/active_support/core_ext/time/compatibility.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/time/conversions.rb29
-rw-r--r--activesupport/lib/active_support/core_ext/time/marshal.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/time/zones.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/uri.rb13
-rw-r--r--activesupport/lib/active_support/current_attributes.rb203
-rw-r--r--activesupport/lib/active_support/dependencies.rb183
-rw-r--r--activesupport/lib/active_support/dependencies/autoload.rb2
-rw-r--r--activesupport/lib/active_support/dependencies/interlock.rb8
-rw-r--r--activesupport/lib/active_support/dependencies/zeitwerk_integration.rb100
-rw-r--r--activesupport/lib/active_support/deprecation.rb21
-rw-r--r--activesupport/lib/active_support/deprecation/behaviors.rb41
-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.rb49
-rw-r--r--activesupport/lib/active_support/deprecation/proxy_wrappers.rb13
-rw-r--r--activesupport/lib/active_support/deprecation/reporting.rb23
-rw-r--r--activesupport/lib/active_support/descendants_tracker.rb63
-rw-r--r--activesupport/lib/active_support/digest.rb20
-rw-r--r--activesupport/lib/active_support/duration.rb337
-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.rb100
-rw-r--r--activesupport/lib/active_support/evented_file_update_checker.rb202
-rw-r--r--activesupport/lib/active_support/execution_wrapper.rb59
-rw-r--r--activesupport/lib/active_support/executor.rb4
-rw-r--r--activesupport/lib/active_support/file_update_checker.rb95
-rw-r--r--activesupport/lib/active_support/gem_version.rb4
-rw-r--r--activesupport/lib/active_support/gzip.rb12
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb147
-rw-r--r--activesupport/lib/active_support/i18n.rb15
-rw-r--r--activesupport/lib/active_support/i18n_railtie.rb42
-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.rb43
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb174
-rw-r--r--activesupport/lib/active_support/inflector/transliterate.rb61
-rw-r--r--activesupport/lib/active_support/json.rb6
-rw-r--r--activesupport/lib/active_support/json/decoding.rb51
-rw-r--r--activesupport/lib/active_support/json/encoding.rb27
-rw-r--r--activesupport/lib/active_support/key_generator.rb48
-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/log_subscriber.rb60
-rw-r--r--activesupport/lib/active_support/log_subscriber/test_helper.rb20
-rw-r--r--activesupport/lib/active_support/logger.rb41
-rw-r--r--activesupport/lib/active_support/logger_silence.rb47
-rw-r--r--activesupport/lib/active_support/logger_thread_safe_level.rb34
-rw-r--r--activesupport/lib/active_support/message_encryptor.rb218
-rw-r--r--activesupport/lib/active_support/message_verifier.rb103
-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.rb105
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb362
-rw-r--r--activesupport/lib/active_support/notifications.rb57
-rw-r--r--activesupport/lib/active_support/notifications/fanout.rb123
-rw-r--r--activesupport/lib/active_support/notifications/instrumenter.rb96
-rw-r--r--activesupport/lib/active_support/number_helper.rb25
-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.rb8
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb7
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_human_converter.rb22
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb21
-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.rb8
-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.rb12
-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.rb21
-rw-r--r--activesupport/lib/active_support/rescuable.rb163
-rw-r--r--activesupport/lib/active_support/security_utils.rb26
-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.rb135
-rw-r--r--activesupport/lib/active_support/testing/assertions.rb143
-rw-r--r--activesupport/lib/active_support/testing/autorun.rb15
-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.rb6
-rw-r--r--activesupport/lib/active_support/testing/file_fixtures.rb4
-rw-r--r--activesupport/lib/active_support/testing/isolation.rb43
-rw-r--r--activesupport/lib/active_support/testing/method_call_assertions.rb33
-rw-r--r--activesupport/lib/active_support/testing/parallelization.rb109
-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.rb110
-rw-r--r--activesupport/lib/active_support/time.rb26
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb122
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb193
-rw-r--r--activesupport/lib/active_support/values/unicode_tables.datbin1068675 -> 0 bytes
-rw-r--r--activesupport/lib/active_support/version.rb4
-rw-r--r--activesupport/lib/active_support/xml_mini.rb110
-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.rb40
-rw-r--r--activesupport/test/array_inquirer_test.rb38
-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.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/raises_load_error.rb4
-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.rb2
-rw-r--r--activesupport/test/autoloading_fixtures/typo.rb3
-rw-r--r--activesupport/test/benchmarkable_test.rb26
-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.rb545
-rw-r--r--activesupport/test/cache/behaviors/cache_store_version_behavior.rb88
-rw-r--r--activesupport/test/cache/behaviors/connection_pool_behavior.rb56
-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.rb140
-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.rb1180
-rw-r--r--activesupport/test/callback_inheritance_test.rb28
-rw-r--r--activesupport/test/callbacks_test.rb336
-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.rb24
-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.rb27
-rw-r--r--activesupport/test/core_ext/array/access_test.rb18
-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.rb15
-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.rb380
-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.rb322
-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.rb170
-rw-r--r--activesupport/test/core_ext/file_test.rb26
-rw-r--r--activesupport/test/core_ext/hash/transform_keys_test.rb46
-rw-r--r--activesupport/test/core_ext/hash/transform_values_test.rb81
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb1278
-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.rb31
-rw-r--r--activesupport/test/core_ext/marshal_test.rb44
-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.rb48
-rw-r--r--activesupport/test/core_ext/module/attribute_accessor_test.rb72
-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.rb118
-rw-r--r--activesupport/test/core_ext/module/reachable_test.rb41
-rw-r--r--activesupport/test/core_ext/module/remove_method_test.rb36
-rw-r--r--activesupport/test/core_ext/module_test.rb442
-rw-r--r--activesupport/test/core_ext/name_error_test.rb8
-rw-r--r--activesupport/test/core_ext/numeric_ext_test.rb534
-rw-r--r--activesupport/test/core_ext/object/acts_like_test.rb16
-rw-r--r--activesupport/test/core_ext/object/blank_test.rb12
-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.rb49
-rw-r--r--activesupport/test/core_ext/range_ext_test.rb58
-rw-r--r--activesupport/test/core_ext/regexp_ext_test.rb6
-rw-r--r--activesupport/test/core_ext/secure_random_test.rb26
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb435
-rw-r--r--activesupport/test/core_ext/time_ext_test.rb903
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb764
-rw-r--r--activesupport/test/core_ext/uri_ext_test.rb10
-rw-r--r--activesupport/test/current_attributes_test.rb128
-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.rb388
-rw-r--r--activesupport/test/dependencies_test_helpers.rb8
-rw-r--r--activesupport/test/deprecation/method_wrappers_test.rb76
-rw-r--r--activesupport/test/deprecation/proxy_wrappers_test.rb12
-rw-r--r--activesupport/test/deprecation_test.rb141
-rw-r--r--activesupport/test/descendants_tracker_test_cases.rb51
-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.rb132
-rw-r--r--activesupport/test/executor_test.rb168
-rw-r--r--activesupport/test/file_update_checker_shared_tests.rb148
-rw-r--r--activesupport/test/file_update_checker_test.rb6
-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/fixtures/concern/some_concern.rb11
-rw-r--r--activesupport/test/gzip_test.rb24
-rw-r--r--activesupport/test/hash_with_indifferent_access_test.rb839
-rw-r--r--activesupport/test/i18n_test.rb46
-rw-r--r--activesupport/test/inflector_test.rb172
-rw-r--r--activesupport/test/inflector_test_cases.rb111
-rw-r--r--activesupport/test/json/decoding_test.rb130
-rw-r--r--activesupport/test/json/encoding_test.rb241
-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.rb85
-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.rb516
-rw-r--r--activesupport/test/multibyte_conformance_test.rb139
-rw-r--r--activesupport/test/multibyte_grapheme_break_conformance_test.rb62
-rw-r--r--activesupport/test/multibyte_normalization_conformance_test.rb153
-rw-r--r--activesupport/test/multibyte_proxy_test.rb6
-rw-r--r--activesupport/test/multibyte_test_helpers.rb38
-rw-r--r--activesupport/test/multibyte_unicode_database_test.rb26
-rw-r--r--activesupport/test/notifications/evented_notification_test.rb87
-rw-r--r--activesupport/test/notifications/instrumenter_test.rb22
-rw-r--r--activesupport/test/notifications_test.rb127
-rw-r--r--activesupport/test/number_helper_i18n_test.rb155
-rw-r--r--activesupport/test/number_helper_test.rb413
-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.rb24
-rw-r--r--activesupport/test/rescuable_test.rb86
-rw-r--r--activesupport/test/safe_buffer_test.rb197
-rw-r--r--activesupport/test/security_utils_test.rb21
-rw-r--r--activesupport/test/share_lock_test.rb282
-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.rb52
-rw-r--r--activesupport/test/test_case_test.rb227
-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.rb112
-rw-r--r--activesupport/test/time_travel_test.rb126
-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.rb34
-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.rb22
-rwxr-xr-xci/travis.rb106
-rw-r--r--guides/CHANGELOG.md27
-rw-r--r--guides/Rakefile69
-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.pngbin25803 -> 0 bytes
-rw-r--r--guides/assets/images/credits_pic_blank.gifbin597 -> 0 bytes
-rw-r--r--guides/assets/images/fxn.pngbin15436 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/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.pngbin1053549 -> 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 -> 26796 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.pngbin28919 -> 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_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--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/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.rb30
-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.rb22
-rw-r--r--guides/rails_guides.rb39
-rw-r--r--guides/rails_guides/generator.rb348
-rw-r--r--guides/rails_guides/helpers.rb27
-rw-r--r--guides/rails_guides/indexer.rb78
-rw-r--r--guides/rails_guides/kindle.rb95
-rw-r--r--guides/rails_guides/levenshtein.rb18
-rw-r--r--guides/rails_guides/markdown.rb64
-rw-r--r--guides/rails_guides/markdown/renderer.rb91
-rw-r--r--guides/source/2_2_release_notes.md40
-rw-r--r--guides/source/2_3_release_notes.md64
-rw-r--r--guides/source/3_0_release_notes.md41
-rw-r--r--guides/source/3_1_release_notes.md18
-rw-r--r--guides/source/3_2_release_notes.md12
-rw-r--r--guides/source/4_0_release_notes.md20
-rw-r--r--guides/source/4_1_release_notes.md12
-rw-r--r--guides/source/4_2_release_notes.md20
-rw-r--r--guides/source/5_0_release_notes.md343
-rw-r--r--guides/source/5_1_release_notes.md659
-rw-r--r--guides/source/5_2_release_notes.md865
-rw-r--r--guides/source/6_0_release_notes.md299
-rw-r--r--guides/source/_welcome.html.erb23
-rw-r--r--guides/source/action_cable_overview.md702
-rw-r--r--guides/source/action_controller_overview.md176
-rw-r--r--guides/source/action_mailbox_basics.md398
-rw-r--r--guides/source/action_mailer_basics.md263
-rw-r--r--guides/source/action_text_overview.md99
-rw-r--r--guides/source/action_view_overview.md114
-rw-r--r--guides/source/active_job_basics.md184
-rw-r--r--guides/source/active_model_basics.md90
-rw-r--r--guides/source/active_record_basics.md60
-rw-r--r--guides/source/active_record_callbacks.md106
-rw-r--r--guides/source/active_record_migrations.md229
-rw-r--r--guides/source/active_record_postgresql.md73
-rw-r--r--guides/source/active_record_querying.md223
-rw-r--r--guides/source/active_record_validations.md144
-rw-r--r--guides/source/active_storage_overview.md770
-rw-r--r--guides/source/active_support_core_extensions.md788
-rw-r--r--guides/source/active_support_instrumentation.md266
-rw-r--r--guides/source/api_app.md67
-rw-r--r--guides/source/api_documentation_guidelines.md28
-rw-r--r--guides/source/asset_pipeline.md291
-rw-r--r--guides/source/association_basics.md306
-rw-r--r--guides/source/autoloading_and_reloading_constants.md149
-rw-r--r--guides/source/caching_with_rails.md220
-rw-r--r--guides/source/command_line.md314
-rw-r--r--guides/source/configuring.md655
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md268
-rw-r--r--guides/source/credits.html.erb80
-rw-r--r--guides/source/debugging_rails_applications.md419
-rw-r--r--guides/source/development_dependencies_install.md292
-rw-r--r--guides/source/documents.yaml94
-rw-r--r--guides/source/engines.md237
-rw-r--r--guides/source/form_helpers.md398
-rw-r--r--guides/source/generators.md99
-rw-r--r--guides/source/getting_started.md429
-rw-r--r--guides/source/i18n.md231
-rw-r--r--guides/source/index.html.erb9
-rw-r--r--guides/source/initialization.md188
-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.erb4
-rw-r--r--guides/source/layout.html.erb88
-rw-r--r--guides/source/layouts_and_rendering.md74
-rw-r--r--guides/source/maintenance_policy.md10
-rw-r--r--guides/source/nested_model_forms.md230
-rw-r--r--guides/source/plugins.md91
-rw-r--r--guides/source/profiling.md16
-rw-r--r--guides/source/rails_application_templates.md24
-rw-r--r--guides/source/rails_on_rack.md84
-rw-r--r--guides/source/routing.md219
-rw-r--r--guides/source/ruby_on_rails_guides_guidelines.md50
-rw-r--r--guides/source/security.md443
-rw-r--r--guides/source/testing.md860
-rw-r--r--guides/source/threading_and_code_execution.md324
-rw-r--r--guides/source/upgrading_ruby_on_rails.md461
-rw-r--r--guides/source/working_with_javascript_in_rails.md233
-rw-r--r--guides/w3c_validator.rb71
-rw-r--r--package.json12
-rw-r--r--rails.gemspec49
-rw-r--r--railties/.gitignore7
-rw-r--r--railties/CHANGELOG.md578
-rw-r--r--railties/MIT-LICENSE2
-rw-r--r--railties/RDOC_MAIN.rdoc119
-rw-r--r--railties/README.rdoc5
-rw-r--r--railties/Rakefile100
-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.rb48
-rw-r--r--railties/lib/rails/all.rb9
-rw-r--r--railties/lib/rails/api/generator.rb38
-rw-r--r--railties/lib/rails/api/task.rb122
-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.rb265
-rw-r--r--railties/lib/rails/application/bootstrap.rb14
-rw-r--r--railties/lib/rails/application/configuration.rb326
-rw-r--r--railties/lib/rails/application/default_middleware_stack.rb20
-rw-r--r--railties/lib/rails/application/dummy_erb_compiler.rb19
-rw-r--r--railties/lib/rails/application/finisher.rb84
-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/autoloaders.rb44
-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.rb42
-rw-r--r--railties/lib/rails/code_statistics_calculator.rb8
-rw-r--r--railties/lib/rails/command.rb114
-rw-r--r--railties/lib/rails/command/actions.rb54
-rw-r--r--railties/lib/rails/command/base.rb165
-rw-r--r--railties/lib/rails/command/behavior.rb84
-rw-r--r--railties/lib/rails/command/environment_argument.rb38
-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.rb8
-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.rb180
-rw-r--r--railties/lib/rails/commands/console.rb68
-rw-r--r--railties/lib/rails/commands/console/console_command.rb106
-rw-r--r--railties/lib/rails/commands/console_helper.rb34
-rw-r--r--railties/lib/rails/commands/credentials/USAGE58
-rw-r--r--railties/lib/rails/commands/credentials/credentials_command.rb112
-rw-r--r--railties/lib/rails/commands/db/system/change/change_command.rb20
-rw-r--r--railties/lib/rails/commands/dbconsole.rb173
-rw-r--r--railties/lib/rails/commands/dbconsole/dbconsole_command.rb182
-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/USAGE28
-rw-r--r--railties/lib/rails/commands/encrypted/encrypted_command.rb87
-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.rb23
-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/rake_proxy.rb34
-rw-r--r--railties/lib/rails/commands/routes/routes_command.rb37
-rw-r--r--railties/lib/rails/commands/runner.rb69
-rw-r--r--railties/lib/rails/commands/runner/USAGE20
-rw-r--r--railties/lib/rails/commands/runner/runner_command.rb57
-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.rb139
-rw-r--r--railties/lib/rails/commands/server/server_command.rb312
-rw-r--r--railties/lib/rails/commands/test.rb9
-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.rb24
-rw-r--r--railties/lib/rails/console/app.rb10
-rw-r--r--railties/lib/rails/console/helpers.rb2
-rw-r--r--railties/lib/rails/dev_caching.rb17
-rw-r--r--railties/lib/rails/engine.rb172
-rw-r--r--railties/lib/rails/engine/commands.rb19
-rw-r--r--railties/lib/rails/engine/commands_tasks.rb116
-rw-r--r--railties/lib/rails/engine/configuration.rb10
-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.rb553
-rw-r--r--railties/lib/rails/generators/actions.rb189
-rw-r--r--railties/lib/rails/generators/actions/create_migration.rb50
-rw-r--r--railties/lib/rails/generators/active_model.rb8
-rw-r--r--railties/lib/rails/generators/app_base.rb367
-rw-r--r--railties/lib/rails/generators/app_name.rb50
-rw-r--r--railties/lib/rails/generators/base.rb106
-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/database.rb58
-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.rb27
-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.tt1
-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.tt37
-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.tt19
-rw-r--r--railties/lib/rails/generators/generated_attribute.rb112
-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.rb22
-rw-r--r--railties/lib/rails/generators/model_helpers.rb15
-rw-r--r--railties/lib/rails/generators/named_base.rb169
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb394
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile55
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile.tt78
-rw-r--r--railties/lib/rails/generators/rails/app/templates/README.md.tt (renamed from railties/lib/rails/generators/rails/app/templates/README.md)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Rakefile.tt6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt5
-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/javascripts/cable.js13
-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.rb5
-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.rb5
-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.tt2
-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.tt23
-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.tt11
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/bundle2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/rails.tt (renamed from railties/lib/rails/generators/rails/app/templates/bin/rails)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/rake.tt (renamed from railties/lib/rails/generators/rails/app/templates/bin/rake)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/setup33
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/setup.tt38
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/update28
-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.ru.tt5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/application.rb36
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/application.rb.tt47
-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.yml10
-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.rb.tt5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt19
-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.tt13
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/active_record_belongs_to_required_by_default.rb6
-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.tt11
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt7
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb6
-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.rb.tt5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb.tt (renamed from railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb)0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt16
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt33
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/per_form_csrf_tokens.rb4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/request_forgery_protection.rb4
-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/initializers/ssl_options.rb4
-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.rb47
-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.rb3
-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.rb6
-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/gitignore25
-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/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/channels/application_cable/connection_test.rb.tt11
-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/db/system/change/change_generator.rb65
-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.rb9
-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/model_generator.rb4
-rw-r--r--railties/lib/rails/generators/rails/plugin/plugin_generator.rb96
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec24
-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.md28
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/README.md.tt28
-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.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt6
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt27
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/bin/test.tt10
-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/gitignore9
-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.rb7
-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.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/application.rb20
-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.rb.tt5
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js11
-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.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt17
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt (renamed from railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.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.rb27
-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/scaffold_generator.rb5
-rw-r--r--railties/lib/rails/generators/rails/scaffold/templates/scaffold.css12
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/USAGE2
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb20
-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.rb.tt (renamed from railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb)0
-rw-r--r--railties/lib/rails/generators/test_unit/generator/generator_generator.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.rb12
-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.rb32
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb44
-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.rb54
-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.tt57
-rw-r--r--railties/lib/rails/generators/test_unit/system/system_generator.rb24
-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.rb58
-rw-r--r--railties/lib/rails/generators/testing/setup_and_teardown.rb2
-rw-r--r--railties/lib/rails/info.rb51
-rw-r--r--railties/lib/rails/info_controller.rb34
-rw-r--r--railties/lib/rails/initializable.rb10
-rw-r--r--railties/lib/rails/mailers_controller.rb46
-rw-r--r--railties/lib/rails/paths.rb48
-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.rb92
-rw-r--r--railties/lib/rails/railtie.rb165
-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.rb7
-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.rake28
-rw-r--r--railties/lib/rails/tasks/framework.rake59
-rw-r--r--railties/lib/rails/tasks/initializers.rake11
-rw-r--r--railties/lib/rails/tasks/log.rake25
-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.rake9
-rw-r--r--railties/lib/rails/tasks/routes.rake38
-rw-r--r--railties/lib/rails/tasks/statistics.rake13
-rw-r--r--railties/lib/rails/tasks/tmp.rake29
-rw-r--r--railties/lib/rails/tasks/yarn.rake18
-rw-r--r--railties/lib/rails/templates/rails/mailers/email.html.erb60
-rw-r--r--railties/lib/rails/templates/rails/welcome/index.html.erb20
-rw-r--r--railties/lib/rails/test_help.rb40
-rw-r--r--railties/lib/rails/test_unit/line_filtering.rb73
-rw-r--r--railties/lib/rails/test_unit/minitest_plugin.rb101
-rw-r--r--railties/lib/rails/test_unit/railtie.rb11
-rw-r--r--railties/lib/rails/test_unit/reporter.rb26
-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.rake48
-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.rb41
-rw-r--r--railties/test/app_loader_test.rb36
-rw-r--r--railties/test/application/asset_debugging_test.rb123
-rw-r--r--railties/test/application/assets_test.rb119
-rw-r--r--railties/test/application/bin_setup_test.rb34
-rw-r--r--railties/test/application/configuration/custom_test.rb27
-rw-r--r--railties/test/application/configuration_test.rb1651
-rw-r--r--railties/test/application/console_test.rb97
-rw-r--r--railties/test/application/content_security_policy_test.rb223
-rw-r--r--railties/test/application/credentials_test.rb56
-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.rb44
-rw-r--r--railties/test/application/help_test.rb25
-rw-r--r--railties/test/application/initializers/frameworks_test.rb97
-rw-r--r--railties/test/application/initializers/hooks_test.rb9
-rw-r--r--railties/test/application/initializers/i18n_test.rb37
-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.rb44
-rw-r--r--railties/test/application/loading_test.rb177
-rw-r--r--railties/test/application/mailer_previews_test.rb317
-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.rb35
-rw-r--r--railties/test/application/middleware/remote_ip_test.rb16
-rw-r--r--railties/test/application/middleware/sendfile_test.rb15
-rw-r--r--railties/test/application/middleware/session_test.rb236
-rw-r--r--railties/test/application/middleware/static_test.rb28
-rw-r--r--railties/test/application/middleware_test.rb113
-rw-r--r--railties/test/application/multiple_applications_test.rb41
-rw-r--r--railties/test/application/paths_test.rb11
-rw-r--r--railties/test/application/per_request_digest_cache_test.rb32
-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.rb519
-rw-r--r--railties/test/application/rake/dev_test.rb50
-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.rb493
-rw-r--r--railties/test/application/rake/multi_dbs_test.rb251
-rw-r--r--railties/test/application/rake/notes_test.rb149
-rw-r--r--railties/test/application/rake/restart_test.rb26
-rw-r--r--railties/test/application/rake/routes_test.rb59
-rw-r--r--railties/test/application/rake/tmp_test.rb43
-rw-r--r--railties/test/application/rake_test.rb287
-rw-r--r--railties/test/application/rendering_test.rb19
-rw-r--r--railties/test/application/routing_test.rb415
-rw-r--r--railties/test/application/runner_test.rb87
-rw-r--r--railties/test/application/server_test.rb52
-rw-r--r--railties/test/application/test_runner_test.rb693
-rw-r--r--railties/test/application/test_test.rb130
-rw-r--r--railties/test/application/url_generation_test.rb15
-rw-r--r--railties/test/application/version_test.rb26
-rw-r--r--railties/test/application/zeitwerk_integration_test.rb304
-rw-r--r--railties/test/backtrace_cleaner_test.rb32
-rw-r--r--railties/test/code_statistics_calculator_test.rb46
-rw-r--r--railties/test/code_statistics_test.rb21
-rw-r--r--railties/test/command/base_test.rb15
-rw-r--r--railties/test/command/spellchecker_test.rb10
-rw-r--r--railties/test/commands/console_test.rb117
-rw-r--r--railties/test/commands/credentials_test.rb135
-rw-r--r--railties/test/commands/db_system_change_test.rb62
-rw-r--r--railties/test/commands/dbconsole_test.rb305
-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.rb48
-rw-r--r--railties/test/commands/notes_test.rb128
-rw-r--r--railties/test/commands/routes_test.rb319
-rw-r--r--railties/test/commands/secrets_test.rb77
-rw-r--r--railties/test/commands/server_test.rb251
-rw-r--r--railties/test/configuration/middleware_stack_proxy_test.rb24
-rw-r--r--railties/test/console_helpers.rb25
-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.rb320
-rw-r--r--railties/test/generators/api_app_generator_test.rb187
-rw-r--r--railties/test/generators/app_generator_test.rb905
-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.rb81
-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/db_system_change_generator_test.rb93
-rw-r--r--railties/test/generators/generated_attribute_test.rb60
-rw-r--r--railties/test/generators/generator_generator_test.rb14
-rw-r--r--railties/test/generators/generator_test.rb56
-rw-r--r--railties/test/generators/generators_test_helper.rb82
-rw-r--r--railties/test/generators/helper_generator_test.rb25
-rw-r--r--railties/test/generators/integration_test_generator_test.rb21
-rw-r--r--railties/test/generators/job_generator_test.rb23
-rw-r--r--railties/test/generators/mailer_generator_test.rb36
-rw-r--r--railties/test/generators/migration_generator_test.rb111
-rw-r--r--railties/test/generators/model_generator_test.rb185
-rw-r--r--railties/test/generators/named_base_test.rb187
-rw-r--r--railties/test/generators/namespaced_generators_test.rb37
-rw-r--r--railties/test/generators/orm_test.rb2
-rw-r--r--railties/test/generators/plugin_generator_test.rb417
-rw-r--r--railties/test/generators/plugin_test_helper.rb8
-rw-r--r--railties/test/generators/plugin_test_runner_test.rb77
-rw-r--r--railties/test/generators/resource_generator_test.rb20
-rw-r--r--railties/test/generators/scaffold_controller_generator_test.rb59
-rw-r--r--railties/test/generators/scaffold_generator_test.rb238
-rw-r--r--railties/test/generators/shared_generator_tests.rb333
-rw-r--r--railties/test/generators/system_test_generator_test.rb33
-rw-r--r--railties/test/generators/task_generator_test.rb10
-rw-r--r--railties/test/generators/test_runner_in_engine_test.rb14
-rw-r--r--railties/test/generators_test.rb66
-rw-r--r--railties/test/initializable_test.rb16
-rw-r--r--railties/test/isolation/abstract_unit.rb385
-rw-r--r--railties/test/isolation/assets/config/webpack/development.js3
-rw-r--r--railties/test/isolation/assets/config/webpack/production.js3
-rw-r--r--railties/test/isolation/assets/config/webpack/test.js3
-rw-r--r--railties/test/isolation/assets/config/webpacker.yml8
-rw-r--r--railties/test/isolation/assets/package.json11
-rw-r--r--railties/test/json_params_parsing_test.rb51
-rw-r--r--railties/test/minitest/rails_plugin_test.rb42
-rw-r--r--railties/test/path_generation_test.rb30
-rw-r--r--railties/test/paths_test.rb84
-rw-r--r--railties/test/rack_logger_test.rb50
-rw-r--r--railties/test/rails_info_controller_test.rb47
-rw-r--r--railties/test/rails_info_test.rb41
-rw-r--r--railties/test/railties/engine_test.rb374
-rw-r--r--railties/test/railties/generators_test.rb12
-rw-r--r--railties/test/railties/mounted_engine_test.rb73
-rw-r--r--railties/test/railties/railtie_test.rb62
-rw-r--r--railties/test/secrets_test.rb176
-rw-r--r--railties/test/test_unit/reporter_test.rb93
-rw-r--r--railties/test/version_test.rb4
-rw-r--r--tasks/release.rb270
-rw-r--r--tasks/release_announcement_draft.erb45
-rw-r--r--tools/README.md2
-rwxr-xr-xtools/console12
-rwxr-xr-xtools/profile78
-rw-r--r--tools/test.rb21
-rw-r--r--version.rb4
-rw-r--r--yarn.lock6078
3709 files changed, 221381 insertions, 91981 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml
new file mode 100644
index 0000000000..3c6e743df6
--- /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-66
+
+ratings:
+ paths:
+ - "**.rb"
+
+exclude_paths:
+ - ci/
+ - guides/
+ - tasks/
+ - tools/
diff --git a/.github/autolabeler.yml b/.github/autolabeler.yml
new file mode 100644
index 0000000000..6eb6f354e5
--- /dev/null
+++ b/.github/autolabeler.yml
@@ -0,0 +1,28 @@
+actioncable:
+ - "actioncable/**/*"
+actionmailbox:
+ - "actionmailbox/**/*"
+actionmailer:
+ - "actionmailer/**/*"
+actionpack:
+ - "actionpack/**/*"
+actiontext:
+ - "actiontext/**/*"
+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
index 2d071d4a71..0eb7cc22a2 100644
--- a/.github/issue_template.md
+++ b/.github/issue_template.md
@@ -1,13 +1,12 @@
### Steps to reproduce
-
-(Guidelines for creating a bug report are [available
-here](http://guides.rubyonrails.org/contributing_to_ruby_on_rails.html#creating-a-bug-report))
+<!-- (Guidelines for creating a bug report are [available
+here](https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#creating-a-bug-report)) -->
### Expected behavior
-Tell us what should happen
+<!-- Tell us what should happen -->
### Actual behavior
-Tell us what happens instead
+<!-- Tell us what happens instead -->
### System configuration
**Rails 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
index 48f7b0e214..7b0e9215f2 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,18 +1,21 @@
### Summary
-Provide a general description of the code changes in your pull
+<!-- 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.
+to keep the conversation linked together. -->
### Other Information
-If there's anything else that's important and relevant to your pull
+<!-- 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://guides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)
+here](https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)
-Thanks for contributing to Rails!
+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 9268977c2f..e127852627 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,21 +1,18 @@
-# Don't put *.swp, *.bak, etc here; those belong in a global ~/.gitignore.
+# Don't put *.swp, *.bak, etc here; those belong in a global .gitignore.
# Check out https://help.github.com/articles/ignoring-files for how to set that up.
.Gemfile
+.byebug_history
.ruby-version
-debug.log
-pkg
+/*/doc/
+/*/test/tmp/
/.bundle
-/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
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000000..54dcaf80f9
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,250 @@
+AllCops:
+ TargetRubyVersion: 2.5
+ # 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/**/*'
+ - 'actionmailbox/test/dummy/**/*'
+ - 'actiontext/test/dummy/**/*'
+ - '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/SpaceAfterSemicolon:
+ 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'
+ - 'activestorage/db/update_migrate/**/*.rb'
+ - 'actionmailbox/db/migrate/**/*.rb'
+ - 'actiontext/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
+
+Lint/AmbiguousOperator:
+ Enabled: true
+
+Lint/AmbiguousRegexpLiteral:
+ Enabled: true
+
+Lint/ErbNewArguments:
+ Enabled: true
+
+# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
+Lint/RequireParentheses:
+ Enabled: true
+
+Lint/ShadowingOuterLocalVariable:
+ Enabled: true
+
+Lint/StringConversionInInterpolation:
+ Enabled: true
+
+Lint/UriEscapeUnescape:
+ Enabled: true
+
+Lint/UselessAssignment:
+ Enabled: true
+
+Lint/DeprecatedClassMethods:
+ Enabled: true
+
+Style/ParenthesesAroundCondition:
+ Enabled: true
+
+Style/RedundantBegin:
+ 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/ReverseEach:
+ Enabled: true
+
+Performance/UnfreezeString:
+ Enabled: true
diff --git a/.travis.yml b/.travis.yml
index ef85107515..ba1263ad2e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,60 +1,163 @@
+dist: xenial
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
+ - mysql
+
+addons:
+ postgresql: 10
+ chrome: stable
+ apt:
+ sources:
+ - sourceline: "ppa:jonathonf/ffmpeg-3"
+ - sourceline: "ppa:ubuntuhandbook1/apps"
+ packages:
+ - ffmpeg
+ - mupdf
+ - mupdf-tools
+ - poppler-utils
+
+bundler_args: --jobs 3 --retry 3
before_install:
- - gem install bundler
- "rm ${BUNDLE_GEMFILE}.lock"
- - curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp
- - cd /tmp/beanstalkd-1.10/
- - make
- - ./beanstalkd &
- - cd $TRAVIS_BUILD_DIR
+ - "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 != 'actioncable:integration' ]] || yarn install"
+ - "[[ $GEM != 'actionview:ujs' ]] || nvm install node"
+ - "[[ $GEM != 'actionview:ujs' ]] || node --version"
+ - "[[ $GEM != 'actionview:ujs' ]] || (cd actionview && npm install)"
+ - "[[ $GEM != 'railties' ]] || (curl -o- -L https://yarnpkg.com/install.sh | bash)"
+ - "[[ $GEM != 'railties' ]] || export PATH=$HOME/.yarn/bin:$PATH"
+ - "[[ $GEM != 'activerecord:postgresql' ]] || sudo mount -o remount,size=50% /var/ramfs"
+
before_script:
- - 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")
+ - "[[ $GEM != 'railties' ]] || (cd actionview && yarn build)"
+ - "[[ $GEM != 'railties' ]] || (cd railties/test/isolation/assets && yarn install)"
+
+script: 'ci/travis.rb'
+
env:
+ global:
+ - "JRUBY_OPTS='--dev -J-Xmx1024M'"
matrix:
- "GEM=railties"
- - "GEM=ap"
- - "GEM=ac"
- - "GEM=ac FAYE=1"
- - "GEM=am,amo,as,av,aj"
- - "GEM=ar:mysql2"
- - "GEM=ar:sqlite3"
- - "GEM=ar:postgresql"
- - "GEM=aj:integration"
+ - "GEM=actionpack,actioncable"
+ - "GEM=actionmailer,activemodel,activesupport,actionview,activejob,activestorage,actionmailbox,actiontext"
+ - "GEM=activesupport PRESERVE_TIMEZONES=1"
+ - "GEM=activerecord:sqlite3"
+ - "GEM=activerecord:postgresql"
+ - "GEM=activerecord:mysql2"
- "GEM=guides"
+ - "GEM=actioncable:integration"
+
rvm:
- - 2.2.4
- - 2.3.0
+ - 2.5.3
+ - 2.6.0
- ruby-head
+
matrix:
include:
- # Latest compiled version in http://rubies.travis-ci.org
- - rvm: jruby-9.0.5.0
- jdk: oraclejdk8
+ - rvm: 2.5.3
+ env: "GEM=actionview:ujs"
+ - rvm: 2.5.3
+ sudo: required
+ env: "GEM=activejob:integration"
+ addons:
+ postgresql: 10
+ apt:
+ packages:
+ - rabbitmq-server
+ services:
+ - memcached
+ - redis-server
+ - rabbitmq-server
+ before_install:
+ - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/beanstalkd/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)"
+ - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd"
+ - rvm: 2.6.0
+ sudo: required
+ env: "GEM=activejob:integration"
+ addons:
+ postgresql: 10
+ apt:
+ packages:
+ - rabbitmq-server
+ services:
+ - memcached
+ - redis-server
+ - rabbitmq-server
+ before_install:
+ - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/beanstalkd/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)"
+ - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd"
+ - rvm: ruby-head
+ sudo: required
+ env: "GEM=activejob:integration"
+ addons:
+ postgresql: 10
+ apt:
+ packages:
+ - rabbitmq-server
+ services:
+ - memcached
+ - redis-server
+ - rabbitmq-server
+ before_install:
+ - "rm ${BUNDLE_GEMFILE}.lock"
+ - "travis_retry gem update --system"
+ - "travis_retry gem install bundler"
+ - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/beanstalkd/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)"
+ - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd"
+ - rvm: 2.5.3
env:
- - "JRUBY_OPTS='--dev -J-Xmx1024M'"
- - "GEM='ap'"
+ - "GEM=activerecord:mysql2 MYSQL=mariadb"
+ before_install:
+ - "sudo mysql_upgrade"
+ - "sudo service mysql restart"
+ addons:
+ mariadb: 10.3
+ - rvm: 2.5.3
+ env:
+ - "GEM=activerecord:sqlite3_mem"
+ - rvm: jruby-head
+ jdk: oraclejdk11
+ env:
+ - "GEM=actionpack"
+ - rvm: jruby-head
+ jdk: oraclejdk11
+ env:
+ - "GEM=actionmailer,activemodel,activejob"
allow_failures:
- rvm: ruby-head
+ - rvm: jruby-head
+ - env: "GEM=actioncable: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.4"
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
index 078d5f1219..2288e6a7d0 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -4,9 +4,9 @@ The Rails team is committed to fostering a welcoming community.
**Our Code of Conduct can be found here**:
-http://rubyonrails.org/conduct/
+https://rubyonrails.org/conduct/
For a history of updates, see the page history here:
-https://github.com/rails/rails.github.com/commits/master/conduct/index.html
+https://github.com/rails/homepage/commits/master/conduct.html
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 961b48733c..f97c1bbd0b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,6 +2,9 @@
#### **Did you find a bug?**
+* **Do not open up a GitHub issue if the bug is a security vulnerability
+ in Rails**, and instead to refer to our [security policy](https://rubyonrails.org/security/).
+
* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/rails/rails/issues).
* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/rails/rails/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
@@ -11,7 +14,7 @@
* [**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)
-* 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).
+* For more detailed information on submitting a bug report and creating an issue, visit our [reporting guidelines](https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#reporting-an-issue).
#### **Did you write a patch that fixes a bug?**
@@ -19,7 +22,11 @@
* 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.
+* Before submitting, please read the [Contributing to Ruby on Rails](https://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?**
@@ -33,10 +40,9 @@
#### **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).
+* Please read [Contributing to the Rails Documentation](https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation).
-</br>
-Ruby on Rails is a volunteer effort. We encourage you to pitch in and [join the team](http://contributors.rubyonrails.org)!
+Ruby on Rails is a volunteer effort. We encourage you to pitch in and [join the team](https://contributors.rubyonrails.org)!
Thanks! :heart: :heart: :heart:
diff --git a/Gemfile b/Gemfile
index 5205896846..e80691bce5 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,113 +1,143 @@
-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', '>= 11.1'
+gem "rake", ">= 11.1"
-# This needs to be with require false to ensure correct loading order, as it has to
-# be loaded after loading the test library.
-gem 'mocha', '~> 0.14', require: false
-
-gem 'rack-cache', '~> 1.2'
-gem 'jquery-rails'
-gem 'coffee-rails', '~> 4.1.0'
-gem 'turbolinks', github: 'turbolinks/turbolinks-rails'
+gem "capybara", ">= 2.15"
+gem "selenium-webdriver", ">= 3.5.0", "< 3.13.0"
+gem "rack-cache", "~> 1.2"
+gem "sass-rails"
+gem "turbolinks", "~> 5"
+gem "webpacker", "~> 4.0", 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 "bcrypt", "~> 3.1.11", require: false
# This needs to be with require false to avoid it being automatically loaded by
# sprockets.
-gem 'uglifier', '>= 1.3.0', require: false
+gem "uglifier", ">= 1.3.0", require: false
+
+# Explicitly avoid 1.x that doesn't support Ruby 2.4+
+gem "json", ">= 2.0.0"
-# Track stable branch of sass because it doesn't have circular require warnings.
-gem 'sass', github: 'sass/sass', branch: 'stable', require: false
+gem "rubocop", ">= 0.47", 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
-# Active Support.
-gem 'dalli', '>= 2.2.1'
-gem 'listen', '~> 3.0.5', require: false
+# Active Support
+gem "dalli"
+gem "listen", ">= 3.0.5", "< 3.2", require: false
+gem "libxml-ruby", platforms: :ruby
+gem "connection_pool", require: false
-# Active Job.
+# for railties app_generator_test
+gem "bootsnap", ">= 1.4.2", require: false
+
+# Active Job
group :job do
- gem 'resque', '< 1.26', 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: "QueueClassic/queue_classic", branch: 'master', 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: "QueueClassic/queue_classic", 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 "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
- gem 'em-hiredis', require: false
- gem 'hiredis', require: false
- gem 'redis', require: false
+# Action Mailbox
+gem "aws-sdk-sns", require: false
+gem "webmock"
- gem 'faye-websocket', require: false
+group :ujs do
+ gem "qunit-selenium"
+ gem "webdrivers"
end
# Add your own local bundler stuff.
-local_gemfile = File.dirname(__FILE__) + "/.Gemfile"
+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, :mswin, :mswin64, :mingw, :x64_mingw do
- gem 'nokogiri', '>= 1.6.7.1'
+ gem "nokogiri", ">= 1.8.1"
# Needed for compiling the ActionDispatch::Journey parser.
- gem 'racc', '>=1.4.6', require: false
+ gem "racc", ">=1.4.6", require: false
# Active Record.
- gem 'sqlite3', '~> 1.3.6'
+ gem "sqlite3", "~> 1.3", ">= 1.3.6"
group :db do
- gem 'pg', '>= 0.18.0'
- gem 'mysql2', '>= 0.4.0'
+ gem "pg", ">= 0.18.0"
+ gem "mysql2", ">= 0.4.10"
end
end
platforms :jruby do
- 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
@@ -115,17 +145,18 @@ end
platforms :rbx do
# The rubysl-yaml gem doesn't ship with Psych by default as it needs
# libyaml that isn't always available.
- gem 'psych', '~> 2.0'
+ gem "psych", "~> 3.0"
end
# Gems that are necessary for Active Record tests with Oracle.
-if ENV['ORACLE_ENHANCED']
+if ENV["ORACLE_ENHANCED"]
platforms :ruby do
- gem 'ruby-oci8', '~> 2.2'
+ 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 Active Record tests with IBM DB.
-gem 'ibm_db' if ENV['IBM_DB']
-gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
+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 ce854e6183..7617942267 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,275 +1,530 @@
GIT
- remote: git://github.com/QueueClassic/queue_classic.git
- revision: 2e3b624f3043849751b24a758d8c156337ead862
- branch: master
+ remote: https://github.com/QueueClassic/queue_classic.git
+ revision: 4cdc9b8e804badf7dea7078dd81092972d292c14
specs:
queue_classic (3.2.0.RC1)
- pg (>= 0.17, < 0.19)
+ pg (>= 0.17, < 2.0)
GIT
- remote: git://github.com/bkeepers/qu.git
- revision: d098e2657c92e89a6413bebd9c033930759c061f
- branch: master
+ remote: https://github.com/matthewd/websocket-client-simple.git
+ revision: e161305f1a466b9398d86df3b1731b03362da91b
+ branch: close-race
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
-
-GIT
- remote: git://github.com/sass/sass.git
- revision: ac8e7aa8e99c425237c708f347f76495a2e18519
- branch: stable
- specs:
- sass (3.4.21)
-
-GIT
- remote: git://github.com/turbolinks/turbolinks-rails.git
- revision: 65884729016dbb4d032f12bb01b7e7c1ddeb68ac
- specs:
- turbolinks (5.0.0.beta2)
- turbolinks-source
+ websocket-client-simple (0.3.0)
+ event_emitter
+ websocket
PATH
remote: .
specs:
- actioncable (5.0.0.beta3)
- actionpack (= 5.0.0.beta3)
- nio4r (~> 1.2)
- websocket-driver (~> 0.6.1)
- actionmailer (5.0.0.beta3)
- actionpack (= 5.0.0.beta3)
- actionview (= 5.0.0.beta3)
- activejob (= 5.0.0.beta3)
+ actioncable (6.0.0.beta3)
+ actionpack (= 6.0.0.beta3)
+ nio4r (~> 2.0)
+ websocket-driver (>= 0.6.1)
+ actionmailbox (6.0.0.beta3)
+ actionpack (= 6.0.0.beta3)
+ activejob (= 6.0.0.beta3)
+ activerecord (= 6.0.0.beta3)
+ activestorage (= 6.0.0.beta3)
+ activesupport (= 6.0.0.beta3)
+ mail (>= 2.7.1)
+ actionmailer (6.0.0.beta3)
+ actionpack (= 6.0.0.beta3)
+ actionview (= 6.0.0.beta3)
+ activejob (= 6.0.0.beta3)
mail (~> 2.5, >= 2.5.4)
- rails-dom-testing (~> 1.0, >= 1.0.5)
- actionpack (5.0.0.beta3)
- actionview (= 5.0.0.beta3)
- activesupport (= 5.0.0.beta3)
- rack (~> 2.x)
- rack-test (~> 0.6.3)
- rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-dom-testing (~> 2.0)
+ actionpack (6.0.0.beta3)
+ actionview (= 6.0.0.beta3)
+ activesupport (= 6.0.0.beta3)
+ rack (~> 2.0)
+ rack-test (>= 0.6.3)
+ rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (5.0.0.beta3)
- activesupport (= 5.0.0.beta3)
+ actiontext (6.0.0.beta3)
+ actionpack (= 6.0.0.beta3)
+ activerecord (= 6.0.0.beta3)
+ activestorage (= 6.0.0.beta3)
+ activesupport (= 6.0.0.beta3)
+ nokogiri (>= 1.8.5)
+ actionview (6.0.0.beta3)
+ activesupport (= 6.0.0.beta3)
builder (~> 3.1)
- erubis (~> 2.7.0)
- rails-dom-testing (~> 1.0, >= 1.0.5)
- rails-html-sanitizer (~> 1.0, >= 1.0.2)
- activejob (5.0.0.beta3)
- activesupport (= 5.0.0.beta3)
+ erubi (~> 1.4)
+ rails-dom-testing (~> 2.0)
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
+ activejob (6.0.0.beta3)
+ activesupport (= 6.0.0.beta3)
globalid (>= 0.3.6)
- activemodel (5.0.0.beta3)
- activesupport (= 5.0.0.beta3)
- activerecord (5.0.0.beta3)
- activemodel (= 5.0.0.beta3)
- activesupport (= 5.0.0.beta3)
- arel (~> 7.0)
- activesupport (5.0.0.beta3)
- concurrent-ruby (~> 1.0)
- i18n (~> 0.7)
+ activemodel (6.0.0.beta3)
+ activesupport (= 6.0.0.beta3)
+ activerecord (6.0.0.beta3)
+ activemodel (= 6.0.0.beta3)
+ activesupport (= 6.0.0.beta3)
+ activestorage (6.0.0.beta3)
+ actionpack (= 6.0.0.beta3)
+ activejob (= 6.0.0.beta3)
+ activerecord (= 6.0.0.beta3)
+ marcel (~> 0.3.1)
+ activesupport (6.0.0.beta3)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
- rails (5.0.0.beta3)
- actioncable (= 5.0.0.beta3)
- actionmailer (= 5.0.0.beta3)
- actionpack (= 5.0.0.beta3)
- actionview (= 5.0.0.beta3)
- activejob (= 5.0.0.beta3)
- activemodel (= 5.0.0.beta3)
- activerecord (= 5.0.0.beta3)
- activesupport (= 5.0.0.beta3)
- bundler (>= 1.3.0, < 2.0)
- railties (= 5.0.0.beta3)
+ zeitwerk (~> 1.4, >= 1.4.3)
+ rails (6.0.0.beta3)
+ actioncable (= 6.0.0.beta3)
+ actionmailbox (= 6.0.0.beta3)
+ actionmailer (= 6.0.0.beta3)
+ actionpack (= 6.0.0.beta3)
+ actiontext (= 6.0.0.beta3)
+ actionview (= 6.0.0.beta3)
+ activejob (= 6.0.0.beta3)
+ activemodel (= 6.0.0.beta3)
+ activerecord (= 6.0.0.beta3)
+ activestorage (= 6.0.0.beta3)
+ activesupport (= 6.0.0.beta3)
+ bundler (>= 1.3.0)
+ railties (= 6.0.0.beta3)
sprockets-rails (>= 2.0.0)
- railties (5.0.0.beta3)
- actionpack (= 5.0.0.beta3)
- activesupport (= 5.0.0.beta3)
+ railties (6.0.0.beta3)
+ actionpack (= 6.0.0.beta3)
+ activesupport (= 6.0.0.beta3)
method_source
rake (>= 0.8.7)
- thor (>= 0.18.1, < 2.0)
+ thor (>= 0.20.3, < 2.0)
GEM
remote: https://rubygems.org/
specs:
- amq-protocol (2.0.1)
- arel (7.0.0)
- backburner (1.2.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)
+ 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-sdk-sns (1.8.1)
+ aws-sdk-core (~> 3, >= 3.37.0)
+ 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.11)
- bcrypt (3.1.11-x64-mingw32)
- bcrypt (3.1.11-x86-mingw32)
+ 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.3.0)
- builder (3.2.2)
- bunny (2.2.2)
- amq-protocol (>= 2.0.1)
- byebug (8.2.1)
- coffee-rails (4.1.1)
- coffee-script (>= 2.2.0)
- railties (>= 4.0.0, < 5.1.x)
+ 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.4.2)
+ msgpack (~> 1.0)
+ bootsnap (1.4.2-java)
+ msgpack (~> 1.0)
+ builder (3.2.3)
+ bunny (2.13.0)
+ amq-protocol (~> 2.3, >= 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)
coffee-script (2.4.1)
coffee-script-source
execjs
- coffee-script-source (1.10.0)
- concurrent-ruby (1.0.1)
- connection_pool (2.2.0)
- dalli (2.7.6)
+ coffee-script-source (1.12.2)
+ concurrent-ruby (1.1.4)
+ connection_pool (2.2.2)
+ cookiejar (0.3.3)
+ crack (0.4.3)
+ safe_yaml (~> 1.0.0)
+ crass (1.0.4)
+ curses (1.0.2)
+ daemons (1.2.6)
+ dalli (2.7.9)
dante (0.2.0)
- delayed_job (4.1.1)
- activesupport (>= 3.0, < 5.0)
- delayed_job_active_record (4.1.0)
- activerecord (>= 3.0, < 5)
+ 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)
- em-hiredis (0.3.1)
- eventmachine (~> 1.0)
- hiredis (~> 0.6.0)
- erubis (2.7.0)
- eventmachine (1.0.9.1)
- execjs (2.6.0)
- faye-websocket (0.10.3)
+ 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.8.0)
+ 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.10)
- ffi (1.9.10-x64-mingw32)
- ffi (1.9.10-x86-mingw32)
- globalid (0.3.6)
- activesupport (>= 4.1.0)
- hiredis (0.6.1)
- i18n (0.7.0)
- jquery-rails (4.1.0)
- rails-dom-testing (~> 1.0)
- railties (>= 4.2.0)
- thor (>= 0.14, < 2.0)
- json (1.8.3)
- kindlerb (0.1.1)
+ ffi (1.10.0)
+ ffi (1.10.0-java)
+ ffi (1.10.0-x64-mingw32)
+ ffi (1.10.0-x86-mingw32)
+ fugit (1.1.6)
+ et-orbi (~> 1.1, >= 1.1.6)
+ raabro (~> 1.1)
+ globalid (0.4.2)
+ 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)
+ hashdiff (0.3.7)
+ hiredis (0.6.3)
+ hiredis (0.6.3-java)
+ http_parser.rb (0.6.0)
+ httpclient (2.8.3)
+ i18n (1.6.0)
+ concurrent-ruby (~> 1.0)
+ image_processing (1.7.1)
+ mini_magick (~> 4.0)
+ ruby-vips (>= 2.0.13, < 3)
+ jaro_winkler (1.5.2)
+ jaro_winkler (1.5.2-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
- listen (3.0.5)
- rb-fsevent (>= 0.9.3)
- rb-inotify (>= 0.9)
- loofah (2.0.3)
+ 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)
- mail (2.6.3)
- mime-types (>= 1.16, < 3)
- metaclass (0.0.4)
- method_source (0.8.2)
- mime-types (2.99.1)
- mini_portile2 (2.0.0)
- 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.2)
+ mime-types (3.2.2)
+ mime-types-data (~> 3.2015)
+ mime-types-data (3.2018.0812)
+ mimemagic (0.3.3)
+ mini_magick (4.9.2)
+ mini_mime (1.0.1)
+ mini_portile2 (2.4.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.2)
- mustache (1.0.2)
- mysql2 (0.4.2)
- mysql2 (0.4.2-x64-mingw32)
- mysql2 (0.4.2-x86-mingw32)
- nio4r (1.2.1)
- nokogiri (1.6.7.2)
- mini_portile2 (~> 2.0.0.rc2)
- nokogiri (1.6.7.2-x64-mingw32)
- mini_portile2 (~> 2.0.0.rc2)
- nokogiri (1.6.7.2-x86-mingw32)
- mini_portile2 (~> 2.0.0.rc2)
- pg (0.18.4)
- pg (0.18.4-x64-mingw32)
- pg (0.18.4-x86-mingw32)
- psych (2.0.17)
- puma (2.16.0)
- que (0.11.2)
- racc (1.4.14)
- rack (2.0.0.alpha)
- json
- rack-cache (1.5.1)
+ msgpack (1.2.9)
+ msgpack (1.2.9-java)
+ msgpack (1.2.9-x64-mingw32)
+ msgpack (1.2.9-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)
+ net_http_ssl_fix (0.0.10)
+ nio4r (2.3.1)
+ nio4r (2.3.1-java)
+ nokogiri (1.9.1)
+ mini_portile2 (~> 2.4.0)
+ nokogiri (1.9.1-java)
+ nokogiri (1.9.1-x64-mingw32)
+ mini_portile2 (~> 2.4.0)
+ nokogiri (1.9.1-x86-mingw32)
+ mini_portile2 (~> 2.4.0)
+ os (1.0.0)
+ parallel (1.13.0)
+ parser (2.6.2.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)
+ psych (3.1.0)
+ psych (3.1.0-java)
+ jar-dependencies (>= 0.1.7)
+ psych (3.1.0-x64-mingw32)
+ psych (3.1.0-x86-mingw32)
+ public_suffix (3.0.3)
+ puma (3.12.1)
+ puma (3.12.1-java)
+ que (0.14.3)
+ qunit-selenium (0.0.4)
+ selenium-webdriver
+ thor
+ raabro (1.1.6)
+ racc (1.4.15)
+ rack (2.0.6)
+ rack-cache (1.8.0)
rack (>= 0.4)
- rack-test (0.6.3)
- rack (>= 1.0)
- rails-deprecated_sanitizer (1.0.3)
- activesupport (>= 4.2.0.alpha)
- rails-dom-testing (1.0.7)
- activesupport (>= 4.2.0.beta, < 5.0)
- nokogiri (~> 1.6.0)
- rails-deprecated_sanitizer (>= 1.0.1)
- rails-html-sanitizer (1.0.3)
- loofah (~> 2.0)
- rake (11.1.1)
- rb-fsevent (0.9.7)
- rb-inotify (0.9.5)
- ffi (>= 0.5.0)
- rdoc (4.2.1)
+ rack-protection (2.0.4)
+ rack
+ rack-proxy (0.6.5)
+ rack
+ 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)
+ rb-inotify (0.10.0)
+ ffi (~> 1.0)
+ rdoc (6.0.4)
redcarpet (3.2.3)
- redis (3.2.2)
- redis-namespace (1.5.2)
- 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.3.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.2.0)
- sdoc (0.4.1)
- json (~> 1.7, >= 1.7.7)
- rdoc (~> 4.0)
- sequel (4.30.0)
- serverengine (1.5.11)
+ redis (>= 3.3, < 5)
+ resque (~> 1.26)
+ rufus-scheduler (~> 3.2)
+ retriable (3.1.2)
+ rubocop (0.66.0)
+ jaro_winkler (~> 1.5.1)
+ parallel (~> 1.10)
+ parser (>= 2.5, != 2.5.1.1)
+ psych (>= 3.1.0)
+ rainbow (>= 2.2.2, < 4.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 1.4.0, < 1.6)
+ 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)
+ safe_yaml (1.0.4)
+ 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.12.0)
+ childprocess (~> 0.5)
+ rubyzip (~> 1.2)
+ sequel (5.14.0)
+ serverengine (2.0.7)
sigdump (~> 0.2.2)
- sidekiq (4.0.2)
+ 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.11.0)
+ bunny (~> 2.12)
concurrent-ruby (~> 1.0)
- connection_pool (~> 2.2, >= 2.2.0)
- redis (~> 3.2, >= 3.2.1)
- sigdump (0.2.3)
- sinatra (1.0)
- rack (>= 1.0)
- sneakers (2.3.5)
- bunny (~> 2.2.0)
- serverengine (~> 1.5.11)
+ rake
+ serverengine (~> 2.0.5)
thor
- thread (~> 0.1.7)
- sprockets (3.5.2)
+ sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
- sprockets-rails (3.0.4)
+ sprockets-export (1.0.0)
+ sprockets-rails (3.2.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
- sqlite3 (1.3.11)
- sqlite3 (1.3.11-x64-mingw32)
- sqlite3 (1.3.11-x86-mingw32)
- stackprof (0.2.8)
- sucker_punch (2.0.0)
- concurrent-ruby (~> 1.0.0)
- thor (0.19.1)
- thread (0.1.7)
- thread_safe (0.3.5)
- turbolinks-source (5.0.0.beta2)
- tzinfo (1.2.2)
+ 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)
- tzinfo-data (1.2015.7)
+ tzinfo-data (1.2018.7)
tzinfo (>= 1.0.0)
- uglifier (2.7.2)
- execjs (>= 0.3.0)
- json (>= 1.8.0)
+ uber (0.1.0)
+ uglifier (4.1.19)
+ execjs (>= 0.3.0, < 3)
+ unicode-display_width (1.5.0)
+ useragent (0.16.10)
vegas (0.1.11)
rack (>= 1.0.0)
- w3c_validators (1.2)
- json
- nokogiri
- websocket-driver (0.6.3)
+ w3c_validators (1.3.4)
+ json (>= 1.8)
+ nokogiri (~> 1.6)
+ wdm (0.1.1)
+ webdrivers (3.7.0)
+ net_http_ssl_fix
+ nokogiri (~> 1.6)
+ rubyzip (~> 1.0)
+ selenium-webdriver (~> 3.0)
+ webmock (3.4.2)
+ addressable (>= 2.3.6)
+ crack (>= 0.3.2)
+ hashdiff
+ webpacker (4.0.2)
+ activesupport (>= 4.2)
+ rack-proxy (>= 0.6.1)
+ railties (>= 4.2)
+ 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.2)
+ websocket-extensions (0.1.3)
+ xpath (3.2.0)
+ nokogiri (~> 1.8)
+ zeitwerk (1.4.3)
PLATFORMS
+ java
ruby
x64-mingw32
x86-mingw32
@@ -278,51 +533,67 @@ DEPENDENCIES
activerecord-jdbcmysql-adapter (>= 1.3.0)
activerecord-jdbcpostgresql-adapter (>= 1.3.0)
activerecord-jdbcsqlite3-adapter (>= 1.3.0)
+ aws-sdk-s3
+ aws-sdk-sns
+ azure-storage
backburner
bcrypt (~> 3.1.11)
benchmark-ips
+ blade
+ blade-sauce_labs_plugin
+ bootsnap (>= 1.4.2)
byebug
- coffee-rails (~> 4.1.0)
- dalli (>= 2.2.1)
+ capybara (>= 2.15)
+ connection_pool
+ dalli
delayed_job
delayed_job_active_record
- em-hiredis
- faye-websocket
+ google-cloud-storage (~> 1.11)
hiredis
- jquery-rails
- kindlerb (= 0.1.1)
- listen (~> 3.0.5)
- minitest (< 5.3.4)
- mocha (~> 0.14)
- mysql2 (>= 0.4.0)
- nokogiri (>= 1.6.7.1)
+ 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)
+ psych (~> 3.0)
puma
- qu-rails!
- qu-redis
que
queue_classic!
+ qunit-selenium
racc (>= 1.4.6)
rack-cache (~> 1.2)
rails!
rake (>= 11.1)
redcarpet (~> 3.2.3)
- redis
- resque (< 1.26)
+ redis (~> 4.0)
+ redis-namespace
+ resque
resque-scheduler
- sass!
- sdoc (~> 0.4.0)
+ rubocop (>= 0.47)
+ sass-rails
+ sdoc (~> 1.0)
+ selenium-webdriver (>= 3.5.0, < 3.13.0)
sequel
sidekiq
sneakers
- sqlite3 (~> 1.3.6)
+ sprockets-export
+ sqlite3 (~> 1.3, >= 1.3.6)
stackprof
sucker_punch
- turbolinks!
+ turbolinks (~> 5)
tzinfo-data
uglifier (>= 1.3.0)
w3c_validators
+ wdm (>= 0.1.0)
+ webdrivers
+ webmock
+ webpacker (~> 4.0)
+ websocket-client-simple!
BUNDLED WITH
- 1.11.2
+ 1.17.3
diff --git a/MIT-LICENSE b/MIT-LICENSE
new file mode 100644
index 0000000000..315a4bc7c4
--- /dev/null
+++ b/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2005-2019 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 d727b28ee9..6b829f5d9b 100644
--- a/RAILS_VERSION
+++ b/RAILS_VERSION
@@ -1 +1 @@
-5.0.0.beta3
+6.0.0.beta3
diff --git a/README.md b/README.md
index a2b726ea6c..566136e2a2 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,55 @@
# Welcome to Rails
+## What's Rails?
+
Rails is a web-application framework that includes everything needed to
create database-backed web applications according to the
-[Model-View-Controller (MVC)](http://en.wikipedia.org/wiki/Model-view-controller)
+[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.
+
+## Controller layer
-The _Controller layer_ is responsible for handling incoming HTTP requests and
+The _**Controller layer**_ is responsible for handling incoming HTTP requests and
providing a suitable response. Usually this means returning HTML, but Rails controllers
can also generate XML, JSON, PDFs, mobile-specific views, and more. Controllers load and
manipulate models, and render view templates in order to generate the appropriate HTTP response.
In Rails, incoming requests are routed by Action Dispatch to an appropriate controller, and
controller classes are derived from `ActionController::Base`. Action Dispatch and Action Controller
-are bundled together in Action Pack. You can read more about Action Pack in its
-[README](actionpack/README.rdoc).
+are bundled together in [Action Pack](actionpack/README.rdoc).
-The _View layer_ is composed of "templates" that are responsible for providing
+## View layer
+
+The _**View layer**_ is composed of "templates" that are responsible for providing
appropriate representations of your application's resources. Templates can
come in a variety of formats, but most view templates are HTML with embedded
Ruby code (ERB files). Views are typically rendered to generate a controller response,
-or to generate the body of an email. In Rails, View generation is handled by Action View.
-You can read more about Action View in its [README](actionview/README.rdoc).
-
-Active Record, Active Model, Action Pack, and Action View can each be used independently outside Rails.
-In addition to that, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library
-to generate and send emails; Active Job ([README](activejob/README.md)), a
-framework for declaring jobs and making them run on a variety of queueing
-backends; Action Cable ([README](actioncable/README.md)), a framework to
-integrate WebSockets with a Rails application;
-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; [Action Mailbox](actionmailbox/README.md), a library to receive emails within a Rails application;
+[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; [Action Text](actiontext/README.md), a library to handle rich text content;
+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.
@@ -63,22 +72,28 @@ and may also be used independently outside Rails.
Run with `--help` or `-h` for options.
-4. Using a browser, go to `http://localhost:3000` and you'll see:
+4. Go to `http://localhost:3000` and you'll see:
"Yay! You’re on Rails!"
5. Follow the guidelines to start developing your application. You may find
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](http://rubyonrails.org/conduct/).
+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
@@ -86,4 +101,4 @@ Everyone interacting in Rails and its sub-projects' codebases, issue trackers, c
## 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
index 037aa8c119..dd2742b403 100644
--- a/RELEASING_RAILS.md
+++ b/RELEASING_RAILS.md
@@ -1,35 +1,35 @@
# 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
+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:
+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")
+### 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:
+Do not release with a Red CI. You can find the CI status here:
```
-http://travis-ci.org/rails/rails
+https://travis-ci.org/rails/rails
```
-### Is Sam Ruby happy? If not, make him happy.
+### Is Sam Ruby happy? If not, make him happy.
Sam Ruby keeps a [test suite](https://github.com/rubys/awdwr) that makes
-sure the code samples in his book
-([Agile Web Development with Rails](https://pragprog.com/titles/rails4/agile-web-development-with-rails-4th-edition))
-all work. These are valuable system tests
-for Rails. You can check the status of these tests here:
+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.
+### 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.
@@ -38,16 +38,16 @@ 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
+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
+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.
+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:
@@ -58,20 +58,20 @@ lists:
Implementors will love you and help you.
-### 3 Days before release
+## 3 Days before release
-This is when you should release the release candidate. Here are your tasks
+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 the CI green? If not, make it green.
-### Is Sam Ruby happy? If not, make him happy.
+### Is Sam Ruby happy? If not, make him happy.
-### Contact the security team. CVE emails must be sent on this day.
+### 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
+From the stable branch, create a release branch. For example, if you're
releasing Rails 3.0.10, do this:
```
@@ -82,7 +82,7 @@ Switched to a new branch '3-0-10'
### Update each CHANGELOG.
-Many times commits are made without the CHANGELOG being updated. You should
+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.
@@ -96,58 +96,69 @@ 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.
+### 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 install` to generate the gems and install them locally. Then try
-generating a new app and ensure that nothing explodes.
+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 the gem.
+### Release to RubyGems and npm.
-IMPORTANT: Due to YAML parse problems on the rubygems.org server, it is safest
-to use Ruby 1.8 when releasing.
+IMPORTANT: Several gems have JavaScript components that are released as npm
+packages, so you must have Node.js installed, have an npm account (npmjs.com),
+and be a package owner for `@rails/actioncable`, `@rails/actiontext`,
+`@rails/activestorage`, and `@rails/ujs`. You can check this by making sure your
+npm user (`npm whoami`) is listed as an owner (`npm owner ls <pkg>`) of each
+package. Do not release until you're set up with npm!
-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:
+The release task will sign the release tag. If you haven't got commit signing
+set up, use https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work as a
+guide. You can generate keys with the GPG suite from here: https://gpgtools.org.
-```
-$ 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
-```
+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
+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
+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*
+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
+If you used Markdown format for your email, you can just paste it into the
blog.
-* http://weblog.rubyonrails.org
+* https://weblog.rubyonrails.org
### Post the announcement to the Rails Twitter account.
@@ -157,12 +168,12 @@ 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
+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
+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
@@ -173,7 +184,7 @@ more explanation on a particular step, see the RC steps.
Today, do this stuff in this order:
* Apply security patches to the release branch
-* Update CHANGELOG with security fixes.
+* Update CHANGELOG with security fixes
* Update RAILS_VERSION to remove the rc
* Build and test the gem
* Release the gems
@@ -195,12 +206,12 @@ Email the security reports to:
* 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,
+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.
+* Merge the release branch to the stable branch
* Drink beer (or other cocktail)
## Misc
diff --git a/Rakefile b/Rakefile
index 1d70ce96ea..a67f8fd028 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,20 +1,25 @@
-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 "Build, install and verify the gem files in a generated Rails app."
+task verify: "all:verify"
desc "Prepare the release"
-task :prep_release => "all:prep_release"
+task prep_release: "all:prep_release"
desc "Release all gems to rubygems and create a tag"
-task :release => "all:release"
+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"
@@ -23,9 +28,6 @@ task :default => %w(test test:isolated)
FRAMEWORKS.each do |project|
system(%(cd #{project} && #{$0} #{task_name} --trace)) || errors << project
end
- if task_name =~ /test/
- system(%(cd actioncable && env FAYE=1 #{$0} #{task_name} --trace)) || errors << 'actioncable-faye'
- end
fail("Errors in #{errors.join(', ')}") unless errors.empty?
end
end
@@ -36,21 +38,20 @@ task :smoke do
system %(cd #{project} && #{$0} test:isolated --trace)
end
system %(cd activerecord && #{$0} sqlite3:isolated_test --trace)
- system %(cd actioncable && env FAYE=1 #{$0} test:isolated --trace)
end
desc "Install gems for all projects."
-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:
@@ -61,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
index 0a04b29786..7fa7c03e03 100644
--- a/actioncable/.gitignore
+++ b/actioncable/.gitignore
@@ -1,2 +1,4 @@
-/lib/assets/compiled
-/tmp
+/app/javascript/action_cable/internal.js
+/src
+/test/javascript/compiled/
+/tmp/
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md
index d59e48e00c..9f312f8806 100644
--- a/actioncable/CHANGELOG.md
+++ b/actioncable/CHANGELOG.md
@@ -1,61 +1,154 @@
-* Pubsub: automatic stream decoding.
+## Rails 6.0.0.beta3 (March 11, 2019) ##
- stream_for @room, coder: ActiveSupport::JSON do |message|
- # `message` is a Ruby hash here instead of a JSON string
+* No changes.
- The `coder` must respond to `#decode`. Defaults to `coder: nil`
- which skips decoding entirely.
- *Jeremy Daer*
+## Rails 6.0.0.beta2 (February 25, 2019) ##
-* Add ActiveSupport::Notifications to ActionCable::Channel.
+* PostgreSQL subscription adapters now support `channel_prefix` option in cable.yml
- *Matthew Wear*
+ Avoids channel name collisions when multiple apps use the same database for Action Cable.
-* Safely support autoloading and class unloading, by preventing concurrent
- loads, and disconnecting all cables during reload.
+ *Vladimir Dementyev*
- *Matthew Draper*
+* Allow passing custom configuration to `ActionCable::Server::Base`.
-* Ensure ActionCable behaves correctly for non-string queue names.
+ You can now create a standalone Action Cable server with a custom configuration
+ (e.g. to run it in isolation from the default one):
- *Jay Hayes*
+ ```ruby
+ config = ActionCable::Server::Configuration.new
+ config.cable = { adapter: "redis", channel_prefix: "custom_" }
-## Rails 5.0.0.beta3 (February 24, 2016) ##
+ CUSTOM_CABLE = ActionCable::Server::Base.new(config: config)
+ ```
-* Added `em_redis_connector` and `redis_connector` to
- `ActionCable::SubscriptionAdapter::EventedRedis` and added `redis_connector`
- to `ActionCable::SubscriptionAdapter::Redis`, so you can overwrite with your
- own initializers. This is used when you want to use different-than-standard
- Redis adapters, like for Makara distributed Redis.
+ Then you can mount it in the `routes.rb` file:
- *DHH*
+ ```ruby
+ Rails.application.routes.draw do
+ mount CUSTOM_CABLE => "/custom_cable"
+ # ...
+ end
+ ```
-## Rails 5.0.0.beta2 (February 01, 2016) ##
+ *Vladimir Dementyev*
-* Support PostgreSQL pubsub adapter.
+* Add `:action_cable_connection` and `:action_cable_channel` load hooks.
- *Jon Moss*
+ You can use them to extend `ActionCable::Connection::Base` and `ActionCable::Channel::Base`
+ functionality:
-* Remove EventMachine dependency.
+ ```ruby
+ ActiveSupport.on_load(:action_cable_channel) do
+ # do something in the context of ActionCable::Channel::Base
+ end
+ ```
- *Matthew Draper*
+ *Vladimir Dementyev*
-* Remove Celluloid dependency.
+* Add `Channel::Base#broadcast_to`.
- *Mike Perham*
+ You can now call `broadcast_to` within a channel action, which equals to
+ the `self.class.broadcast_to`.
-* Create notion of an `ActionCable::SubscriptionAdapter`.
- Separate out Redis functionality into
- `ActionCable::SubscriptionAdapter::Redis`, and add a
- PostgreSQL adapter as well. Configuration file for
- ActionCable was changed from`config/redis/cable.yml` to
- `config/cable.yml`.
+ *Vladimir Dementyev*
- *Jon Moss*
+* Make `Channel::Base.broadcasting_for` a public API.
-## Rails 5.0.0.beta1 (December 18, 2015) ##
+ You can use `.broadcasting_for` to generate a unique stream identifier within
+ a channel for the specified target (e.g. Active Record model):
-* Added to Rails!
+ ```ruby
+ ChatChannel.broadcasting_for(model) # => "chat:<model.to_gid_param>"
+ ```
- *DHH*
+ *Vladimir Dementyev*
+
+
+## Rails 6.0.0.beta1 (January 18, 2019) ##
+
+* [Rename npm package](https://github.com/rails/rails/pull/34905) from
+ [`actioncable`](https://www.npmjs.com/package/actioncable) to
+ [`@rails/actioncable`](https://www.npmjs.com/package/@rails/actioncable).
+
+ *Javan Makhmali*
+
+* Merge [`action-cable-testing`](https://github.com/palkan/action-cable-testing) to Rails.
+
+ *Vladimir Dementyev*
+
+* The JavaScript WebSocket client will no longer try to reconnect
+ when you call `reject_unauthorized_connection` on the connection.
+
+ *Mick Staugaard*
+
+* `ActionCable.Connection#getState` now references the configurable
+ `ActionCable.adapters.WebSocket` property rather than the `WebSocket` global
+ variable, matching the behavior of `ActionCable.Connection#open`.
+
+ *Richard Macklin*
+
+* The ActionCable javascript package has been converted from CoffeeScript
+ to ES2015, and we now publish the source code in the npm distribution.
+
+ This allows ActionCable users to depend on the javascript source code
+ rather than the compiled code, which can produce smaller javascript bundles.
+
+ This change includes some breaking changes to optional parts of the
+ ActionCable javascript API:
+
+ - Configuration of the WebSocket adapter and logger adapter have been moved
+ from properties of `ActionCable` to properties of `ActionCable.adapters`.
+ If you are currently configuring these adapters you will need to make
+ these changes when upgrading:
+
+ ```diff
+ - ActionCable.WebSocket = MyWebSocket
+ + ActionCable.adapters.WebSocket = MyWebSocket
+ ```
+ ```diff
+ - ActionCable.logger = myLogger
+ + ActionCable.adapters.logger = myLogger
+ ```
+
+ - The `ActionCable.startDebugging()` and `ActionCable.stopDebugging()`
+ methods have been removed and replaced with the property
+ `ActionCable.logger.enabled`. If you are currently using these methods you
+ will need to make these changes when upgrading:
+
+ ```diff
+ - ActionCable.startDebugging()
+ + ActionCable.logger.enabled = true
+ ```
+ ```diff
+ - ActionCable.stopDebugging()
+ + ActionCable.logger.enabled = false
+ ```
+
+ *Richard Macklin*
+
+* 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.5.0 or newer.
+
+ *Jeremy Daer*, *Kasper Timm Hansen*
+
+
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actioncable/CHANGELOG.md) for previous changes.
diff --git a/actioncable/MIT-LICENSE b/actioncable/MIT-LICENSE
index 27a17cf41b..bdb42d988a 100644
--- a/actioncable/MIT-LICENSE
+++ b/actioncable/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2015-2016 Basecamp, LLC
+Copyright (c) 2015-2019 Basecamp, LLC
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actioncable/README.md b/actioncable/README.md
index 595830feb0..38eb251f42 100644
--- a/actioncable/README.md
+++ b/actioncable/README.md
@@ -7,468 +7,15 @@ 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
-
- protected
- def find_verified_user
- if current_user = User.find_by(id: cookies.signed[:user_id])
- current_user
- else
- reject_unauthorized_connection
- end
- end
- end
-end
-```
-Here `identified_by` is a connection identifier that can be used to find the specific connection 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:
-
-```coffeescript
-# app/assets/javascripts/cable.coffee
-#= require action_cable
-
-@App = {}
-App.cable = ActionCable.createConsumer("ws://cable.example.com")
-```
-
-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 "page:change.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 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](http://github.com/rails/actioncable-examples) repository for a full example of how to setup Action Cable in a Rails app, and how to add channels.
-
-
-## Configuration
-
-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 specified origins, which are passed to the server config as an array. The origins can be instances of strings or regular expressions, against which a check for match will be performed.
-
-```ruby
-Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/]
-```
-
-When running in the development environment, this defaults to "http://localhost:3000".
-
-To disable and allow requests from any origin:
-
-```ruby
-Rails.application.config.action_cable.disable_request_forgery_protection = true
-```
-
-### Consumer Configuration
-
-Once you have decided how to run your cable server (see below), you must provide the server URL (or path) to your client-side setup.
-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 close to what we're using in Basecamp:
-
-```ruby
-Rails.application.config.action_cable.log_tags = [
- -> request { request.env['bc.account_id'] || "no-account" },
- :action_cable,
- -> request { request.uuid }
-]
-```
-
-For a full list of all configuration options, see the `ActionCable::Server::Configuration` class.
-
-Also note that your server must provide at least the same number of database connections as you have workers. The default worker pool is set to 100, so that means you have to make at least that available. You can change that in `config/database.yml` through the `pool` attribute.
-
-
-## Running 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 ::File.expand_path('../../config/environment', __FILE__)
-Rails.application.eager_load!
-
-run ActionCable.server
-```
-
-Then you start the server using a binstub in bin/cable ala:
-```
-#!/bin/bash
-bundle exec puma -p 28080 cable/config.ru
-```
-
-The above will start a cable server on port 28080.
-
-### In app
-
-If you are using a server that supports the [Rack socket hijacking API](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking), Action Cable can run alongside your Rails application. For example, to listen for WebSocket requests on `/cable`, mount the server at that path:
-
-```ruby
-# config/routes.rb
-Example::Application.routes.draw do
- mount ActionCable.server => '/cable'
-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](http://www.rubytutorial.io/actioncable-devise-authentication).
-
-## Dependencies
-
-Action Cable provides a subscription adapter interface to process its pubsub internals. By default, asynchronous, inline, PostgreSQL, evented Redis, and non-evented Redis adapters are included. The default adapter in new Rails applications is the asynchronous (`async`) adapter. 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](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking)
-to take over control of connections from the application server. Action Cable
-then manages connections internally, in a multithreaded manner, regardless of
-whether the application server is multi-threaded or not. So Action Cable works
-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.
-
-## License
-
-Action Cable is released under the MIT license:
-
-* http://www.opensource.org/licenses/MIT
-
+You can read more about Action Cable in the [Action Cable Overview](https://edgeguides.rubyonrails.org/action_cable_overview.html) guide.
## Support
API documentation is at:
-* http://api.rubyonrails.org
+* https://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/actioncable/Rakefile b/actioncable/Rakefile
index 5ba7b7f7f6..121037844d 100644
--- a/actioncable/Rakefile
+++ b/actioncable/Rakefile
@@ -1,19 +1,18 @@
-require 'rake/testtask'
-require 'pathname'
-require 'sprockets'
-require 'coffee-script'
-require 'action_cable'
+# frozen_string_literal: true
-dir = File.dirname(__FILE__)
+require "base64"
+require "rake/testtask"
+require "pathname"
+require "open3"
+require "action_cable"
-task :default => :test
+task default: :test
-task :package => "assets:compile"
-task "package:clean" => "assets:clean"
+task :package
Rake::TestTask.new do |t|
t.libs << "test"
- t.test_files = Dir.glob("#{dir}/test/**/*_test.rb")
+ t.test_files = Dir.glob("#{__dir__}/test/**/*_test.rb")
t.warning = true
t.verbose = true
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
@@ -22,44 +21,24 @@ 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
+
+ task :integration do
+ system(Hash[*Base64.decode64(ENV.fetch("ENCODED", "")).split(/[ =]/)], "yarn", "test")
+ exit($?.exitstatus) unless $?.success?
end
end
namespace :assets do
- root_path = Pathname.new(dir)
- destination_path = root_path.join("lib/assets/compiled")
-
- desc "Compile dist/action_cable.js"
- task :compile do
- puts 'Compiling Action Cable assets...'
+ desc "Generate ActionCable::INTERNAL JS module"
+ task :codegen do
+ require "json"
+ require "action_cable"
- precompile_list = %w(action_cable.js)
-
- environment = Sprockets::Environment.new
- environment.gzip = false
- Pathname.glob(root_path.join("app/assets/*/")) do |subdir|
- environment.append_path subdir
+ 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
-
- compile_path = root_path.join("tmp/sprockets")
- compile_path.rmtree if compile_path.exist?
- compile_path.mkpath
-
- manifest = Sprockets::Manifest.new(environment.index, compile_path)
- manifest.compile(precompile_list)
-
- destination_path.rmtree if destination_path.exist?
- manifest.assets.each do |path, fingerprint_path|
- destination_path.join(path).dirname.mkpath
- FileUtils.cp(compile_path.join(fingerprint_path), destination_path.join(path))
- end
-
- puts 'Done'
- end
-
- task :clean do
- destination_path.rmtree if destination_path.exist?
end
end
diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec
index c65ff7871f..7aefb67c51 100644
--- a/actioncable/actioncable.gemspec
+++ b/actioncable/actioncable.gemspec
@@ -1,25 +1,35 @@
-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 = 'actioncable'
+ 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.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.5.0"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = ["Pratik Naik", "David Heinemeier Hansson"]
+ s.email = ["pratiknaik@gmail.com", "david@loudthinking.com"]
+ s.homepage = "https://rubyonrails.org"
- s.license = 'MIT'
+ s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/assets/javascripts/action_cable.js"]
+ s.require_path = "lib"
- s.author = ['Pratik Naik', 'David Heinemeier Hansson']
- s.email = ['pratiknaik@gmail.com', 'david@loudthinking.com']
- s.homepage = 'http://rubyonrails.org'
+ 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"
+ }
- 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 'actionpack', version
+ s.add_dependency "actionpack", version
- s.add_dependency 'nio4r', '~> 1.2'
- s.add_dependency 'websocket-driver', '~> 0.6.1'
+ s.add_dependency "nio4r", "~> 2.0"
+ s.add_dependency "websocket-driver", ">= 0.6.1"
end
diff --git a/actioncable/app/assets/javascripts/action_cable.coffee.erb b/actioncable/app/assets/javascripts/action_cable.coffee.erb
deleted file mode 100644
index f0422d9d9c..0000000000
--- a/actioncable/app/assets/javascripts/action_cable.coffee.erb
+++ /dev/null
@@ -1,35 +0,0 @@
-#= require_self
-#= require ./action_cable/consumer
-
-@ActionCable =
- INTERNAL: <%= ActionCable::INTERNAL.to_json %>
-
- createConsumer: (url) ->
- url ?= @getConfig("url") ? @INTERNAL.default_mount_path
- new ActionCable.Consumer @createWebSocketURL(url)
-
- getConfig: (name) ->
- element = document.head.querySelector("meta[name='action-cable-#{name}']")
- element?.getAttribute("content")
-
- createWebSocketURL: (url) ->
- if url and not /^wss?:/i.test(url)
- 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")
- a.href
- else
- url
-
- startDebugging: ->
- @debugging = true
-
- stopDebugging: ->
- @debugging = null
-
- log: (messages...) ->
- if @debugging
- messages.push(Date.now())
- console.log("[ActionCable]", messages...)
diff --git a/actioncable/app/assets/javascripts/action_cable.js b/actioncable/app/assets/javascripts/action_cable.js
new file mode 100644
index 0000000000..8349361405
--- /dev/null
+++ b/actioncable/app/assets/javascripts/action_cable.js
@@ -0,0 +1,517 @@
+(function(global, factory) {
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : factory(global.ActionCable = {});
+})(this, function(exports) {
+ "use strict";
+ var adapters = {
+ logger: self.console,
+ WebSocket: self.WebSocket
+ };
+ var logger = {
+ log: function log() {
+ if (this.enabled) {
+ var _adapters$logger;
+ for (var _len = arguments.length, messages = Array(_len), _key = 0; _key < _len; _key++) {
+ messages[_key] = arguments[_key];
+ }
+ messages.push(Date.now());
+ (_adapters$logger = adapters.logger).log.apply(_adapters$logger, [ "[ActionCable]" ].concat(messages));
+ }
+ }
+ };
+ 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 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 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();
+ addEventListener("visibilitychange", this.visibilityDidChange);
+ logger.log("ConnectionMonitor started. pollInterval = " + this.getPollInterval() + " ms");
+ }
+ };
+ ConnectionMonitor.prototype.stop = function stop() {
+ if (this.isRunning()) {
+ this.stoppedAt = now();
+ this.stopPolling();
+ removeEventListener("visibilitychange", this.visibilityDidChange);
+ logger.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;
+ logger.log("ConnectionMonitor recorded connect");
+ };
+ ConnectionMonitor.prototype.recordDisconnect = function recordDisconnect() {
+ this.disconnectedAt = now();
+ logger.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()) {
+ logger.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()) {
+ logger.log("ConnectionMonitor skipping reopening recent disconnect");
+ } else {
+ logger.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()) {
+ logger.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 INTERNAL = {
+ message_types: {
+ welcome: "welcome",
+ disconnect: "disconnect",
+ ping: "ping",
+ confirmation: "confirm_subscription",
+ rejection: "reject_subscription"
+ },
+ disconnect_reasons: {
+ unauthorized: "unauthorized",
+ invalid_request: "invalid_request",
+ server_restart: "server_restart"
+ },
+ default_mount_path: "/cable",
+ protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
+ };
+ 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 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()) {
+ logger.log("Attempted to open WebSocket, but existing socket is " + this.getState());
+ return false;
+ } else {
+ logger.log("Opening WebSocket, current state is " + this.getState() + ", subprotocols: " + protocols);
+ if (this.webSocket) {
+ this.uninstallEventHandlers();
+ }
+ this.webSocket = new adapters.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.close();
+ }
+ };
+ Connection.prototype.reopen = function reopen() {
+ logger.log("Reopening WebSocket, current state is " + this.getState());
+ if (this.isActive()) {
+ try {
+ return this.close();
+ } catch (error) {
+ logger.log("Failed to reopen WebSocket", error);
+ } finally {
+ logger.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms");
+ setTimeout(this.open, this.constructor.reopenDelay);
+ }
+ } else {
+ return this.open();
+ }
+ };
+ Connection.prototype.getProtocol = function getProtocol() {
+ if (this.webSocket) {
+ return this.webSocket.protocol;
+ }
+ };
+ 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 adapters.WebSocket) {
+ if (adapters.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, reason = _JSON$parse.reason, reconnect = _JSON$parse.reconnect, type = _JSON$parse.type;
+ switch (type) {
+ case message_types.welcome:
+ this.monitor.recordConnect();
+ return this.subscriptions.reload();
+
+ case message_types.disconnect:
+ logger.log("Disconnecting. Reason: " + reason);
+ return this.close({
+ allowReconnect: reconnect
+ });
+
+ 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() {
+ logger.log("WebSocket onopen event, using '" + this.getProtocol() + "' subprotocol");
+ this.disconnected = false;
+ if (!this.isProtocolSupported()) {
+ logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
+ return this.close({
+ allowReconnect: false
+ });
+ }
+ },
+ close: function close(event) {
+ logger.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() {
+ logger.log("WebSocket onerror event");
+ }
+ };
+ 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 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 Consumer = function() {
+ function Consumer(url) {
+ classCallCheck(this, Consumer);
+ this._url = url;
+ this.subscriptions = new Subscriptions(this);
+ this.connection = new 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();
+ }
+ };
+ createClass(Consumer, [ {
+ key: "url",
+ get: function get$$1() {
+ return createWebSocketURL(this._url);
+ }
+ } ]);
+ return Consumer;
+ }();
+ function createWebSocketURL(url) {
+ if (typeof url === "function") {
+ url = 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;
+ }
+ }
+ function createConsumer() {
+ var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getConfig("url") || INTERNAL.default_mount_path;
+ return new Consumer(url);
+ }
+ function getConfig(name) {
+ var element = document.head.querySelector("meta[name='action-cable-" + name + "']");
+ if (element) {
+ return element.getAttribute("content");
+ }
+ }
+ exports.Connection = Connection;
+ exports.ConnectionMonitor = ConnectionMonitor;
+ exports.Consumer = Consumer;
+ exports.INTERNAL = INTERNAL;
+ exports.Subscription = Subscription;
+ exports.Subscriptions = Subscriptions;
+ exports.adapters = adapters;
+ exports.createWebSocketURL = createWebSocketURL;
+ exports.logger = logger;
+ exports.createConsumer = createConsumer;
+ exports.getConfig = getConfig;
+ Object.defineProperty(exports, "__esModule", {
+ value: true
+ });
+});
diff --git a/actioncable/app/assets/javascripts/action_cable/connection.coffee b/actioncable/app/assets/javascripts/action_cable/connection.coffee
deleted file mode 100644
index 3a139acf3a..0000000000
--- a/actioncable/app/assets/javascripts/action_cable/connection.coffee
+++ /dev/null
@@ -1,107 +0,0 @@
-#= require ./connection_monitor
-
-# Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation.
-
-{message_types} = ActionCable.INTERNAL
-
-class ActionCable.Connection
- @reopenDelay: 500
-
- constructor: (@consumer) ->
- {@subscriptions} = @consumer
- @monitor = new ActionCable.ConnectionMonitor this
-
- send: (data) ->
- if @isOpen()
- @webSocket.send(JSON.stringify(data))
- true
- else
- false
-
- open: =>
- if @isActive()
- ActionCable.log("Attempted to open WebSocket, but existing socket is #{@getState()}")
- throw new Error("Existing connection must be closed before opening")
- else
- ActionCable.log("Opening WebSocket, current state is #{@getState()}")
- @uninstallEventHandlers() if @webSocket?
- @webSocket = new WebSocket(@consumer.url)
- @installEventHandlers()
- @monitor.start()
- true
-
- close: ->
- @webSocket?.close()
-
- reopen: ->
- ActionCable.log("Reopening WebSocket, current state is #{@getState()}")
- if @isActive()
- try
- @close()
- catch error
- ActionCable.log("Failed to reopen WebSocket", error)
- finally
- ActionCable.log("Reopening WebSocket in #{@constructor.reopenDelay}ms")
- setTimeout(@open, @constructor.reopenDelay)
- else
- @open()
-
- isOpen: ->
- @isState("open")
-
- isActive: ->
- @isState("open", "connecting")
-
- # Private
-
- isState: (states...) ->
- @getState() in states
-
- getState: ->
- return state.toLowerCase() for state, value of WebSocket when value is @webSocket?.readyState
- null
-
- installEventHandlers: ->
- for eventName of @events
- handler = @events[eventName].bind(this)
- @webSocket["on#{eventName}"] = handler
- return
-
- uninstallEventHandlers: ->
- for eventName of @events
- @webSocket["on#{eventName}"] = ->
- return
-
- events:
- message: (event) ->
- {identifier, message, type} = JSON.parse(event.data)
- switch type
- when message_types.welcome
- @monitor.recordConnect()
- when message_types.ping
- @monitor.recordPing()
- when message_types.confirmation
- @subscriptions.notify(identifier, "connected")
- when message_types.rejection
- @subscriptions.reject(identifier)
- else
- @subscriptions.notify(identifier, "received", message)
-
- open: ->
- ActionCable.log("WebSocket onopen event")
- @disconnected = false
- @subscriptions.reload()
-
- close: ->
- ActionCable.log("WebSocket onclose event")
- @disconnect()
-
- error: ->
- ActionCable.log("WebSocket onerror event")
- @disconnect()
-
- disconnect: ->
- return if @disconnected
- @disconnected = true
- @subscriptions.notifyAll("disconnected")
- @monitor.recordDisconnect()
diff --git a/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee b/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee
deleted file mode 100644
index 0cc675fa94..0000000000
--- a/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee
+++ /dev/null
@@ -1,95 +0,0 @@
-# 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.
-class ActionCable.ConnectionMonitor
- @pollInterval:
- min: 3
- max: 30
-
- @staleThreshold: 6 # Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
-
- constructor: (@connection) ->
- @reconnectAttempts = 0
-
- start: ->
- unless @isRunning()
- @startedAt = now()
- delete @stoppedAt
- @startPolling()
- document.addEventListener("visibilitychange", @visibilityDidChange)
- ActionCable.log("ConnectionMonitor started. pollInterval = #{@getPollInterval()} ms")
-
- stop: ->
- if @isRunning()
- @stoppedAt = now()
- @stopPolling()
- document.removeEventListener("visibilitychange", @visibilityDidChange)
- ActionCable.log("ConnectionMonitor stopped")
-
- isRunning: ->
- @startedAt? and not @stoppedAt?
-
- recordPing: ->
- @pingedAt = now()
-
- recordConnect: ->
- @reconnectAttempts = 0
- @recordPing()
- delete @disconnectedAt
- ActionCable.log("ConnectionMonitor recorded connect")
-
- recordDisconnect: ->
- @disconnectedAt = now()
- ActionCable.log("ConnectionMonitor recorded disconnect")
-
- # Private
-
- startPolling: ->
- @stopPolling()
- @poll()
-
- stopPolling: ->
- clearTimeout(@pollTimeout)
-
- poll: ->
- @pollTimeout = setTimeout =>
- @reconnectIfStale()
- @poll()
- , @getPollInterval()
-
- getPollInterval: ->
- {min, max} = @constructor.pollInterval
- interval = 5 * Math.log(@reconnectAttempts + 1)
- Math.round(clamp(interval, min, max) * 1000)
-
- reconnectIfStale: ->
- if @connectionIsStale()
- ActionCable.log("ConnectionMonitor detected stale connection. reconnectAttempts = #{@reconnectAttempts}, pollInterval = #{@getPollInterval()} ms, time disconnected = #{secondsSince(@disconnectedAt)} s, stale threshold = #{@constructor.staleThreshold} s")
- @reconnectAttempts++
- if @disconnectedRecently()
- ActionCable.log("ConnectionMonitor skipping reopening recent disconnect")
- else
- ActionCable.log("ConnectionMonitor reopening")
- @connection.reopen()
-
- connectionIsStale: ->
- secondsSince(@pingedAt ? @startedAt) > @constructor.staleThreshold
-
- disconnectedRecently: ->
- @disconnectedAt and secondsSince(@disconnectedAt) < @constructor.staleThreshold
-
- visibilityDidChange: =>
- if document.visibilityState is "visible"
- setTimeout =>
- if @connectionIsStale() or not @connection.isOpen()
- ActionCable.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = #{document.visibilityState}")
- @connection.reopen()
- , 200
-
- now = ->
- new Date().getTime()
-
- secondsSince = (time) ->
- (now() - time) / 1000
-
- clamp = (number, min, max) ->
- Math.max(min, Math.min(max, number))
diff --git a/actioncable/app/assets/javascripts/action_cable/consumer.coffee b/actioncable/app/assets/javascripts/action_cable/consumer.coffee
deleted file mode 100644
index 7aae1ed8ed..0000000000
--- a/actioncable/app/assets/javascripts/action_cable/consumer.coffee
+++ /dev/null
@@ -1,27 +0,0 @@
-#= require ./connection
-#= require ./subscriptions
-#= require ./subscription
-
-# 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.
-class ActionCable.Consumer
- constructor: (@url) ->
- @subscriptions = new ActionCable.Subscriptions this
- @connection = new ActionCable.Connection this
-
- send: (data) ->
- @connection.send(data)
-
- ensureActiveConnection: ->
- unless @connection.isActive()
- @connection.open()
diff --git a/actioncable/app/assets/javascripts/action_cable/subscription.coffee b/actioncable/app/assets/javascripts/action_cable/subscription.coffee
deleted file mode 100644
index 61a3fb1309..0000000000
--- a/actioncable/app/assets/javascripts/action_cable/subscription.coffee
+++ /dev/null
@@ -1,66 +0,0 @@
-# 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
-#
-# appear: ->
-# @perform 'appear', appearing_on: @appearingOn()
-#
-# away: ->
-# @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.
-class ActionCable.Subscription
- constructor: (@consumer, params = {}, mixin) ->
- @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
- @send(data)
-
- send: (data) ->
- @consumer.send(command: "message", identifier: @identifier, data: JSON.stringify(data))
-
- unsubscribe: ->
- @consumer.subscriptions.remove(this)
-
- extend = (object, properties) ->
- if properties?
- for key, value of properties
- object[key] = value
- object
diff --git a/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee b/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee
deleted file mode 100644
index aa052bf5d8..0000000000
--- a/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee
+++ /dev/null
@@ -1,66 +0,0 @@
-# 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.
-class ActionCable.Subscriptions
- constructor: (@consumer) ->
- @subscriptions = []
-
- create: (channelName, mixin) ->
- channel = channelName
- params = if typeof channel is "object" then channel else {channel}
- subscription = new ActionCable.Subscription @consumer, params, mixin
- @add(subscription)
-
- # Private
-
- add: (subscription) ->
- @subscriptions.push(subscription)
- @consumer.ensureActiveConnection()
- @notify(subscription, "initialized")
- @sendCommand(subscription, "subscribe")
- subscription
-
- remove: (subscription) ->
- @forget(subscription)
- unless @findAll(subscription.identifier).length
- @sendCommand(subscription, "unsubscribe")
- subscription
-
- reject: (identifier) ->
- for subscription in @findAll(identifier)
- @forget(subscription)
- @notify(subscription, "rejected")
- subscription
-
- forget: (subscription) ->
- @subscriptions = (s for s in @subscriptions when s isnt subscription)
- subscription
-
- findAll: (identifier) ->
- s for s in @subscriptions when s.identifier is identifier
-
- reload: ->
- for subscription in @subscriptions
- @sendCommand(subscription, "subscribe")
-
- notifyAll: (callbackName, args...) ->
- for subscription in @subscriptions
- @notify(subscription, callbackName, args...)
-
- notify: (subscription, callbackName, args...) ->
- if typeof subscription is "string"
- subscriptions = @findAll(subscription)
- else
- subscriptions = [subscription]
-
- for subscription in subscriptions
- subscription[callbackName]?(args...)
-
- sendCommand: (subscription, command) ->
- {identifier} = subscription
- @consumer.send({command, identifier})
diff --git a/actioncable/app/javascript/action_cable/adapters.js b/actioncable/app/javascript/action_cable/adapters.js
new file mode 100644
index 0000000000..4de8131438
--- /dev/null
+++ b/actioncable/app/javascript/action_cable/adapters.js
@@ -0,0 +1,4 @@
+export default {
+ logger: self.console,
+ WebSocket: self.WebSocket
+}
diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js
new file mode 100644
index 0000000000..96bac132c1
--- /dev/null
+++ b/actioncable/app/javascript/action_cable/connection.js
@@ -0,0 +1,165 @@
+import adapters from "./adapters"
+import ConnectionMonitor from "./connection_monitor"
+import INTERNAL from "./internal"
+import logger from "./logger"
+
+// 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 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()) {
+ logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`)
+ return false
+ } else {
+ logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`)
+ if (this.webSocket) { this.uninstallEventHandlers() }
+ this.webSocket = new adapters.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.close()
+ }
+ }
+
+ reopen() {
+ logger.log(`Reopening WebSocket, current state is ${this.getState()}`)
+ if (this.isActive()) {
+ try {
+ return this.close()
+ } catch (error) {
+ logger.log("Failed to reopen WebSocket", error)
+ }
+ finally {
+ logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`)
+ setTimeout(this.open, this.constructor.reopenDelay)
+ }
+ } else {
+ return this.open()
+ }
+ }
+
+ getProtocol() {
+ if (this.webSocket) {
+ return this.webSocket.protocol
+ }
+ }
+
+ 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 adapters.WebSocket) {
+ if (adapters.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, reason, reconnect, type} = JSON.parse(event.data)
+ switch (type) {
+ case message_types.welcome:
+ this.monitor.recordConnect()
+ return this.subscriptions.reload()
+ case message_types.disconnect:
+ logger.log(`Disconnecting. Reason: ${reason}`)
+ return this.close({allowReconnect: reconnect})
+ 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() {
+ logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`)
+ this.disconnected = false
+ if (!this.isProtocolSupported()) {
+ logger.log("Protocol is unsupported. Stopping monitor and disconnecting.")
+ return this.close({allowReconnect: false})
+ }
+ },
+
+ close(event) {
+ logger.log("WebSocket onclose event")
+ if (this.disconnected) { return }
+ this.disconnected = true
+ this.monitor.recordDisconnect()
+ return this.subscriptions.notifyAll("disconnected", {willAttemptReconnect: this.monitor.isRunning()})
+ },
+
+ error() {
+ logger.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..312a71d154
--- /dev/null
+++ b/actioncable/app/javascript/action_cable/connection_monitor.js
@@ -0,0 +1,126 @@
+import logger from "./logger"
+
+// 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()
+ addEventListener("visibilitychange", this.visibilityDidChange)
+ logger.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`)
+ }
+ }
+
+ stop() {
+ if (this.isRunning()) {
+ this.stoppedAt = now()
+ this.stopPolling()
+ removeEventListener("visibilitychange", this.visibilityDidChange)
+ logger.log("ConnectionMonitor stopped")
+ }
+ }
+
+ isRunning() {
+ return this.startedAt && !this.stoppedAt
+ }
+
+ recordPing() {
+ this.pingedAt = now()
+ }
+
+ recordConnect() {
+ this.reconnectAttempts = 0
+ this.recordPing()
+ delete this.disconnectedAt
+ logger.log("ConnectionMonitor recorded connect")
+ }
+
+ recordDisconnect() {
+ this.disconnectedAt = now()
+ logger.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()) {
+ logger.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()) {
+ logger.log("ConnectionMonitor skipping reopening recent disconnect")
+ } else {
+ logger.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()) {
+ logger.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..e2e0dea8b5
--- /dev/null
+++ b/actioncable/app/javascript/action_cable/consumer.js
@@ -0,0 +1,75 @@
+import Connection from "./connection"
+import Subscriptions from "./subscriptions"
+
+// 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 Subscriptions(this)
+ this.connection = new Connection(this)
+ }
+
+ get url() {
+ return createWebSocketURL(this._url)
+ }
+
+ 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()
+ }
+ }
+}
+
+export function createWebSocketURL(url) {
+ if (typeof url === "function") {
+ url = 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
+ }
+}
diff --git a/actioncable/app/javascript/action_cable/index.js b/actioncable/app/javascript/action_cable/index.js
new file mode 100644
index 0000000000..848b5631d6
--- /dev/null
+++ b/actioncable/app/javascript/action_cable/index.js
@@ -0,0 +1,31 @@
+import Connection from "./connection"
+import ConnectionMonitor from "./connection_monitor"
+import Consumer, { createWebSocketURL } from "./consumer"
+import INTERNAL from "./internal"
+import Subscription from "./subscription"
+import Subscriptions from "./subscriptions"
+import adapters from "./adapters"
+import logger from "./logger"
+
+export {
+ Connection,
+ ConnectionMonitor,
+ Consumer,
+ INTERNAL,
+ Subscription,
+ Subscriptions,
+ adapters,
+ createWebSocketURL,
+ logger,
+}
+
+export function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
+ return new Consumer(url)
+}
+
+export function getConfig(name) {
+ const element = document.head.querySelector(`meta[name='action-cable-${name}']`)
+ if (element) {
+ return element.getAttribute("content")
+ }
+}
diff --git a/actioncable/app/javascript/action_cable/logger.js b/actioncable/app/javascript/action_cable/logger.js
new file mode 100644
index 0000000000..ef4661ead1
--- /dev/null
+++ b/actioncable/app/javascript/action_cable/logger.js
@@ -0,0 +1,10 @@
+import adapters from "./adapters"
+
+export default {
+ log(...messages) {
+ if (this.enabled) {
+ messages.push(Date.now())
+ adapters.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..867cafb407
--- /dev/null
+++ b/actioncable/app/javascript/action_cable/subscriptions.js
@@ -0,0 +1,86 @@
+import Subscription from "./subscription"
+
+// 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 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/karma.conf.js b/actioncable/karma.conf.js
new file mode 100644
index 0000000000..845b38d74f
--- /dev/null
+++ b/actioncable/karma.conf.js
@@ -0,0 +1,64 @@
+const config = {
+ browsers: ["ChromeHeadless"],
+ frameworks: ["qunit"],
+ files: [
+ "test/javascript/compiled/test.js",
+ ],
+
+ client: {
+ clearContext: false,
+ qunit: {
+ showUI: true
+ }
+ },
+
+ singleRun: true,
+ autoWatch: false,
+
+ captureTimeout: 180000,
+ browserDisconnectTimeout: 180000,
+ browserDisconnectTolerance: 3,
+ browserNoActivityTimeout: 300000,
+}
+
+if (process.env.CI) {
+ config.customLaunchers = {
+ sl_chrome: sauce("chrome", 70),
+ sl_ff: sauce("firefox", 63),
+ sl_safari: sauce("safari", 12.0, "macOS 10.13"),
+ sl_edge: sauce("microsoftedge", 17.17134, "Windows 10"),
+ sl_ie_11: sauce("internet explorer", 11, "Windows 8.1"),
+ }
+
+ config.browsers = Object.keys(config.customLaunchers)
+ config.reporters = ["dots", "saucelabs"]
+
+ config.sauceLabs = {
+ testName: "ActionCable JS Client",
+ retryLimit: 3,
+ build: buildId(),
+ }
+
+ function sauce(browserName, version, platform) {
+ const options = {
+ base: "SauceLabs",
+ browserName: browserName.toString(),
+ version: version.toString(),
+ }
+ if (platform) {
+ options.platform = platform.toString()
+ }
+ return options
+ }
+
+ function buildId() {
+ const { TRAVIS_BUILD_NUMBER, TRAVIS_BUILD_ID } = process.env
+ return TRAVIS_BUILD_NUMBER && TRAVIS_BUILD_ID
+ ? `TRAVIS #${TRAVIS_BUILD_NUMBER} (${TRAVIS_BUILD_ID})`
+ : ""
+ }
+}
+
+module.exports = function(karmaConfig) {
+ karmaConfig.set(config)
+}
diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb
index 68a5fff3e7..ad5fd43155 100644
--- a/actioncable/lib/action_cable.rb
+++ b/actioncable/lib/action_cable.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2015-2016 Basecamp, LLC
+# Copyright (c) 2015-2019 Basecamp, LLC
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -21,21 +23,28 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
-require 'active_support'
-require 'active_support/rails'
-require 'action_cable/version'
+require "active_support"
+require "active_support/rails"
+require "action_cable/version"
module ActionCable
extend ActiveSupport::Autoload
INTERNAL = {
message_types: {
- welcome: 'welcome'.freeze,
- ping: 'ping'.freeze,
- confirmation: 'confirm_subscription'.freeze,
- rejection: 'reject_subscription'.freeze
+ welcome: "welcome",
+ disconnect: "disconnect",
+ ping: "ping",
+ confirmation: "confirm_subscription",
+ rejection: "reject_subscription"
+ },
+ disconnect_reasons: {
+ unauthorized: "unauthorized",
+ invalid_request: "invalid_request",
+ server_restart: "server_restart"
},
- default_mount_path: '/cable'.freeze
+ default_mount_path: "/cable",
+ protocols: ["actioncable-v1-json", "actioncable-unsupported"].freeze
}
# Singleton instance of the server
@@ -48,4 +57,6 @@ module ActionCable
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
index 7ae262ce5f..d5118b9dc9 100644
--- a/actioncable/lib/action_cable/channel.rb
+++ b/actioncable/lib/action_cable/channel.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionCable
module Channel
extend ActiveSupport::Autoload
@@ -9,6 +11,7 @@ module ActionCable
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
index 845b747fc5..af061c843a 100644
--- a/actioncable/lib/action_cable/channel/base.rb
+++ b/actioncable/lib/action_cable/channel/base.rb
@@ -1,4 +1,7 @@
-require 'set'
+# frozen_string_literal: true
+
+require "set"
+require "active_support/rescuable"
module ActionCable
module Channel
@@ -97,6 +100,7 @@ module ActionCable
include Streams
include Naming
include Broadcasting
+ include ActiveSupport::Rescuable
attr_reader :params, :connection, :identifier
delegate :logger, to: :connection
@@ -122,16 +126,16 @@ module ActionCable
end
end
- protected
+ private
# action_methods are cached and there is sometimes need to refresh
# them. ::clear_action_methods! allows you to do that, so next time
# you run action_methods, they will be recalculated.
- def clear_action_methods!
+ def clear_action_methods! # :doc:
@action_methods = nil
end
# Refresh the cached action_methods when a new action_method is added.
- def method_added(name)
+ def method_added(name) # :doc:
super
clear_action_methods!
end
@@ -144,13 +148,14 @@ module ActionCable
# When a channel is streaming via pubsub, we want to delay the confirmation
# transmission until pubsub subscription is confirmed.
- @defer_subscription_confirmation = false
+ #
+ # 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
- subscribe_to_channel
end
# Extract the action name from the passed data and process it via the channel. The process will ensure
@@ -169,6 +174,17 @@ module ActionCable
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:
@@ -177,24 +193,25 @@ module ActionCable
end
end
-
- protected
+ private
# Called once a consumer has become a subscriber of the channel. Usually the place to setup any streams
# you want this channel to be sending to the subscriber.
- def subscribed
+ def subscribed # :doc:
# Override in subclasses
end
# Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking
# users as offline or the like.
- def unsubscribed
+ def unsubscribed # :doc:
# Override in subclasses
end
# Transmit a hash of data to the subscriber. The hash will automatically be wrapped in a JSON envelope with
# the proper channel identifier marked as the recipient.
- def transmit(data, via: nil)
- logger.info "#{self.class.name} transmitting #{data.inspect.truncate(300)}".tap { |m| m << " (via #{via})" if via }
+ def transmit(data, via: nil) # :doc:
+ 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
@@ -202,27 +219,32 @@ module ActionCable
end
end
- def defer_subscription_confirmation!
- @defer_subscription_confirmation = true
+ 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?
- @defer_subscription_confirmation
+ def defer_subscription_confirmation? # :doc:
+ @defer_subscription_confirmation_counter.value > 0
end
- def subscription_confirmation_sent?
+ def subscription_confirmation_sent? # :doc:
@subscription_confirmation_sent
end
- def reject
+ def reject # :doc:
@reject_subscription = true
end
- def subscription_rejected?
+ def subscription_rejected? # :doc:
@reject_subscription
end
- private
def delegate_connection_identifiers
connection.identifiers.each do |identifier|
define_singleton_method(identifier) do
@@ -231,24 +253,12 @@ module ActionCable
end
end
- def subscribe_to_channel
- run_callbacks :subscribe do
- subscribed
- end
-
- if subscription_rejected?
- reject_subscription
- else
- transmit_subscription_confirmation unless defer_subscription_confirmation?
- end
- end
-
def extract_action(data)
- (data['action'].presence || :receive).to_sym
+ (data["action"].presence || :receive).to_sym
end
def processable_action?(action)
- self.class.action_methods.include?(action.to_s)
+ self.class.action_methods.include?(action.to_s) unless subscription_rejected?
end
def dispatch_action(action, data)
@@ -259,11 +269,13 @@ module ActionCable
else
public_send action
end
+ rescue Exception => exception
+ rescue_with_handler(exception) || raise
end
def action_signature(action, data)
- "#{self.class.name}##{action}".tap do |signature|
- if (arguments = data.except('action')).any?
+ (+"#{self.class.name}##{action}").tap do |signature|
+ if (arguments = data.except("action")).any?
signature << "(#{arguments.inspect})"
end
end
@@ -295,3 +307,5 @@ module ActionCable
end
end
end
+
+ActiveSupport.run_load_hooks(:action_cable_channel, ActionCable::Channel::Base)
diff --git a/actioncable/lib/action_cable/channel/broadcasting.rb b/actioncable/lib/action_cable/channel/broadcasting.rb
index afc23d7d1a..9f702e425e 100644
--- a/actioncable/lib/action_cable/channel/broadcasting.rb
+++ b/actioncable/lib/action_cable/channel/broadcasting.rb
@@ -1,26 +1,38 @@
-require 'active_support/core_ext/object/to_param'
+# 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
+ delegate :broadcasting_for, :broadcast_to, to: :class
- class_methods do
+ module ClassMethods
# Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel.
def broadcast_to(model, message)
- ActionCable.server.broadcast(broadcasting_for([ channel_name, model ]), message)
+ ActionCable.server.broadcast(broadcasting_for(model), message)
+ end
+
+ # Returns a unique broadcasting identifier for this <tt>model</tt> in this channel:
+ #
+ # CommentsChannel.broadcasting_for("all") # => "comments:all"
+ #
+ # You can pass any object as a target (e.g. Active Record model), and it
+ # would be serialized into a string under the hood.
+ def broadcasting_for(model)
+ serialize_broadcasting([ channel_name, model ])
end
- def broadcasting_for(model) #:nodoc:
+ def serialize_broadcasting(object) #:nodoc:
case
- when model.is_a?(Array)
- model.map { |m| broadcasting_for(m) }.join(':')
- when model.respond_to?(:to_gid_param)
- model.to_gid_param
+ when object.is_a?(Array)
+ object.map { |m| serialize_broadcasting(m) }.join(":")
+ when object.respond_to?(:to_gid_param)
+ object.to_gid_param
else
- model.to_param
+ object.to_param
end
end
end
diff --git a/actioncable/lib/action_cable/channel/callbacks.rb b/actioncable/lib/action_cable/channel/callbacks.rb
index 295d750e86..e4cb19b26a 100644
--- a/actioncable/lib/action_cable/channel/callbacks.rb
+++ b/actioncable/lib/action_cable/channel/callbacks.rb
@@ -1,4 +1,6 @@
-require 'active_support/callbacks'
+# frozen_string_literal: true
+
+require "active_support/callbacks"
module ActionCable
module Channel
@@ -11,7 +13,7 @@ module ActionCable
define_callbacks :unsubscribe
end
- class_methods do
+ module ClassMethods
def before_subscribe(*methods, &block)
set_callback(:subscribe, :before, *methods, &block)
end
diff --git a/actioncable/lib/action_cable/channel/naming.rb b/actioncable/lib/action_cable/channel/naming.rb
index 4c9d53b15a..9c324a2a53 100644
--- a/actioncable/lib/action_cable/channel/naming.rb
+++ b/actioncable/lib/action_cable/channel/naming.rb
@@ -1,17 +1,20 @@
+# frozen_string_literal: true
+
module ActionCable
module Channel
module Naming
extend ActiveSupport::Concern
- class_methods do
+ module ClassMethods
# Returns the name of the channel, underscored, without the <tt>Channel</tt> ending.
# If the channel is in a namespace, then the namespaces are represented by single
# colon separators in the channel name.
#
# 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
+ @channel_name ||= name.sub(/Channel$/, "").gsub("::", ":").underscore
end
end
diff --git a/actioncable/lib/action_cable/channel/periodic_timers.rb b/actioncable/lib/action_cable/channel/periodic_timers.rb
index b414255707..830b3efa3c 100644
--- a/actioncable/lib/action_cable/channel/periodic_timers.rb
+++ b/actioncable/lib/action_cable/channel/periodic_timers.rb
@@ -1,22 +1,54 @@
+# frozen_string_literal: true
+
module ActionCable
module Channel
module PeriodicTimers
extend ActiveSupport::Concern
included do
- class_attribute :periodic_timers, instance_reader: false
- self.periodic_timers = []
+ class_attribute :periodic_timers, instance_reader: false, default: []
after_subscribe :start_periodic_timers
after_unsubscribe :stop_periodic_timers
end
module ClassMethods
- # Allows you to call a private method <tt>every</tt> so often seconds. This periodic timer can be useful
- # for sending a steady flow of updates to a client based off an object that was configured on subscription.
- # It's an alternative to using streams if the channel is able to do the work internally.
- def periodically(callback, every:)
- self.periodic_timers += [ [ callback, every: every ] ]
+ # Periodically performs a task on the channel, like updating an online
+ # user counter, polling a backend for new status messages, sending
+ # regular "heartbeat" messages, or doing some internal work and giving
+ # progress updates.
+ #
+ # Pass a method name or lambda argument or provide a block to call.
+ # Specify the calling period in seconds using the <tt>every:</tt>
+ # keyword argument.
+ #
+ # periodically :transmit_progress, every: 5.seconds
+ #
+ # periodically every: 3.minutes do
+ # transmit action: :update_count, count: current_count
+ # end
+ #
+ def periodically(callback_or_method_name = nil, every:, &block)
+ callback =
+ if block_given?
+ raise ArgumentError, "Pass a block or provide a callback arg, not both" if callback_or_method_name
+ block
+ else
+ case callback_or_method_name
+ when Proc
+ callback_or_method_name
+ when Symbol
+ -> { __send__ callback_or_method_name }
+ else
+ raise ArgumentError, "Expected a Symbol method name or a Proc, got #{callback_or_method_name.inspect}"
+ end
+ end
+
+ unless every.kind_of?(Numeric) && every > 0
+ raise ArgumentError, "Expected every: to be a positive number of seconds, got #{every.inspect}"
+ end
+
+ self.periodic_timers += [[ callback, every: every ]]
end
end
@@ -27,14 +59,19 @@ module ActionCable
def start_periodic_timers
self.class.periodic_timers.each do |callback, options|
- active_periodic_timers << connection.server.event_loop.timer(options[:every]) do
- connection.worker_pool.async_run_periodic_timer(self, callback)
- end
+ 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
diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb
index f654ce0bfa..7e1ed3c850 100644
--- a/actioncable/lib/action_cable/channel/streams.rb
+++ b/actioncable/lib/action_cable/channel/streams.rb
@@ -1,8 +1,10 @@
+# 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, if you connect after it has been sent.
+ # 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
@@ -19,14 +21,14 @@ module ActionCable
# end
#
# Based on the above example, the subscribers of this channel will get whatever data is put into the,
- # let's say, `comments_for_45` broadcasting as soon as it's put there.
+ # 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 `comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE`
+ # The following example would subscribe to a broadcasting like <tt>comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE</tt>.
#
# class CommentsChannel < ApplicationCable::Channel
# def subscribed
@@ -69,24 +71,22 @@ module ActionCable
# Start streaming from the named <tt>broadcasting</tt> pubsub queue. Optionally, you can pass a <tt>callback</tt> that'll be used
# instead of the default of just transmitting the updates straight to the subscriber.
- # Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback.
- # Defaults to `coder: nil` which does no decoding, passes raw messages.
+ # 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!
- if handler = callback || block
- handler = -> message { handler.(coder.decode(message)) } if coder
- else
- handler = default_stream_handler(broadcasting, coder: coder)
- end
-
+ # 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
- transmit_subscription_confirmation
+ ensure_confirmation_sent
logger.info "#{self.class.name} is streaming from #{broadcasting}"
end)
end
@@ -96,10 +96,10 @@ module ActionCable
# <tt>callback</tt> that'll be used instead of the default of just transmitting the updates straight
# to the subscriber.
#
- # Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback.
- # Defaults to `coder: nil` which does no decoding, passes raw messages.
+ # Pass <tt>coder: ActiveSupport::JSON</tt> to decode messages as JSON before passing to the callback.
+ # Defaults to <tt>coder: nil</tt> which does no decoding, passes raw messages.
def stream_for(model, callback = nil, coder: nil, &block)
- stream_from(broadcasting_for([ channel_name, model ]), callback || block, coder: coder)
+ stream_from(broadcasting_for(model), callback || block, coder: coder)
end
# Unsubscribes all streams associated with this channel from the pubsub queue.
@@ -117,13 +117,60 @@ module ActionCable
@_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 coder.decode(message), via: "streamed from #{broadcasting}"
+ 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..b05d51a61a
--- /dev/null
+++ b/actioncable/lib/action_cable/channel/test_case.rb
@@ -0,0 +1,310 @@
+# 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_has_stream "chat_1"
+ #
+ # # Asserts that the channel subscribes connection to a specific
+ # # stream created for a model
+ # assert_has_stream_for Room.find(1)
+ # end
+ #
+ # def test_does_not_stream_with_incorrect_room_number
+ # subscribe room_number: -1
+ #
+ # # Asserts that not streams was started
+ # assert_no_streams
+ # 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.
+ #
+ #
+ # == 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
+
+ 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
+
+ # Asserts that no streams have been started.
+ #
+ # def test_assert_no_started_stream
+ # subscribe
+ # assert_no_streams
+ # end
+ #
+ def assert_no_streams
+ assert subscription.streams.empty?, "No streams started was expected, but #{subscription.streams.count} found"
+ end
+
+ # Asserts that the specified stream has been started.
+ #
+ # def test_assert_started_stream
+ # subscribe
+ # assert_has_stream 'messages'
+ # end
+ #
+ def assert_has_stream(stream)
+ assert subscription.streams.include?(stream), "Stream #{stream} has not been started"
+ end
+
+ # Asserts that the specified stream for a model has started.
+ #
+ # def test_assert_started_stream_for
+ # subscribe id: 42
+ # assert_has_stream_for User.find(42)
+ # end
+ #
+ def assert_has_stream_for(object)
+ assert_has_stream(broadcasting_for(object))
+ 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(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
index 5f813cf8e0..20b5dbe78d 100644
--- a/actioncable/lib/action_cable/connection.rb
+++ b/actioncable/lib/action_cable/connection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionCable
module Connection
extend ActiveSupport::Autoload
@@ -8,13 +10,12 @@ module ActionCable
autoload :ClientSocket
autoload :Identification
autoload :InternalChannel
- autoload :FayeClientSocket
- autoload :FayeEventLoop
autoload :MessageBuffer
autoload :Stream
autoload :StreamEventLoop
autoload :Subscriptions
autoload :TaggedLoggerProxy
+ autoload :TestCase
autoload :WebSocket
end
end
diff --git a/actioncable/lib/action_cable/connection/authorization.rb b/actioncable/lib/action_cable/connection/authorization.rb
index 070a70e4e2..aef3386f71 100644
--- a/actioncable/lib/action_cable/connection/authorization.rb
+++ b/actioncable/lib/action_cable/connection/authorization.rb
@@ -1,13 +1,15 @@
+# frozen_string_literal: true
+
module ActionCable
module Connection
module Authorization
class UnauthorizedError < StandardError; end
- private
- def reject_unauthorized_connection
- logger.error "An unauthorized connection attempt was rejected"
- raise UnauthorizedError
- end
+ # Closes the WebSocket connection if it is open and returns a 404 "File not Found" response.
+ def reject_unauthorized_connection
+ logger.error "An unauthorized connection attempt was rejected"
+ raise UnauthorizedError
+ end
end
end
-end \ No newline at end of file
+end
diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb
index 604a889bb0..c469f7066c 100644
--- a/actioncable/lib/action_cable/connection/base.rb
+++ b/actioncable/lib/action_cable/connection/base.rb
@@ -1,8 +1,10 @@
-require 'action_dispatch'
+# frozen_string_literal: true
+
+require "action_dispatch"
module ActionCable
module Connection
- # For every WebSocket the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
+ # 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.
@@ -22,13 +24,10 @@ module ActionCable
# # Any cleanup work needed when the cable connection is cut.
# end
#
- # protected
+ # private
# def find_verified_user
- # if current_user = User.find_by_identity cookies.signed[:identity_id]
- # current_user
- # else
+ # User.find_by_identity(cookies.encrypted[:identity_id]) ||
# reject_unauthorized_connection
- # end
# end
# end
# end
@@ -40,7 +39,7 @@ module ActionCable
# Second, we rely on the fact that the WebSocket connection is established with the cookies from the domain being sent along. This makes
# it easy to use signed cookies that were set when logging in via a web interface to authorize the WebSocket connection.
#
- # Finally, we add a tag to the connection-specific logger with name of the current user to easily distinguish their messages in the log.
+ # Finally, we add a tag to the connection-specific logger with the name of the current user to easily distinguish their messages in the log.
#
# Pretty simple, eh?
class Base
@@ -48,7 +47,7 @@ module ActionCable
include InternalChannel
include Authorization
- attr_reader :server, :env, :subscriptions, :logger, :worker_pool
+ attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol
delegate :event_loop, :pubsub, to: :server
def initialize(server, env, coder: ActiveSupport::JSON)
@@ -57,7 +56,7 @@ module ActionCable
@worker_pool = server.worker_pool
@logger = new_tagged_logger
- @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop, server.config.client_socket_class)
+ @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop)
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
@message_buffer = ActionCable::Connection::MessageBuffer.new(self)
@@ -96,7 +95,12 @@ module ActionCable
end
# Close the WebSocket connection.
- def close
+ def close(reason: nil, reconnect: true)
+ transmit(
+ type: ActionCable::INTERNAL[:message_types][:disconnect],
+ reason: reason,
+ reconnect: reconnect
+ )
websocket.close
end
@@ -105,14 +109,14 @@ module ActionCable
worker_pool.async_invoke(self, method, *arguments)
end
- # Return a basic hash of statistics for the connection keyed with `identifier`, `started_at`, and `subscriptions`.
+ # 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']
+ request_id: @env["action_dispatch.request_id"]
}
end
@@ -129,16 +133,20 @@ module ActionCable
end
def on_error(message) # :nodoc:
- # ignore
+ # 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
- protected
+ 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
+ def request # :doc:
@request ||= begin
environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
ActionDispatch::Request.new(environment || env)
@@ -146,14 +154,10 @@ module ActionCable
end
# The cookies of the request that initiated the WebSocket connection. Useful for performing authorization checks.
- def cookies
+ def cookies # :doc:
request.cookie_jar
end
- attr_reader :websocket
- attr_reader :message_buffer
-
- private
def encode(cable_message)
@coder.encode cable_message
end
@@ -163,6 +167,7 @@ module ActionCable
end
def handle_open
+ @protocol = websocket.protocol
connect if respond_to?(:connect)
subscribe_to_internal_channel
send_welcome_message
@@ -170,7 +175,7 @@ module ActionCable
message_buffer.process!
server.add_connection(self)
rescue ActionCable::Connection::Authorization::UnauthorizedError
- respond_to_invalid_request
+ close(reason: ActionCable::INTERNAL[:disconnect_reasons][:unauthorized], reconnect: false) if websocket.alive?
end
def handle_close
@@ -194,7 +199,10 @@ module ActionCable
def allow_request_origin?
return true if server.config.disable_request_forgery_protection
- if Array(server.config.allowed_request_origins).any? { |allowed_origin| allowed_origin === env['HTTP_ORIGIN'] }
+ 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']}")
@@ -208,11 +216,11 @@ module ActionCable
end
def respond_to_invalid_request
- close if websocket.alive?
+ close(reason: ActionCable::INTERNAL[:disconnect_reasons][:invalid_request]) if websocket.alive?
logger.error invalid_request_message
logger.info finished_request_message
- [ 404, { 'Content-Type' => 'text/plain' }, [ 'Page not found' ] ]
+ [ 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.
@@ -225,7 +233,7 @@ module ActionCable
'Started %s "%s"%s for %s at %s' % [
request.request_method,
request.filtered_path,
- websocket.possible? ? ' [WebSocket]' : '[non-WebSocket]',
+ websocket.possible? ? " [WebSocket]" : "[non-WebSocket]",
request.ip,
Time.now.to_s ]
end
@@ -233,22 +241,24 @@ module ActionCable
def finished_request_message
'Finished "%s"%s for %s at %s' % [
request.filtered_path,
- websocket.possible? ? ' [WebSocket]' : '[non-WebSocket]',
+ 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)' % [
+ "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)' % [
+ "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
+
+ActiveSupport.run_load_hooks(:action_cable_connection, ActionCable::Connection::Base)
diff --git a/actioncable/lib/action_cable/connection/client_socket.rb b/actioncable/lib/action_cable/connection/client_socket.rb
index 7d6de78582..4b1964c4ae 100644
--- a/actioncable/lib/action_cable/connection/client_socket.rb
+++ b/actioncable/lib/action_cable/connection/client_socket.rb
@@ -1,4 +1,6 @@
-require 'websocket/driver'
+# frozen_string_literal: true
+
+require "websocket/driver"
module ActionCable
module Connection
@@ -8,18 +10,18 @@ module ActionCable
# Copyright (c) 2010-2015 James Coglan
class ClientSocket # :nodoc:
def self.determine_url(env)
- scheme = secure_request?(env) ? 'wss:' : 'ws:'
+ 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'
+ 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"
- return false
+ false
end
CONNECTING = 0
@@ -29,7 +31,7 @@ module ActionCable
attr_reader :env, :url
- def initialize(env, event_target, event_loop)
+ def initialize(env, event_target, event_loop, protocols)
@env = env
@event_target = event_target
@event_loop = event_loop
@@ -37,12 +39,12 @@ module ActionCable
@url = ClientSocket.determine_url(@env)
@driver = @driver_started = nil
- @close_params = ['', 1006]
+ @close_params = ["", 1006]
@ready_state = CONNECTING
# The driver calls +env+, +url+, and +write+
- @driver = ::WebSocket::Driver.rack(self)
+ @driver = ::WebSocket::Driver.rack(self, protocols: protocols)
@driver.on(:open) { |e| open }
@driver.on(:message) { |e| receive_message(e.data) }
@@ -56,7 +58,7 @@ module ActionCable
return if @driver.nil? || @driver_started
@stream.hijack_rack_socket
- if callback = @env['async.callback']
+ if callback = @env["async.callback"]
callback.call([101, {}, @stream])
end
@@ -78,20 +80,20 @@ module ActionCable
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
+ 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 ||= ''
+ reason ||= ""
- unless code == 1000 or (code >= 3000 and code <= 4999)
- raise ArgumentError, "Failed to execute 'close' on WebSocket: " +
- "The code must be either 1000, or between 3000 and 4999. " +
+ 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
@@ -111,6 +113,10 @@ module ActionCable
@ready_state == OPEN
end
+ def protocol
+ @driver.protocol
+ end
+
private
def open
return unless @ready_state == CONNECTING
diff --git a/actioncable/lib/action_cable/connection/faye_client_socket.rb b/actioncable/lib/action_cable/connection/faye_client_socket.rb
deleted file mode 100644
index 47d09a9e14..0000000000
--- a/actioncable/lib/action_cable/connection/faye_client_socket.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-require 'faye/websocket'
-
-module ActionCable
- module Connection
- class FayeClientSocket
- def initialize(env, event_target, stream_event_loop)
- @env = env
- @event_target = event_target
-
- @faye = nil
- end
-
- def alive?
- @faye && @faye.ready_state == Faye::WebSocket::API::OPEN
- end
-
- def transmit(data)
- connect
- @faye.send data
- end
-
- def close
- @faye && @faye.close
- end
-
- def rack_response
- connect
- @faye.rack_response
- end
-
- private
- def connect
- return if @faye
- @faye = Faye::WebSocket.new(@env)
-
- @faye.on(:open) { |event| @event_target.on_open }
- @faye.on(:message) { |event| @event_target.on_message(event.data) }
- @faye.on(:close) { |event| @event_target.on_close(event.reason, event.code) }
- @faye.on(:error) { |event| @event_target.on_error(event.message) }
- end
- end
- end
-end
diff --git a/actioncable/lib/action_cable/connection/faye_event_loop.rb b/actioncable/lib/action_cable/connection/faye_event_loop.rb
deleted file mode 100644
index 9c44b38bc3..0000000000
--- a/actioncable/lib/action_cable/connection/faye_event_loop.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-require 'thread'
-
-require 'eventmachine'
-EventMachine.epoll if EventMachine.epoll?
-EventMachine.kqueue if EventMachine.kqueue?
-
-module ActionCable
- module Connection
- class FayeEventLoop
- @@mutex = Mutex.new
-
- def timer(interval, &block)
- ensure_reactor_running
- EMTimer.new(::EM::PeriodicTimer.new(interval, &block))
- end
-
- def post(task = nil, &block)
- task ||= block
-
- ensure_reactor_running
- ::EM.next_tick(&task)
- end
-
- private
- def ensure_reactor_running
- return if EventMachine.reactor_running?
- @@mutex.synchronize do
- Thread.new { EventMachine.run } unless EventMachine.reactor_running?
- Thread.pass until EventMachine.reactor_running?
- end
- end
-
- class EMTimer
- def initialize(inner)
- @inner = inner
- end
-
- def shutdown
- @inner.cancel
- end
- end
- end
- end
-end
diff --git a/actioncable/lib/action_cable/connection/identification.rb b/actioncable/lib/action_cable/connection/identification.rb
index 4a54044aff..cc544685dd 100644
--- a/actioncable/lib/action_cable/connection/identification.rb
+++ b/actioncable/lib/action_cable/connection/identification.rb
@@ -1,4 +1,6 @@
-require 'set'
+# frozen_string_literal: true
+
+require "set"
module ActionCable
module Connection
@@ -6,11 +8,10 @@ module ActionCable
extend ActiveSupport::Concern
included do
- class_attribute :identifiers
- self.identifiers = Set.new
+ class_attribute :identifiers, default: Set.new
end
- class_methods do
+ module ClassMethods
# Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
# Common identifiers are current_user and current_account, but could be anything, really.
#
diff --git a/actioncable/lib/action_cable/connection/internal_channel.rb b/actioncable/lib/action_cable/connection/internal_channel.rb
index f70d52f99b..f03904137b 100644
--- a/actioncable/lib/action_cable/connection/internal_channel.rb
+++ b/actioncable/lib/action_cable/connection/internal_channel.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionCable
module Connection
# Makes it possible for the RemoteConnection to disconnect a specific connection.
@@ -27,8 +29,8 @@ module ActionCable
end
def process_internal_message(message)
- case message['type']
- when 'disconnect'
+ case message["type"]
+ when "disconnect"
logger.info "Removing connection (#{connection_identifier})"
websocket.close
end
diff --git a/actioncable/lib/action_cable/connection/message_buffer.rb b/actioncable/lib/action_cable/connection/message_buffer.rb
index 6a80770cae..965841b67e 100644
--- a/actioncable/lib/action_cable/connection/message_buffer.rb
+++ b/actioncable/lib/action_cable/connection/message_buffer.rb
@@ -1,3 +1,5 @@
+# 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.
@@ -28,11 +30,10 @@ module ActionCable
receive_buffered_messages
end
- protected
+ private
attr_reader :connection
attr_reader :buffered_messages
- private
def valid?(message)
message.is_a?(String)
end
diff --git a/actioncable/lib/action_cable/connection/stream.rb b/actioncable/lib/action_cable/connection/stream.rb
index 0cf59091bc..e658948a55 100644
--- a/actioncable/lib/action_cable/connection/stream.rb
+++ b/actioncable/lib/action_cable/connection/stream.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "thread"
+
module ActionCable
module Connection
#--
@@ -8,9 +12,13 @@ module ActionCable
def initialize(event_loop, socket)
@event_loop = event_loop
@socket_object = socket
- @stream_send = socket.env['stream.send']
+ @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)
@@ -27,21 +35,73 @@ module ActionCable
end
def write(data)
- return @rack_hijack_io.write(data) if @rack_hijack_io
- return @stream_send.call(data) if @stream_send
+ 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']
+ return unless @socket_object.env["rack.hijack"]
- @socket_object.env['rack.hijack'].call
- @rack_hijack_io = @socket_object.env['rack.hijack_io']
+ # 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
diff --git a/actioncable/lib/action_cable/connection/stream_event_loop.rb b/actioncable/lib/action_cable/connection/stream_event_loop.rb
index 2abad09c03..d95afc50ba 100644
--- a/actioncable/lib/action_cable/connection/stream_event_loop.rb
+++ b/actioncable/lib/action_cable/connection/stream_event_loop.rb
@@ -1,11 +1,13 @@
-require 'nio'
-require 'thread'
+# frozen_string_literal: true
+
+require "nio"
+require "thread"
module ActionCable
module Connection
class StreamEventLoop
def initialize
- @nio = @thread = nil
+ @nio = @executor = @thread = nil
@map = {}
@stopping = false
@todo = Queue.new
@@ -20,13 +22,14 @@ module ActionCable
def post(task = nil, &block)
task ||= block
- Concurrent.global_io_executor << task
+ spawn
+ @executor << task
end
def attach(io, stream)
@todo << lambda do
- @map[io] = stream
- @nio.register(io, :r)
+ @map[io] = @nio.register(io, :r)
+ @map[io].value = stream
end
wakeup
end
@@ -35,6 +38,16 @@ module ActionCable
@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
@@ -52,6 +65,13 @@ module ActionCable
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
@@ -77,12 +97,25 @@ module ActionCable
monitors.each do |monitor|
io = monitor.io
- stream = @map[io]
+ stream = monitor.value
begin
- stream.receive io.read_nonblock(4096)
- rescue IO::WaitReadable
- next
+ 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
diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb
index 3742f248d1..1ad8d05107 100644
--- a/actioncable/lib/action_cable/connection/subscriptions.rb
+++ b/actioncable/lib/action_cable/connection/subscriptions.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 ActionCable
module Connection
@@ -11,33 +13,37 @@ module ActionCable
end
def execute_command(data)
- case data['command']
- when 'subscribe' then add data
- when 'unsubscribe' then remove data
- when 'message' then perform_action 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(" | ")}"
+ 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_key = data["identifier"]
id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
- subscription_klass = connection.server.channel_classes[id_options[:channel]]
+ return if subscriptions.key?(id_key)
+
+ subscription_klass = id_options[:channel].safe_constantize
- if subscription_klass
- subscriptions[id_key] ||= subscription_klass.new(connection, id_key, id_options)
+ 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 (#{data.inspect})"
+ logger.error "Subscription class not found: #{id_options[:channel].inspect}"
end
end
def remove(data)
logger.info "Unsubscribing from channel: #{data['identifier']}"
- remove_subscription subscriptions[data['identifier']]
+ remove_subscription find(data)
end
def remove_subscription(subscription)
@@ -46,7 +52,7 @@ module ActionCable
end
def perform_action(data)
- find(data).perform_action ActiveSupport::JSON.decode(data['data'])
+ find(data).perform_action ActiveSupport::JSON.decode(data["data"])
end
def identifiers
@@ -57,14 +63,12 @@ module ActionCable
subscriptions.each { |id, channel| remove_subscription(channel) }
end
- protected
- attr_reader :connection, :subscriptions
-
private
+ attr_reader :connection, :subscriptions
delegate :logger, to: :connection
def find(data)
- if subscription = subscriptions[data['identifier']]
+ if subscription = subscriptions[data["identifier"]]
subscription
else
raise "Unable to find subscription with identifier: #{data['identifier']}"
diff --git a/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb
index 41afa9680a..85831806a9 100644
--- a/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb
+++ b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb
@@ -1,3 +1,5 @@
+# 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
@@ -31,8 +33,8 @@ module ActionCable
end
end
- protected
- def log(type, message)
+ private
+ def log(type, message) # :doc:
tag(@logger) { @logger.send type, message }
end
end
diff --git a/actioncable/lib/action_cable/connection/test_case.rb b/actioncable/lib/action_cable/connection/test_case.rb
new file mode 100644
index 0000000000..f1673fea08
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/test_case.rb
@@ -0,0 +1,234 @@
+# frozen_string_literal: true
+
+require "active_support"
+require "active_support/test_case"
+require "active_support/core_ext/hash/indifferent_access"
+require "action_dispatch"
+require "action_dispatch/http/headers"
+require "action_dispatch/testing/test_request"
+
+module ActionCable
+ module Connection
+ class NonInferrableConnectionError < ::StandardError
+ def initialize(name)
+ super "Unable to determine the connection to test from #{name}. " +
+ "You'll need to specify it using `tests YourConnection` in your " +
+ "test case definition."
+ end
+ end
+
+ module Assertions
+ # Asserts that the connection is rejected (via +reject_unauthorized_connection+).
+ #
+ # # Asserts that connection without user_id fails
+ # assert_reject_connection { connect params: { user_id: '' } }
+ def assert_reject_connection(&block)
+ assert_raises(Authorization::UnauthorizedError, "Expected to reject connection but no rejection was made", &block)
+ end
+ end
+
+ # We don't want to use the whole "encryption stack" for connection
+ # unit-tests, but we want to make sure that users test against the correct types
+ # of cookies (i.e. signed or encrypted or plain)
+ class TestCookieJar < ActiveSupport::HashWithIndifferentAccess
+ def signed
+ self[:signed] ||= {}.with_indifferent_access
+ end
+
+ def encrypted
+ self[:encrypted] ||= {}.with_indifferent_access
+ end
+ end
+
+ class TestRequest < ActionDispatch::TestRequest
+ attr_accessor :session, :cookie_jar
+ end
+
+ module TestConnection
+ attr_reader :logger, :request
+
+ def initialize(request)
+ inner_logger = ActiveSupport::Logger.new(StringIO.new)
+ tagged_logging = ActiveSupport::TaggedLogging.new(inner_logger)
+ @logger = ActionCable::Connection::TaggedLoggerProxy.new(tagged_logging, tags: [])
+ @request = request
+ @env = request.env
+ end
+ end
+
+ # Unit test Action Cable connections.
+ #
+ # Useful to check whether a connection's +identified_by+ gets assigned properly
+ # and that any improper connection requests are rejected.
+ #
+ # == Basic example
+ #
+ # Unit tests are written as follows:
+ #
+ # 1. Simulate a connection attempt by calling +connect+.
+ # 2. Assert state, e.g. identifiers, has been assigned.
+ #
+ #
+ # class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
+ # def test_connects_with_proper_cookie
+ # # Simulate the connection request with a cookie.
+ # cookies["user_id"] = users(:john).id
+ #
+ # connect
+ #
+ # # Assert the connection identifier matches the fixture.
+ # assert_equal users(:john).id, connection.user.id
+ # end
+ #
+ # def test_rejects_connection_without_proper_cookie
+ # assert_reject_connection { connect }
+ # end
+ # end
+ #
+ # +connect+ accepts additional information the HTTP request with the
+ # +params+, +headers+, +session+ and Rack +env+ options.
+ #
+ # def test_connect_with_headers_and_query_string
+ # connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" }
+ #
+ # assert_equal "1", connection.user.id
+ # assert_equal "secret-my", connection.token
+ # end
+ #
+ # def test_connect_with_params
+ # connect params: { user_id: 1 }
+ #
+ # assert_equal "1", connection.user.id
+ # end
+ #
+ # You can also setup the correct cookies before the connection request:
+ #
+ # def test_connect_with_cookies
+ # # Plain cookies:
+ # cookies["user_id"] = 1
+ #
+ # # Or signed/encrypted:
+ # # cookies.signed["user_id"] = 1
+ # # cookies.encrypted["user_id"] = 1
+ #
+ # connect
+ #
+ # assert_equal "1", connection.user_id
+ # end
+ #
+ # == Connection is automatically inferred
+ #
+ # ActionCable::Connection::TestCase will automatically infer the connection 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 ConnectionTest < ActionCable::Connection::TestCase
+ # tests ApplicationCable::Connection
+ # end
+ #
+ class TestCase < ActiveSupport::TestCase
+ module Behavior
+ extend ActiveSupport::Concern
+
+ DEFAULT_PATH = "/cable"
+
+ include ActiveSupport::Testing::ConstantLookup
+ include Assertions
+
+ included do
+ class_attribute :_connection_class
+
+ attr_reader :connection
+
+ ActiveSupport.run_load_hooks(:action_cable_connection_test_case, self)
+ end
+
+ module ClassMethods
+ def tests(connection)
+ case connection
+ when String, Symbol
+ self._connection_class = connection.to_s.camelize.constantize
+ when Module
+ self._connection_class = connection
+ else
+ raise NonInferrableConnectionError.new(connection)
+ end
+ end
+
+ def connection_class
+ if connection = self._connection_class
+ connection
+ else
+ tests determine_default_connection(name)
+ end
+ end
+
+ def determine_default_connection(name)
+ connection = determine_constant_from_test_name(name) do |constant|
+ Class === constant && constant < ActionCable::Connection::Base
+ end
+ raise NonInferrableConnectionError.new(name) if connection.nil?
+ connection
+ end
+ end
+
+ # Performs connection attempt to exert #connect on the connection under test.
+ #
+ # Accepts request path as the first argument and the following request options:
+ #
+ # - params – URL parameters (Hash)
+ # - headers – request headers (Hash)
+ # - session – session data (Hash)
+ # - env – additional Rack env configuration (Hash)
+ def connect(path = ActionCable.server.config.mount_path, **request_params)
+ path ||= DEFAULT_PATH
+
+ connection = self.class.connection_class.allocate
+ connection.singleton_class.include(TestConnection)
+ connection.send(:initialize, build_test_request(path, request_params))
+ connection.connect if connection.respond_to?(:connect)
+
+ # Only set instance variable if connected successfully
+ @connection = connection
+ end
+
+ # Exert #disconnect on the connection under test.
+ def disconnect
+ raise "Must be connected!" if connection.nil?
+
+ connection.disconnect if connection.respond_to?(:disconnect)
+ @connection = nil
+ end
+
+ def cookies
+ @cookie_jar ||= TestCookieJar.new
+ end
+
+ private
+ def build_test_request(path, params: nil, headers: {}, session: {}, env: {})
+ wrapped_headers = ActionDispatch::Http::Headers.from_hash(headers)
+
+ uri = URI.parse(path)
+
+ query_string = params.nil? ? uri.query : params.to_query
+
+ request_env = {
+ "QUERY_STRING" => query_string,
+ "PATH_INFO" => uri.path
+ }.merge(env)
+
+ if wrapped_headers.present?
+ ActionDispatch::Http::Headers.from_hash(request_env).merge!(wrapped_headers)
+ end
+
+ TestRequest.create(request_env).tap do |request|
+ request.session = session.with_indifferent_access
+ request.cookie_jar = cookies
+ end
+ end
+ end
+
+ include Behavior
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb
index 0bec9b6a96..31f29fdd2f 100644
--- a/actioncable/lib/action_cable/connection/web_socket.rb
+++ b/actioncable/lib/action_cable/connection/web_socket.rb
@@ -1,11 +1,13 @@
-require 'websocket/driver'
+# frozen_string_literal: true
+
+require "websocket/driver"
module ActionCable
module Connection
# Wrap the real socket to minimize the externally-presented API
- class WebSocket
- def initialize(env, event_target, event_loop, client_socket_class)
- @websocket = ::WebSocket::Driver.websocket?(env) ? client_socket_class.new(env, event_target, event_loop) : nil
+ 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?
@@ -24,11 +26,15 @@ module ActionCable
websocket.close
end
+ def protocol
+ websocket.protocol
+ end
+
def rack_response
websocket.rack_response
end
- protected
+ private
attr_reader :websocket
end
end
diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb
index 7dc541d00c..53cbb597cd 100644
--- a/actioncable/lib/action_cable/engine.rb
+++ b/actioncable/lib/action_cable/engine.rb
@@ -1,10 +1,12 @@
+# 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 Railtie < Rails::Engine # :nodoc:
+ class Engine < Rails::Engine # :nodoc:
config.action_cable = ActiveSupport::OrderedOptions.new
config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
@@ -22,7 +24,7 @@ module ActionCable
initializer "action_cable.set_configs" do |app|
options = app.config.action_cable
- options.allowed_request_origins ||= "http://localhost:3000" if ::Rails.env.development?
+ options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development?
app.paths.add "config/cable", with: "config/cable.yml"
@@ -31,13 +33,10 @@ module ActionCable
self.cable = Rails.application.config_for(config_path).with_indifferent_access
end
- if 'ApplicationCable::Connection'.safe_constantize
- self.connection_class = ApplicationCable::Connection
- end
-
- self.channel_paths = Rails.application.paths['app/channels'].existent
+ previous_connection_class = connection_class
+ self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call }
- options.each { |k,v| send("#{k}=", v) }
+ options.each { |k, v| send("#{k}=", v) }
end
end
diff --git a/actioncable/lib/action_cable/gem_version.rb b/actioncable/lib/action_cable/gem_version.rb
index 67adeefaff..2be81736c6 100644
--- a/actioncable/lib/action_cable/gem_version.rb
+++ b/actioncable/lib/action_cable/gem_version.rb
@@ -1,3 +1,5 @@
+# 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
@@ -5,7 +7,7 @@ module ActionCable
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "beta3"
diff --git a/actioncable/lib/action_cable/helpers/action_cable_helper.rb b/actioncable/lib/action_cable/helpers/action_cable_helper.rb
index 2081a37db6..df16c02e83 100644
--- a/actioncable/lib/action_cable/helpers/action_cable_helper.rb
+++ b/actioncable/lib/action_cable/helpers/action_cable_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionCable
module Helpers
module ActionCableHelper
@@ -6,7 +8,7 @@ module ActionCable
#
# <head>
# <%= action_cable_meta_tag %>
- # <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
+ # <%= javascript_include_tag 'application', 'data-turbolinks-track' => 'reload' %>
# </head>
#
# This is then used by Action Cable to determine the URL of your WebSocket server.
diff --git a/actioncable/lib/action_cable/remote_connections.rb b/actioncable/lib/action_cable/remote_connections.rb
index aeef8abc72..283400d9e7 100644
--- a/actioncable/lib/action_cable/remote_connections.rb
+++ b/actioncable/lib/action_cable/remote_connections.rb
@@ -1,3 +1,7 @@
+# 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
@@ -28,7 +32,7 @@ module ActionCable
private
# Represents a single remote connection found via <tt>ActionCable.server.remote_connections.where(*)</tt>.
- # Exists for the solely for the purpose of calling #disconnect on that connection.
+ # Exists solely for the purpose of calling #disconnect on that connection.
class RemoteConnection
class InvalidIdentifiersError < StandardError; end
@@ -41,20 +45,21 @@ module ActionCable
# Uses the internal channel to disconnect the connection.
def disconnect
- server.broadcast internal_channel, type: 'disconnect'
+ server.broadcast internal_channel, type: "disconnect"
end
# Returns all the identifiers that were applied to this connection.
- def identifiers
+ redefine_method :identifiers do
server.connection_identifiers
end
- private
+ 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) }
+ ids.each { |k, v| instance_variable_set("@#{k}", v) }
end
def valid_identifiers?(ids)
diff --git a/actioncable/lib/action_cable/server.rb b/actioncable/lib/action_cable/server.rb
index bd6a3826a3..8d485a44f6 100644
--- a/actioncable/lib/action_cable/server.rb
+++ b/actioncable/lib/action_cable/server.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionCable
module Server
extend ActiveSupport::Autoload
@@ -9,7 +11,7 @@ module ActionCable
autoload :Configuration
autoload :Worker
- autoload :ActiveRecordConnectionManagement, 'action_cable/server/worker/active_record_connection_management'
+ 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
index b1a0e11631..98b3743175 100644
--- a/actioncable/lib/action_cable/server/base.rb
+++ b/actioncable/lib/action_cable/server/base.rb
@@ -1,4 +1,6 @@
-require 'monitor'
+# frozen_string_literal: true
+
+require "monitor"
module ActionCable
module Server
@@ -10,36 +12,45 @@ module ActionCable
include ActionCable::Server::Broadcasting
include ActionCable::Server::Connections
- cattr_accessor(:config, instance_accessor: true) { ActionCable::Server::Configuration.new }
+ cattr_accessor :config, instance_accessor: false, default: ActionCable::Server::Configuration.new
+
+ attr_reader :config
def self.logger; config.logger; end
delegate :logger, to: :config
attr_reader :mutex
- def initialize
+ def initialize(config: self.class.config)
+ @config = config
@mutex = Monitor.new
- @remote_connections = @event_loop = @worker_pool = @channel_classes = @pubsub = nil
+ @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.new(self, env).process
+ config.connection_class.call.new(self, env).process
end
- # Disconnect all the connections identified by `identifiers` on this server or any others via RemoteConnections.
+ # 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)
+ connections.each do |connection|
+ connection.close(reason: ActionCable::INTERNAL[:disconnect_reasons][:server_restart])
+ end
@mutex.synchronize do
- worker_pool.halt if @worker_pool
-
+ # 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
@@ -49,34 +60,24 @@ module ActionCable
end
def event_loop
- @event_loop || @mutex.synchronize { @event_loop ||= config.event_loop_class.new }
+ @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 config.action_cable.worker_pool_size.
+ # 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 db connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger
- # db connection pool instead.
+ # 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
- # Requires and returns a hash of all of the channel class constants, which are keyed by name.
- def channel_classes
- @channel_classes || @mutex.synchronize do
- @channel_classes ||= begin
- config.channel_paths.each { |channel_path| require channel_path }
- config.channel_class_names.each_with_object({}) { |name, hash| hash[name] = name.constantize }
- end
- end
- end
-
# Adapter used for all streams/broadcasting.
def pubsub
@pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) }
@@ -84,7 +85,7 @@ module ActionCable
# All of the identifiers applied to the connection class associated with this server.
def connection_identifiers
- config.connection_class.identifiers
+ config.connection_class.call.identifiers
end
end
diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb
index 8f93564113..bc54d784b3 100644
--- a/actioncable/lib/action_cable/server/broadcasting.rb
+++ b/actioncable/lib/action_cable/server/broadcasting.rb
@@ -1,3 +1,5 @@
+# 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
@@ -38,9 +40,13 @@ module ActionCable
end
def broadcast(message)
- server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}"
- encoded = coder ? coder.encode(message) : message
- server.pubsub.broadcast broadcasting, encoded
+ 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
diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb
index 0bb378cf03..26209537df 100644
--- a/actioncable/lib/action_cable/server/configuration.rb
+++ b/actioncable/lib/action_cable/server/configuration.rb
@@ -1,64 +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 :use_faye, :connection_class, :worker_pool_size
- attr_accessor :disable_request_forgery_protection, :allowed_request_origins
+ 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
- attr_accessor :channel_paths # :nodoc:
-
def initialize
@log_tags = []
- @connection_class = ActionCable::Connection::Base
+ @connection_class = -> { ActionCable::Connection::Base }
@worker_pool_size = 4
@disable_request_forgery_protection = false
- end
-
- def channel_class_names
- @channel_class_names ||= channel_paths.collect do |channel_path|
- Pathname.new(channel_path).basename.to_s.split('.').first.camelize
- end
+ @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' })
+ adapter = (cable.fetch("adapter") { "redis" })
+
+ # Require the adapter itself and give useful feedback about
+ # 1. Missing adapter gems and
+ # 2. Adapter gems' missing dependencies.
path_to_adapter = "action_cable/subscription_adapter/#{adapter}"
begin
require path_to_adapter
- rescue Gem::LoadError => e
- raise Gem::LoadError, "Specified '#{adapter}' for Action Cable pubsub adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by Action Cable)."
rescue LoadError => e
- raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/cable.yml is valid. If you use an adapter other than 'postgresql' or 'redis' add the necessary adapter gem to the Gemfile.", e.backtrace
+ # We couldn't require the adapter itself. Raise an exception that
+ # points out config typos and missing gems.
+ if e.path == path_to_adapter
+ # We can assume that a non-builtin adapter was specified, so it's
+ # either misspelled or missing from Gemfile.
+ raise e.class, "Could not load the '#{adapter}' Action Cable pubsub adapter. Ensure that the adapter is spelled correctly in config/cable.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
+
+ # Bubbled up from the adapter require. Prefix the exception message
+ # with some guidance about how to address it and reraise.
+ else
+ raise e.class, "Error loading the '#{adapter}' Action Cable pubsub adapter. Missing a gem it depends on? #{e.message}", e.backtrace
+ end
end
adapter = adapter.camelize
- adapter = 'PostgreSQL' if adapter == 'Postgresql'
+ adapter = "PostgreSQL" if adapter == "Postgresql"
"ActionCable::SubscriptionAdapter::#{adapter}".constantize
end
-
- def event_loop_class
- if use_faye
- ActionCable::Connection::FayeEventLoop
- else
- ActionCable::Connection::StreamEventLoop
- end
- end
-
- def client_socket_class
- if use_faye
- ActionCable::Connection::FayeClientSocket
- else
- ActionCable::Connection::ClientSocket
- end
- end
end
end
end
diff --git a/actioncable/lib/action_cable/server/connections.rb b/actioncable/lib/action_cable/server/connections.rb
index 5e61b4e335..39557d63a7 100644
--- a/actioncable/lib/action_cable/server/connections.rb
+++ b/actioncable/lib/action_cable/server/connections.rb
@@ -1,3 +1,5 @@
+# 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
diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb
index 49cbaec0c0..187c8f7939 100644
--- a/actioncable/lib/action_cable/server/worker.rb
+++ b/actioncable/lib/action_cable/server/worker.rb
@@ -1,6 +1,8 @@
-require 'active_support/callbacks'
-require 'active_support/core_ext/module/attribute_accessors_per_thread'
-require 'concurrent'
+# frozen_string_literal: true
+
+require "active_support/callbacks"
+require "active_support/core_ext/module/attribute_accessors_per_thread"
+require "concurrent"
module ActionCable
module Server
@@ -12,8 +14,10 @@ module ActionCable
define_callbacks :work
include ActiveRecordConnectionManagement
+ attr_reader :executor
+
def initialize(max_size: 5)
- @pool = Concurrent::ThreadPoolExecutor.new(
+ @executor = Concurrent::ThreadPoolExecutor.new(
min_threads: 1,
max_threads: max_size,
max_queue: 0,
@@ -23,11 +27,11 @@ module ActionCable
# Stop processing work: any work that has not already started
# running will be discarded from the queue
def halt
- @pool.kill
+ @executor.shutdown
end
def stopping?
- @pool.shuttingdown?
+ @executor.shuttingdown?
end
def work(connection)
@@ -40,34 +44,24 @@ module ActionCable
self.connection = nil
end
- def async_invoke(receiver, method, *args)
- @pool.post do
- invoke(receiver, method, *args)
- end
+ def async_exec(receiver, *args, connection:, &block)
+ async_invoke receiver, :instance_exec, *args, connection: connection, &block
end
- def invoke(receiver, method, *args)
- work(receiver) do
- begin
- receiver.send method, *args
- rescue Exception => e
- logger.error "There was an exception - #{e.class}(#{e.message})"
- logger.error e.backtrace.join("\n")
-
- receiver.handle_exception if receiver.respond_to?(:handle_exception)
- end
+ def async_invoke(receiver, method, *args, connection: receiver, &block)
+ @executor.post do
+ invoke(receiver, method, *args, connection: connection, &block)
end
end
- def async_run_periodic_timer(channel, callback)
- @pool.post do
- run_periodic_timer(channel, callback)
- end
- end
+ def invoke(receiver, method, *args, connection:, &block)
+ work(connection) do
+ receiver.send method, *args, &block
+ rescue Exception => e
+ logger.error "There was an exception - #{e.class}(#{e.message})"
+ logger.error e.backtrace.join("\n")
- def run_periodic_timer(channel, callback)
- work(channel.connection) do
- callback.respond_to?(:call) ? channel.instance_exec(&callback) : channel.send(callback)
+ receiver.handle_exception if receiver.respond_to?(:handle_exception)
end
end
diff --git a/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb
index c1e4aa8103..2e378d4bf3 100644
--- a/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb
+++ b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionCable
module Server
class Worker
diff --git a/actioncable/lib/action_cable/subscription_adapter.rb b/actioncable/lib/action_cable/subscription_adapter.rb
index 72e62f3daf..6a9d5c2080 100644
--- a/actioncable/lib/action_cable/subscription_adapter.rb
+++ b/actioncable/lib/action_cable/subscription_adapter.rb
@@ -1,8 +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
index 10b3ac8cd8..c9930299c7 100644
--- a/actioncable/lib/action_cable/subscription_adapter/async.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/async.rb
@@ -1,4 +1,6 @@
-require 'action_cable/subscription_adapter/inline'
+# frozen_string_literal: true
+
+require "action_cable/subscription_adapter/inline"
module ActionCable
module SubscriptionAdapter
diff --git a/actioncable/lib/action_cable/subscription_adapter/base.rb b/actioncable/lib/action_cable/subscription_adapter/base.rb
index 796db5ffa3..34077707fd 100644
--- a/actioncable/lib/action_cable/subscription_adapter/base.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionCable
module SubscriptionAdapter
class Base
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/evented_redis.rb b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb
deleted file mode 100644
index 256876cf30..0000000000
--- a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-require 'thread'
-
-gem 'em-hiredis', '~> 0.3.0'
-gem 'redis', '~> 3.0'
-require 'em-hiredis'
-require 'redis'
-
-EventMachine.epoll if EventMachine.epoll?
-EventMachine.kqueue if EventMachine.kqueue?
-
-module ActionCable
- module SubscriptionAdapter
- class EventedRedis < Base # :nodoc:
- @@mutex = Mutex.new
-
- # Overwrite this factory method for EventMachine Redis connections if you want to use a different Redis connection library than EM::Hiredis.
- # This is needed, for example, when using Makara proxies for distributed Redis.
- cattr_accessor(:em_redis_connector) { ->(config) { EM::Hiredis.connect(config[:url]) } }
-
- # Overwrite this factory method for Redis connections if you want to use a different Redis connection library than Redis.
- # This is needed, for example, when using Makara proxies for distributed Redis.
- cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } }
-
- def initialize(*)
- super
- @redis_connection_for_broadcasts = @redis_connection_for_subscriptions = nil
- end
-
- def broadcast(channel, payload)
- redis_connection_for_broadcasts.publish(channel, payload)
- end
-
- def subscribe(channel, message_callback, success_callback = nil)
- redis_connection_for_subscriptions.pubsub.subscribe(channel, &message_callback).tap do |result|
- result.callback { |reply| success_callback.call } if success_callback
- end
- end
-
- def unsubscribe(channel, message_callback)
- redis_connection_for_subscriptions.pubsub.unsubscribe_proc(channel, message_callback)
- end
-
- def shutdown
- redis_connection_for_subscriptions.pubsub.close_connection
- @redis_connection_for_subscriptions = nil
- end
-
- private
- def redis_connection_for_subscriptions
- ensure_reactor_running
- @redis_connection_for_subscriptions || @server.mutex.synchronize do
- @redis_connection_for_subscriptions ||= self.class.em_redis_connector.call(@server.config.cable).tap do |redis|
- redis.on(:reconnect_failed) do
- @logger.error "[ActionCable] Redis reconnect failed."
- end
- end
- end
- end
-
- def redis_connection_for_broadcasts
- @redis_connection_for_broadcasts || @server.mutex.synchronize do
- @redis_connection_for_broadcasts ||= self.class.redis_connector.call(@server.config.cable)
- end
- end
-
- def ensure_reactor_running
- return if EventMachine.reactor_running?
- @@mutex.synchronize do
- Thread.new { EventMachine.run } unless EventMachine.reactor_running?
- Thread.pass until EventMachine.reactor_running?
- end
- end
- end
- end
-end
diff --git a/actioncable/lib/action_cable/subscription_adapter/inline.rb b/actioncable/lib/action_cable/subscription_adapter/inline.rb
index 81357faead..d2c85c1c8d 100644
--- a/actioncable/lib/action_cable/subscription_adapter/inline.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/inline.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionCable
module SubscriptionAdapter
class Inline < Base # :nodoc:
diff --git a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
index 66c7852f6e..1d60bed4af 100644
--- a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
@@ -1,50 +1,75 @@
-gem 'pg', '~> 0.18'
-require 'pg'
-require 'thread'
+# 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:
+ prepend ChannelPrefix
+
def initialize(*)
super
@listener = nil
end
def broadcast(channel, payload)
- with_connection do |pg_conn|
- pg_conn.exec("NOTIFY #{pg_conn.escape_identifier(channel)}, '#{pg_conn.escape_string(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, callback, success_callback)
+ listener.add_subscriber(channel_identifier(channel), callback, success_callback)
end
def unsubscribe(channel, callback)
- listener.remove_subscriber(channel, callback)
+ listener.remove_subscriber(channel_identifier(channel), callback)
end
def shutdown
listener.shutdown
end
- def with_connection(&block) # :nodoc:
- ActiveRecord::Base.connection_pool.with_connection do |ar_conn|
- pg_conn = ar_conn.raw_connection
+ 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
- unless pg_conn.is_a?(PG::Connection)
- raise 'ActiveRecord database must be Postgres in order to use the Postgres ActionCable storage adapter'
- end
+ 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()
@@ -60,7 +85,7 @@ module ActionCable
end
def listen
- @adapter.with_connection do |pg_conn|
+ @adapter.with_subscriptions_connection do |pg_conn|
catch :shutdown do
loop do
until @queue.empty?
diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb
index 65434f7107..ad8fa52760 100644
--- a/actioncable/lib/action_cable/subscription_adapter/redis.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb
@@ -1,14 +1,21 @@
-require 'thread'
+# frozen_string_literal: true
-gem 'redis', '~> 3.0'
-require 'redis'
+require "thread"
+
+gem "redis", ">= 3", "< 5"
+require "redis"
module ActionCable
module SubscriptionAdapter
class Redis < Base # :nodoc:
- # Overwrite this factory method for redis connections if you want to use a different Redis library than Redis.
+ 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) { ->(config) { ::Redis.new(url: config[:url]) } }
+ 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
@@ -70,9 +77,9 @@ module ActionCable
def listen(conn)
conn.without_reconnect do
- original_client = conn.client
+ original_client = conn.respond_to?(:_client) ? conn._client : conn.client
- conn.subscribe('_action_cable_internal') do |on|
+ conn.subscribe("_action_cable_internal") do |on|
on.subscribe do |chan, count|
@subscription_lock.synchronize do
if count == 1
@@ -111,7 +118,7 @@ module ActionCable
return if @thread.nil?
when_connected do
- send_command('unsubscribe')
+ send_command("unsubscribe")
@raw_client = nil
end
end
@@ -123,13 +130,13 @@ module ActionCable
@subscription_lock.synchronize do
ensure_listener_running
@subscribe_callbacks[channel] << on_success
- when_connected { send_command('subscribe', channel) }
+ when_connected { send_command("subscribe", channel) }
end
end
def remove_channel(channel)
@subscription_lock.synchronize do
- when_connected { send_command('unsubscribe', channel) }
+ when_connected { send_command("unsubscribe", channel) }
end
end
diff --git a/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb b/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb
index 37eed09793..01cdc2dfa1 100644
--- a/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
module ActionCable
module SubscriptionAdapter
class SubscriberMap
def initialize
- @subscribers = Hash.new { |h,k| h[k] = [] }
+ @subscribers = Hash.new { |h, k| h[k] = [] }
@sync = Mutex.new
end
@@ -32,7 +34,11 @@ module ActionCable
end
def broadcast(channel, message)
- list = @sync.synchronize { @subscribers[channel].dup }
+ list = @sync.synchronize do
+ return if !@subscribers.key?(channel)
+ @subscribers[channel].dup
+ end
+
list.each do |subscriber|
invoke_callback(subscriber, message)
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..26b849a273
--- /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_transmitted_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
index e17877202b..86115c6065 100644
--- a/actioncable/lib/action_cable/version.rb
+++ b/actioncable/lib/action_cable/version.rb
@@ -1,4 +1,6 @@
-require_relative 'gem_version'
+# 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>
diff --git a/actioncable/lib/rails/generators/channel/USAGE b/actioncable/lib/rails/generators/channel/USAGE
index 6249553c22..bb5dd7e2db 100644
--- a/actioncable/lib/rails/generators/channel/USAGE
+++ b/actioncable/lib/rails/generators/channel/USAGE
@@ -1,14 +1,13 @@
Description:
============
- Stubs out a new cable channel for the server (in Ruby) and client (in CoffeeScript).
+ 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.
- Note: Turn on the cable connection in app/assets/javascript/cable.js after generating any channels.
-
Example:
========
rails generate channel Chat speak
- creates a Chat channel class and CoffeeScript asset:
+ creates a Chat channel class, test and JavaScript asset:
Channel: app/channels/chat_channel.rb
- Assets: app/assets/javascript/channels/chat.coffee
+ Test: test/channels/chat_channel_test.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
index d89ab45816..0b80d1f96b 100644
--- a/actioncable/lib/rails/generators/channel/channel_generator.rb
+++ b/actioncable/lib/rails/generators/channel/channel_generator.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module Rails
module Generators
class ChannelGenerator < NamedBase
- source_root File.expand_path("../templates", __FILE__)
+ source_root File.expand_path("templates", __dir__)
argument :actions, type: :array, default: [], banner: "method method"
@@ -9,32 +11,39 @@ module Rails
check_class_collision suffix: "Channel"
+ hook_for :test_framework
+
def create_channel_file
- template "channel.rb", File.join('app/channels', class_path, "#{file_name}_channel.rb")
+ template "channel.rb", File.join("app/channels", class_path, "#{file_name}_channel.rb")
if options[:assets]
- template "assets/channel.coffee", File.join('app/assets/javascripts/channels', class_path, "#{file_name}.coffee")
+ 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
- protected
+ private
def file_name
- @_file_name ||= super.gsub(/_channel/i, '')
+ @_file_name ||= super.sub(/_channel\z/i, "")
end
# FIXME: Change these files to symlinks once RubyGems 2.5.0 is required.
def generate_application_cable_files
- return if self.behavior != :invoke
+ return if behavior != :invoke
files = [
- 'application_cable/channel.rb',
- 'application_cable/connection.rb'
+ "application_cable/channel.rb",
+ "application_cable/connection.rb"
]
files.each do |name|
- path = File.join('app/channels/', name)
+ path = File.join("app/channels/", name)
template(name, path) if !File.exist?(path)
end
end
diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb
deleted file mode 100644
index d56fa30f4d..0000000000
--- a/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
-module ApplicationCable
- class Channel < ActionCable::Channel::Base
- end
-end
diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/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 b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb
deleted file mode 100644
index b4f41389ad..0000000000
--- a/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
-module ApplicationCable
- class Connection < ActionCable::Connection::Base
- end
-end
diff --git a/actioncable/lib/rails/generators/channel/templates/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/assets/channel.coffee b/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee
deleted file mode 100644
index 5467811aba..0000000000
--- a/actioncable/lib/rails/generators/channel/templates/assets/channel.coffee
+++ /dev/null
@@ -1,14 +0,0 @@
-App.<%= class_name.underscore %> = App.cable.subscriptions.create "<%= class_name %>Channel",
- connected: ->
- # Called when the subscription is ready for use on the server
-
- disconnected: ->
- # Called when the subscription has been terminated by the server
-
- received: (data) ->
- # Called when there's incoming data on the websocket for this channel
-<% actions.each do |action| -%>
-
- <%= action %>: ->
- @perform '<%= action %>'
-<% end -%>
diff --git a/actioncable/lib/rails/generators/channel/templates/channel.rb b/actioncable/lib/rails/generators/channel/templates/channel.rb
deleted file mode 100644
index 7bff3341c1..0000000000
--- a/actioncable/lib/rails/generators/channel/templates/channel.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
-<% 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/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..ddf6b2d79b
--- /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() {
+ // Called when the subscription is ready for use on the server
+ },
+
+ disconnected() {
+ // Called when the subscription has been terminated by the server
+ },
+
+ received(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..0eceb59b18
--- /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 { createConsumer } from "@rails/actioncable"
+
+export default 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/lib/rails/generators/test_unit/channel_generator.rb b/actioncable/lib/rails/generators/test_unit/channel_generator.rb
new file mode 100644
index 0000000000..7d13a12f0a
--- /dev/null
+++ b/actioncable/lib/rails/generators/test_unit/channel_generator.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module TestUnit
+ module Generators
+ class ChannelGenerator < ::Rails::Generators::NamedBase
+ source_root File.expand_path("templates", __dir__)
+
+ check_class_collision suffix: "ChannelTest"
+
+ def create_test_files
+ template "channel_test.rb", File.join("test/channels", class_path, "#{file_name}_channel_test.rb")
+ end
+
+ private
+ def file_name # :doc:
+ @_file_name ||= super.sub(/_channel\z/i, "")
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/rails/generators/test_unit/templates/channel_test.rb.tt b/actioncable/lib/rails/generators/test_unit/templates/channel_test.rb.tt
new file mode 100644
index 0000000000..7307654611
--- /dev/null
+++ b/actioncable/lib/rails/generators/test_unit/templates/channel_test.rb.tt
@@ -0,0 +1,8 @@
+require "test_helper"
+
+class <%= class_name %>ChannelTest < ActionCable::Channel::TestCase
+ # test "subscribes" do
+ # subscribe
+ # assert subscription.confirmed?
+ # end
+end
diff --git a/actioncable/package.json b/actioncable/package.json
new file mode 100644
index 0000000000..4451afd17f
--- /dev/null
+++ b/actioncable/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@rails/actioncable",
+ "version": "6.0.0-beta3",
+ "description": "WebSocket framework for Ruby on Rails.",
+ "main": "app/assets/javascripts/action_cable.js",
+ "files": [
+ "app/assets/javascripts/*.js",
+ "src/*.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",
+ "karma": "^3.1.1",
+ "karma-chrome-launcher": "^2.2.0",
+ "karma-qunit": "^2.1.0",
+ "karma-sauce-launcher": "^1.2.0",
+ "mock-socket": "^2.0.0",
+ "qunit": "^2.8.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",
+ "prepublishOnly": "rm -rf src && cp -R app/javascript/action_cable src",
+ "pretest": "bundle exec rake assets:codegen && rollup --config rollup.config.test.js",
+ "test": "karma start"
+ }
+}
diff --git a/actioncable/rollup.config.js b/actioncable/rollup.config.js
new file mode 100644
index 0000000000..64727e0887
--- /dev/null
+++ b/actioncable/rollup.config.js
@@ -0,0 +1,24 @@
+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: [
+ babel(),
+ uglify(uglifyOptions)
+ ]
+}
diff --git a/actioncable/rollup.config.test.js b/actioncable/rollup.config.test.js
new file mode 100644
index 0000000000..f92ff36240
--- /dev/null
+++ b/actioncable/rollup.config.test.js
@@ -0,0 +1,18 @@
+import babel from "rollup-plugin-babel"
+import commonjs from "rollup-plugin-commonjs"
+import resolve from "rollup-plugin-node-resolve"
+
+export default {
+ input: "test/javascript/src/test.js",
+
+ output: {
+ file: "test/javascript/compiled/test.js",
+ format: "iife"
+ },
+
+ plugins: [
+ resolve(),
+ commonjs(),
+ babel()
+ ]
+}
diff --git a/actioncable/test/channel/base_test.rb b/actioncable/test/channel/base_test.rb
index daa782eeb3..39b5879607 100644
--- a/actioncable/test/channel/base_test.rb
+++ b/actioncable/test/channel/base_test.rb
@@ -1,8 +1,11 @@
-require 'test_helper'
-require 'stubs/test_connection'
-require 'stubs/room'
+# frozen_string_literal: true
-class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
+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 ]
@@ -23,6 +26,9 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
after_subscribe :toggle_subscribed
after_unsubscribe :toggle_subscribed
+ class SomeCustomError < StandardError; end
+ rescue_from SomeCustomError, with: :error_handler
+
def initialize(*)
@subscribed = false
super
@@ -58,30 +64,40 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
end
def get_latest
- transmit data: 'latest'
+ transmit data: "latest"
end
def receive
@last_action = [ :receive ]
end
+ def error_action
+ raise SomeCustomError
+ end
+
private
def rm_rf
@last_action = [ :rm_rf ]
end
+
+ def error_handler
+ @last_action = [ :error_action ]
+ end
end
setup do
@user = User.new "lifo"
@connection = TestConnection.new(@user)
- @channel = ChatChannel.new @connection, "{id: 1}", { id: 1 }
+ @channel = ChatChannel.new @connection, "{id: 1}", id: 1
end
- test "should subscribe to a channel on initialize" do
+ 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
@@ -90,13 +106,15 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
end
test "unsubscribing from a channel" do
+ @channel.subscribe_to_channel
+
assert @channel.room
- assert @channel.subscribed?
+ assert_predicate @channel, :subscribed?
@channel.unsubscribe_from_channel
- assert ! @channel.room
- assert ! @channel.subscribed?
+ assert_not @channel.room
+ assert_not_predicate @channel, :subscribed?
end
test "connection identifiers" do
@@ -104,141 +122,146 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
end
test "callable action without any argument" do
- @channel.perform_action 'action' => :leave
+ @channel.perform_action "action" => :leave
assert_equal [ :leave ], @channel.last_action
end
test "callable action with arguments" do
- data = { 'action' => :speak, 'content' => "Hello World" }
+ 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
+ @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
+ @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!" }
+ 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
+ @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' }
+ data = { "content" => "hello" }
@channel.perform_action data
assert_equal [ :receive ], @channel.last_action
end
test "transmitting data" do
- @channel.perform_action 'action' => :get_latest
+ @channel.perform_action "action" => :get_latest
- expected = { "identifier" => "{id: 1}", "message" => { "data" => "latest" }}
+ expected = { "identifier" => "{id: 1}", "message" => { "data" => "latest" } }
assert_equal expected, @connection.last_transmission
end
- test "subscription confirmation" do
+ 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
+ available_actions = %w(room last_action subscribed unsubscribed toggle_subscribed leave speak subscribed? get_latest receive chatters topic error_action).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
+ @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
+ 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
+ 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
+ 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
test "notification for transmit" do
- begin
- events = []
- ActiveSupport::Notifications.subscribe 'transmit.action_cable' do |*args|
- events << ActiveSupport::Notifications::Event.new(*args)
- end
+ 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'}
+ @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
+ 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
test "notification for transmit_subscription_confirmation" do
- begin
- events = []
- ActiveSupport::Notifications.subscribe 'transmit_subscription_confirmation.action_cable' do |*args|
- events << ActiveSupport::Notifications::Event.new(*args)
- end
+ @channel.subscribe_to_channel
+
+ events = []
+ ActiveSupport::Notifications.subscribe "transmit_subscription_confirmation.action_cable" do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
- @channel.stubs(:subscription_confirmation_sent?).returns(false)
+ @channel.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]
- ensure
- ActiveSupport::Notifications.unsubscribe 'transmit_subscription_confirmation.action_cable'
+ 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
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
+ events = []
+ ActiveSupport::Notifications.subscribe "transmit_subscription_rejection.action_cable" do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
- @channel.send(:transmit_subscription_rejection)
+ @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
+ 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
+
+ test "behaves like rescuable" do
+ @channel.perform_action "action" => :error_action
+ assert_equal [ :error_action ], @channel.last_action
end
private
diff --git a/actioncable/test/channel/broadcasting_test.rb b/actioncable/test/channel/broadcasting_test.rb
index 1de04243e5..fb501a1bc2 100644
--- a/actioncable/test/channel/broadcasting_test.rb
+++ b/actioncable/test/channel/broadcasting_test.rb
@@ -1,8 +1,10 @@
-require 'test_helper'
-require 'stubs/test_connection'
-require 'stubs/room'
+# frozen_string_literal: true
-class ActionCable::Channel::BroadcastingTest < ActiveSupport::TestCase
+require "test_helper"
+require "stubs/test_connection"
+require "stubs/room"
+
+class ActionCable::Channel::BroadcastingTest < ActionCable::TestCase
class ChatChannel < ActionCable::Channel::Base
end
@@ -11,19 +13,36 @@ class ActionCable::Channel::BroadcastingTest < ActiveSupport::TestCase
end
test "broadcasts_to" do
- ActionCable.stubs(:server).returns mock().tap { |m| m.expects(:broadcast).with('action_cable:channel:broadcasting_test:chat:Room#1-Campfire', "Hello World") }
- ChatChannel.broadcast_to(Room.new(1), "Hello World")
+ 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))
+ assert_equal(
+ "action_cable:channel:broadcasting_test:chat:Room#1-Campfire",
+ ChatChannel.broadcasting_for(Room.new(1))
+ )
end
test "broadcasting_for with an array" do
- assert_equal "Room#1-Campfire:Room#2-Campfire", ChatChannel.broadcasting_for([ Room.new(1), Room.new(2) ])
+ assert_equal(
+ "action_cable:channel:broadcasting_test:chat:Room#1-Campfire:Room#2-Campfire",
+ ChatChannel.broadcasting_for([ Room.new(1), Room.new(2) ])
+ )
end
test "broadcasting_for with a string" do
- assert_equal "hello", ChatChannel.broadcasting_for("hello")
+ assert_equal(
+ "action_cable:channel:broadcasting_test:chat:hello",
+ ChatChannel.broadcasting_for("hello")
+ )
end
end
diff --git a/actioncable/test/channel/naming_test.rb b/actioncable/test/channel/naming_test.rb
index 89ef6ad8b0..45652d9cc9 100644
--- a/actioncable/test/channel/naming_test.rb
+++ b/actioncable/test/channel/naming_test.rb
@@ -1,6 +1,8 @@
-require 'test_helper'
+# frozen_string_literal: true
-class ActionCable::Channel::NamingTest < ActiveSupport::TestCase
+require "test_helper"
+
+class ActionCable::Channel::NamingTest < ActionCable::TestCase
class ChatChannel < ActionCable::Channel::Base
end
diff --git a/actioncable/test/channel/periodic_timers_test.rb b/actioncable/test/channel/periodic_timers_test.rb
index e6f0c14c9d..0c979f4c7c 100644
--- a/actioncable/test/channel/periodic_timers_test.rb
+++ b/actioncable/test/channel/periodic_timers_test.rb
@@ -1,12 +1,23 @@
-require 'test_helper'
-require 'stubs/test_connection'
-require 'stubs/room'
+# frozen_string_literal: true
-class ActionCable::Channel::PeriodicTimersTest < ActiveSupport::TestCase
+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
- periodically -> { ping }, every: 5
+ # Method name arg
periodically :send_updates, every: 1
+ # Proc arg
+ periodically -> { ping }, every: 2
+
+ # Block arg
+ periodically every: 3 do
+ ping
+ end
+
private
def ping
end
@@ -19,22 +30,56 @@ class ActionCable::Channel::PeriodicTimersTest < ActiveSupport::TestCase
test "periodic timers definition" do
timers = ChatChannel.periodic_timers
- assert_equal 2, timers.size
+ assert_equal 3, timers.size
+
+ 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
- first_timer = timers[0]
- assert_kind_of Proc, first_timer[0]
- assert_equal 5, first_timer[1][:every]
+ 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
- second_timer = timers[1]
- assert_equal :send_updates, second_timer[0]
- assert_equal 1, second_timer[1][:every]
+ 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
- @connection.server.event_loop.expects(:timer).times(2).returns(true)
- channel = ChatChannel.new @connection, "{id: 1}", { id: 1 }
+ 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
- channel.expects(:stop_periodic_timers).once
- channel.unsubscribe_from_channel
+ assert mock.verify
end
end
diff --git a/actioncable/test/channel/rejection_test.rb b/actioncable/test/channel/rejection_test.rb
index 15db57d6ba..683eafcac0 100644
--- a/actioncable/test/channel/rejection_test.rb
+++ b/actioncable/test/channel/rejection_test.rb
@@ -1,12 +1,18 @@
-require 'test_helper'
-require 'stubs/test_connection'
-require 'stubs/room'
+# frozen_string_literal: true
-class ActionCable::Channel::RejectionTest < ActiveSupport::TestCase
+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
@@ -15,11 +21,36 @@ class ActionCable::Channel::RejectionTest < ActiveSupport::TestCase
end
test "subscription rejection" do
- @connection.expects(:subscriptions).returns mock().tap { |m| m.expects(:remove_subscription).with instance_of(SecretChannel) }
- @channel = SecretChannel.new @connection, "{id: 1}", { id: 1 }
+ 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
- expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" }
- assert_equal expected, @connection.last_transmission
+ 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
index f51f19eb7d..9ad2213d47 100644
--- a/actioncable/test/channel/stream_test.rb
+++ b/actioncable/test/channel/stream_test.rb
@@ -1,6 +1,9 @@
-require 'test_helper'
-require 'stubs/test_connection'
-require 'stubs/room'
+# 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
@@ -23,22 +26,23 @@ module ActionCable::StreamTests
transmit_subscription_confirmation
end
- private def pick_coder(coder)
- case coder
- when nil, 'json'
- ActiveSupport::JSON
- when 'custom'
- DummyEncoder
- when 'none'
- nil
+ private
+ def pick_coder(coder)
+ case coder
+ when nil, "json"
+ ActiveSupport::JSON
+ when "custom"
+ DummyEncoder
+ when "none"
+ nil
+ end
end
- end
end
module DummyEncoder
extend self
def encode(*) '{ "foo": "encoded" }' end
- def decode(*) { foo: 'decoded' } end
+ def decode(*) { foo: "decoded" } end
end
class SymbolChannel < ActionCable::Channel::Base
@@ -51,32 +55,58 @@ module ActionCable::StreamTests
test "streaming start and stop" do
run_in_eventmachine do
connection = TestConnection.new
- connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("test_room_1", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
- channel = ChatChannel.new connection, "{id: 1}", { id: 1 }
+ 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
- connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
- channel.unsubscribe_from_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
- connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("channel", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
- channel = SymbolChannel.new connection, ""
+ 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
- connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
- channel.unsubscribe_from_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
- connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("action_cable:stream_tests:chat:Room#1-Campfire", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
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
@@ -84,7 +114,9 @@ module ActionCable::StreamTests
run_in_eventmachine do
connection = TestConnection.new
- ChatChannel.new connection, "{id: 1}", { id: 1 }
+ channel = ChatChannel.new connection, "{id: 1}", id: 1
+ channel.subscribe_to_channel
+
assert_nil connection.last_transmission
wait_for_async
@@ -114,45 +146,83 @@ module ActionCable::StreamTests
end
end
- require 'action_cable/subscription_adapter/inline'
+ require "action_cable/subscription_adapter/async"
- class StreamEncodingTest < ActionCable::TestCase
+ 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::Inline)
+ @server = TestServer.new(subscription_adapter: ActionCable::SubscriptionAdapter::Async)
@server.config.allowed_request_origins = %w( http://rubyonrails.com )
- @server.stubs(:channel_classes).returns(ChatChannel.name => ChatChannel)
end
- test 'custom encoder' do
+ test "custom encoder" do
run_in_eventmachine do
connection = open_connection
subscribe_to connection, identifiers: { id: 1 }
- connection.websocket.expects(:transmit)
- @server.broadcast 'test_room_1', { foo: 'bar' }, coder: DummyEncoder
+ 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
+ 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'
+ env = Rack::MockRequest.env_for "/test", "HTTP_HOST" => "localhost", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket", "HTTP_ORIGIN" => "http://rubyonrails.com"
Connection.new(@server, env).tap do |connection|
connection.process
- assert connection.websocket.possible?
+ assert_predicate connection.websocket, :possible?
wait_for_async
- assert connection.websocket.alive?
+ assert_predicate connection.websocket, :alive?
end
end
- def receive(connection, command:, identifiers:)
- identifier = JSON.generate(channel: 'ActionCable::StreamTests::ChatChannel', **identifiers)
+ 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
diff --git a/actioncable/test/channel/test_case_test.rb b/actioncable/test/channel/test_case_test.rb
new file mode 100644
index 0000000000..a166c41e11
--- /dev/null
+++ b/actioncable/test/channel/test_case_test.rb
@@ -0,0 +1,214 @@
+# 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_has_stream "test_0"
+ end
+
+ def test_stream_with_params
+ subscribe id: 42
+
+ assert_has_stream "test_42"
+ end
+end
+
+class StreamsForTestChannel < ActionCable::Channel::Base
+ def subscribed
+ stream_for User.new(params[:id])
+ end
+end
+
+class StreamsForTestChannelTest < ActionCable::Channel::TestCase
+ def test_stream_with_params
+ subscribe id: 42
+
+ assert_has_stream_for User.new(42)
+ end
+end
+
+class NoStreamsTestChannel < ActionCable::Channel::Base
+ def subscribed; end # no-op
+end
+
+class NoStreamsTestChannelTest < ActionCable::Channel::TestCase
+ def test_stream_with_params
+ subscribe
+
+ assert_no_streams
+ 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
+
+ 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/echo_channel.rb b/actioncable/test/client/echo_channel.rb
deleted file mode 100644
index 5a7bac25c5..0000000000
--- a/actioncable/test/client/echo_channel.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-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
diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb
index 5ac453db35..bf141df458 100644
--- a/actioncable/test/client_test.rb
+++ b/actioncable/test/client_test.rb
@@ -1,95 +1,151 @@
-require 'test_helper'
-require 'concurrent'
+# frozen_string_literal: true
-require 'active_support/core_ext/hash/indifferent_access'
-require 'pathname'
+require "test_helper"
+require "concurrent"
-require 'faye/websocket'
-require 'json'
+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 = 8
+ 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 = { adapter: 'async' }.with_indifferent_access
- server.config.use_faye = ENV['FAYE'].present?
+ server.config.cable = ActiveSupport::HashWithIndifferentAccess.new(adapter: "async")
# and now the "real" setup for our test:
server.config.disable_request_forgery_protection = true
- server.config.channel_paths = [ File.expand_path('client/echo_channel.rb', __dir__) ]
-
- Thread.new { EventMachine.run } unless EventMachine.reactor_running?
- Thread.pass until EventMachine.reactor_running?
-
- # faye-websocket is warning-rich
- @previous_verbose, $VERBOSE = $VERBOSE, nil
- end
-
- def teardown
- $VERBOSE = @previous_verbose
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.add_tcp_listener "127.0.0.1", port
server.min_threads = 1
server.max_threads = 4
- t = Thread.new { server.run.join }
- yield port
+ thread = server.run
+
+ begin
+ yield port
- ensure
- server.stop(true) if server
- t.join if t
+ 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)
- @ws = Faye::WebSocket::Client.new("ws://127.0.0.1:#{port}/")
- @messages = Queue.new
- @closed = Concurrent::Event.new
- @has_messages = Concurrent::Semaphore.new(0)
- @pings = 0
-
- open = Concurrent::Event.new
- error = nil
-
- @ws.on(:error) do |event|
- if open.set?
- @messages << RuntimeError.new(event.message)
- else
- error = event.message
- open.set
+ 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
- end
- @ws.on(:open) do |event|
- open.set
- end
+ ws.on(:open) do |event|
+ open.set(true)
+ end
- @ws.on(:message) do |event|
- message = JSON.parse(event.data)
- if message['type'] == 'ping'
- @pings += 1
- else
- @messages << message
- @has_messages.release
+ 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
- end
- @ws.on(:close) do |event|
- @closed.set
+ ws.on(:close) do |_|
+ closed.set
+ end
end
- open.wait(WAIT_WHEN_EXPECTING_EVENT)
- raise error if error
+ open.wait!(WAIT_WHEN_EXPECTING_EVENT)
end
def read_message
@@ -140,76 +196,80 @@ class ClientTest < ActionCable::TestCase
end
end
- def faye_client(port)
+ 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 = faye_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: 'EchoChannel')
- assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello')
- assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "message"=>{"dong"=>"hello"}}, c.read_message)
+ c = 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 = 10.times.map { faye_client(port) }
+ clients = concurrently(10.times) { websocket_client(port) }
barrier_1 = Concurrent::CyclicBarrier.new(clients.size)
barrier_2 = Concurrent::CyclicBarrier.new(clients.size)
- clients.map {|c| Concurrent::Future.execute {
- assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack
- c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
- assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello')
- assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message)
+ 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: 'EchoChannel'), data: JSON.generate(action: 'bulk', message: 'hello')
+ 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
- } }.each(&:wait!)
+ end
- clients.map {|c| Concurrent::Future.execute { c.close } }.each(&:wait!)
+ concurrently(clients, &:close)
end
end
def test_many_clients
with_puma_server do |port|
- clients = 100.times.map { faye_client(port) }
-
- clients.map {|c| Concurrent::Future.execute {
- assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack
- c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
- assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello')
- assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message)
- } }.each(&:wait!)
+ 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
- clients.map {|c| Concurrent::Future.execute { c.close } }.each(&:wait!)
+ concurrently(clients, &:close)
end
end
def test_disappearing_client
with_puma_server do |port|
- c = faye_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: 'EchoChannel')
- assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'delay', message: 'hello')
+ 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 = faye_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: 'EchoChannel')
- assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello')
- assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message)
+ c = 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
@@ -217,19 +277,22 @@ class ClientTest < ActionCable::TestCase
def test_unsubscribe_client
with_puma_server do |port|
app = ActionCable.server
- identifier = JSON.generate(channel: 'EchoChannel')
+ identifier = JSON.generate(channel: "ClientTest::EchoChannel")
- c = faye_client(port)
- assert_equal({"type" => "welcome"}, c.read_message)
- c.send_message command: 'subscribe', identifier: identifier
- assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
+ 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))
- channel = app.connections.first.subscriptions.send(:subscriptions).first[1]
- channel.expects(:unsubscribed)
- c.close
- sleep 0.1 # Data takes a moment to process
+ 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)
@@ -238,14 +301,14 @@ class ClientTest < ActionCable::TestCase
def test_server_restart
with_puma_server do |port|
- c = faye_client(port)
- assert_equal({"type" => "welcome"}, c.read_message)
- c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
- assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
+ 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 c.closed?
+ assert_predicate c, :closed?
end
end
end
diff --git a/actioncable/test/connection/authorization_test.rb b/actioncable/test/connection/authorization_test.rb
index a0506cb9c0..ac5c128135 100644
--- a/actioncable/test/connection/authorization_test.rb
+++ b/actioncable/test/connection/authorization_test.rb
@@ -1,5 +1,7 @@
-require 'test_helper'
-require 'stubs/test_server'
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_server"
class ActionCable::Connection::AuthorizationTest < ActionCable::TestCase
class Connection < ActionCable::Connection::Base
@@ -19,13 +21,16 @@ class ActionCable::Connection::AuthorizationTest < ActionCable::TestCase
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'
+ 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)
- connection.websocket.expects(:close)
- connection.process
+ assert_called_with(connection.websocket, :transmit, [{ type: "disconnect", reason: "unauthorized", reconnect: false }.to_json]) do
+ assert_called(connection.websocket, :close) do
+ connection.process
+ end
+ end
end
end
end
diff --git a/actioncable/test/connection/base_test.rb b/actioncable/test/connection/base_test.rb
index d7e1041e68..299879ad4c 100644
--- a/actioncable/test/connection/base_test.rb
+++ b/actioncable/test/connection/base_test.rb
@@ -1,6 +1,8 @@
-require 'test_helper'
-require 'stubs/test_server'
-require 'active_support/core_ext/object/json'
+# 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
@@ -37,10 +39,10 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
connection = open_connection
connection.process
- assert connection.websocket.possible?
+ assert_predicate connection.websocket, :possible?
wait_for_async
- assert connection.websocket.alive?
+ assert_predicate connection.websocket, :alive?
end
end
@@ -57,11 +59,12 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
run_in_eventmachine do
connection = open_connection
- connection.websocket.expects(:transmit).with({ type: "welcome" }.to_json)
- connection.message_buffer.expects(:process!)
-
- connection.process
- wait_for_async
+ assert_called_with(connection.websocket, :transmit, [{ type: "welcome" }.to_json]) do
+ assert_called(connection.message_buffer, :process!) do
+ connection.process
+ wait_for_async
+ end
+ end
assert_equal [ connection ], @server.connections
assert connection.connected
@@ -74,14 +77,14 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
connection.process
# Setup the connection
- connection.server.stubs(:timer).returns(true)
connection.send :handle_open
assert connection.connected
- connection.subscriptions.expects(:unsubscribe_from_all)
- connection.send :handle_close
+ assert_called(connection.subscriptions, :unsubscribe_from_all) do
+ connection.send :handle_close
+ end
- assert ! connection.connected
+ assert_not connection.connected
assert_equal [], @server.connections
end
end
@@ -93,7 +96,7 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
statistics = connection.statistics
- assert statistics[:identifier].blank?
+ assert_predicate statistics[:identifier], :blank?
assert_kind_of Time, statistics[:started_at]
assert_equal [], statistics[:subscriptions]
end
@@ -104,8 +107,9 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
connection = open_connection
connection.process
- connection.websocket.expects(:close)
- connection.close
+ assert_called(connection.websocket, :close) do
+ connection.close(reason: "testing")
+ end
end
end
@@ -113,14 +117,14 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
run_in_eventmachine do
class CallMeMaybe
def call(*)
- raise 'Do not call me!'
+ 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 }
+ "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)
@@ -131,8 +135,8 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
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::MockRequest.env_for "/test", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket",
+ "HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.com"
Connection.new(@server, env)
end
diff --git a/actioncable/test/connection/client_socket_test.rb b/actioncable/test/connection/client_socket_test.rb
index dd730e348f..1ab3c3b71d 100644
--- a/actioncable/test/connection/client_socket_test.rb
+++ b/actioncable/test/connection/client_socket_test.rb
@@ -1,10 +1,11 @@
-require 'test_helper'
-require 'stubs/test_server'
+# frozen_string_literal: true
-class ActionCable::Connection::StreamTest < ActionCable::TestCase
+require "test_helper"
+require "stubs/test_server"
+
+class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase
class Connection < ActionCable::Connection::Base
- attr_reader :websocket, :subscriptions, :message_buffer, :connected
- attr_reader :errors
+ attr_reader :connected, :websocket, :errors
def initialize(*)
super
@@ -33,31 +34,58 @@ class ActionCable::Connection::StreamTest < ActionCable::TestCase
@server.config.allowed_request_origins = %w( http://rubyonrails.com )
end
- test 'delegate socket errors to on_error handler' do
- skip if ENV['FAYE'].present?
-
+ 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').expects(:write).raises('foo')
- client.expects(:client_gone).never
-
- client.write('boo')
+ 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(reason: "testing")
+ 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'
- env['rack.hijack'] = -> { env['rack.hijack_io'] = StringIO.new }
+ 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
diff --git a/actioncable/test/connection/cross_site_forgery_test.rb b/actioncable/test/connection/cross_site_forgery_test.rb
index 2d516b0533..3e21138ffc 100644
--- a/actioncable/test/connection/cross_site_forgery_test.rb
+++ b/actioncable/test/connection/cross_site_forgery_test.rb
@@ -1,8 +1,10 @@
-require 'test_helper'
-require 'stubs/test_server'
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_server"
class ActionCable::Connection::CrossSiteForgeryTest < ActionCable::TestCase
- HOST = 'rubyonrails.com'
+ HOST = "rubyonrails.com"
class Connection < ActionCable::Connection::Base
def send_async(method, *args)
@@ -13,44 +15,53 @@ class ActionCable::Connection::CrossSiteForgeryTest < ActionCable::TestCase
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'
+ 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'
+ @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'
+ 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'
+ 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'
+ @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
@@ -75,7 +86,7 @@ class ActionCable::Connection::CrossSiteForgeryTest < ActionCable::TestCase
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
+ 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
index b48d9af809..707f4bab72 100644
--- a/actioncable/test/connection/identifier_test.rb
+++ b/actioncable/test/connection/identifier_test.rb
@@ -1,6 +1,8 @@
-require 'test_helper'
-require 'stubs/test_server'
-require 'stubs/user'
+# 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
@@ -16,53 +18,53 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase
test "connection identifier" do
run_in_eventmachine do
- open_connection_with_stubbed_pubsub
+ 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
- pubsub = mock('pubsub_adapter')
- pubsub.expects(:subscribe).with('action_cable/User#lifo', kind_of(Proc))
- pubsub.expects(:unsubscribe).with('action_cable/User#lifo', kind_of(Proc))
-
server = TestServer.new
- server.stubs(:pubsub).returns(pubsub)
- open_connection server: server
+ 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_with_stubbed_pubsub
+ open_connection
- @connection.websocket.expects(:close)
- @connection.process_internal_message 'type' => 'disconnect'
+ assert_called(@connection.websocket, :close) do
+ @connection.process_internal_message "type" => "disconnect"
+ end
end
end
test "processing invalid message" do
run_in_eventmachine do
- open_connection_with_stubbed_pubsub
+ open_connection
- @connection.websocket.expects(:close).never
- @connection.process_internal_message 'type' => 'unknown'
+ assert_not_called(@connection.websocket, :close) do
+ @connection.process_internal_message "type" => "unknown"
+ end
end
end
- protected
- def open_connection_with_stubbed_pubsub
- server = TestServer.new
- server.stubs(:adapter).returns(stub_everything('adapter'))
-
- open_connection server: server
- end
+ private
+ def open_connection(server = nil)
+ server ||= TestServer.new
- def open_connection(server:)
- env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
+ env = Rack::MockRequest.env_for "/test", "HTTP_HOST" => "localhost", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket"
@connection = Connection.new(server, env)
@connection.process
diff --git a/actioncable/test/connection/multiple_identifiers_test.rb b/actioncable/test/connection/multiple_identifiers_test.rb
index 484e73bb30..51716410b2 100644
--- a/actioncable/test/connection/multiple_identifiers_test.rb
+++ b/actioncable/test/connection/multiple_identifiers_test.rb
@@ -1,6 +1,8 @@
-require 'test_helper'
-require 'stubs/test_server'
-require 'stubs/user'
+# 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
@@ -14,28 +16,19 @@ class ActionCable::Connection::MultipleIdentifiersTest < ActionCable::TestCase
test "multiple connection identifiers" do
run_in_eventmachine do
- open_connection_with_stubbed_pubsub
+ open_connection
+
assert_equal "Room#my-room:User#lifo", @connection.connection_identifier
end
end
- protected
- def open_connection_with_stubbed_pubsub
+ private
+ def open_connection
server = TestServer.new
- server.stubs(:pubsub).returns(stub_everything('pubsub'))
-
- open_connection server: server
- end
-
- def open_connection(server:)
- env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
+ env = Rack::MockRequest.env_for "/test", "HTTP_HOST" => "localhost", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket"
@connection = Connection.new(server, env)
@connection.process
@connection.send :handle_open
end
-
- def close_connection
- @connection.send :handle_close
- end
end
diff --git a/actioncable/test/connection/stream_test.rb b/actioncable/test/connection/stream_test.rb
index d5aad63648..0f4576db40 100644
--- a/actioncable/test/connection/stream_test.rb
+++ b/actioncable/test/connection/stream_test.rb
@@ -1,10 +1,12 @@
-require 'test_helper'
-require 'stubs/test_server'
+# 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 :websocket, :subscriptions, :message_buffer, :connected
- attr_reader :errors
+ attr_reader :connected, :websocket, :errors
def initialize(*)
super
@@ -35,17 +37,17 @@ class ActionCable::Connection::StreamTest < ActionCable::TestCase
[ EOFError, Errno::ECONNRESET ].each do |closed_exception|
test "closes socket on #{closed_exception}" do
- skip if ENV['FAYE'].present?
-
run_in_eventmachine do
connection = open_connection
# Internal hax = :(
client = connection.websocket.send(:websocket)
- client.instance_variable_get('@stream').instance_variable_get('@rack_hijack_io').expects(:write).raises(closed_exception, 'foo')
- client.expects(:client_gone)
-
- client.write('boo')
+ 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
@@ -53,10 +55,10 @@ class ActionCable::Connection::StreamTest < ActionCable::TestCase
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 }
+ 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
diff --git a/actioncable/test/connection/string_identifier_test.rb b/actioncable/test/connection/string_identifier_test.rb
index eca0c31060..f7019b926a 100644
--- a/actioncable/test/connection/string_identifier_test.rb
+++ b/actioncable/test/connection/string_identifier_test.rb
@@ -1,5 +1,7 @@
-require 'test_helper'
-require 'stubs/test_server'
+# frozen_string_literal: true
+
+require "test_helper"
+require "stubs/test_server"
class ActionCable::Connection::StringIdentifierTest < ActionCable::TestCase
class Connection < ActionCable::Connection::Base
@@ -16,28 +18,19 @@ class ActionCable::Connection::StringIdentifierTest < ActionCable::TestCase
test "connection identifier" do
run_in_eventmachine do
- open_connection_with_stubbed_pubsub
+ open_connection
+
assert_equal "random-string", @connection.connection_identifier
end
end
- protected
- def open_connection_with_stubbed_pubsub
- @server = TestServer.new
- @server.stubs(:pubsub).returns(stub_everything('pubsub'))
-
- open_connection
- end
-
+ private
def open_connection
- env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
- @connection = Connection.new(@server, env)
+ 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
-
- def close_connection
- @connection.send :on_close
- end
end
diff --git a/actioncable/test/connection/subscriptions_test.rb b/actioncable/test/connection/subscriptions_test.rb
index 53e8547245..902085c5d6 100644
--- a/actioncable/test/connection/subscriptions_test.rb
+++ b/actioncable/test/connection/subscriptions_test.rb
@@ -1,4 +1,6 @@
-require 'test_helper'
+# frozen_string_literal: true
+
+require "test_helper"
class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
class Connection < ActionCable::Connection::Base
@@ -24,9 +26,8 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
setup do
@server = TestServer.new
- @server.stubs(:channel_classes).returns(ChatChannel.name => ChatChannel)
- @chat_identifier = ActiveSupport::JSON.encode(id: 1, channel: 'ActionCable::Connection::SubscriptionsTest::ChatChannel')
+ @chat_identifier = ActiveSupport::JSON.encode(id: 1, channel: "ActionCable::Connection::SubscriptionsTest::ChatChannel")
end
test "subscribe command" do
@@ -43,8 +44,8 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
run_in_eventmachine do
setup_connection
- @subscriptions.execute_command 'command' => 'subscribe'
- assert @subscriptions.identifiers.empty?
+ @subscriptions.execute_command "command" => "subscribe"
+ assert_empty @subscriptions.identifiers
end
end
@@ -54,10 +55,12 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
subscribe_to_chat_channel
channel = subscribe_to_chat_channel
- channel.expects(:unsubscribe_from_channel)
- @subscriptions.execute_command 'command' => 'unsubscribe', 'identifier' => @chat_identifier
- assert @subscriptions.identifiers.empty?
+ assert_called(channel, :unsubscribe_from_channel) do
+ @subscriptions.execute_command "command" => "unsubscribe", "identifier" => @chat_identifier
+ end
+
+ assert_empty @subscriptions.identifiers
end
end
@@ -65,8 +68,8 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
run_in_eventmachine do
setup_connection
- @subscriptions.execute_command 'command' => 'unsubscribe'
- assert @subscriptions.identifiers.empty?
+ @subscriptions.execute_command "command" => "unsubscribe"
+ assert_empty @subscriptions.identifiers
end
end
@@ -75,8 +78,8 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
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)
+ 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
@@ -88,26 +91,27 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
channel1 = subscribe_to_chat_channel
- channel2_id = ActiveSupport::JSON.encode(id: 2, channel: 'ActionCable::Connection::SubscriptionsTest::ChatChannel')
+ channel2_id = ActiveSupport::JSON.encode(id: 2, channel: "ActionCable::Connection::SubscriptionsTest::ChatChannel")
channel2 = subscribe_to_chat_channel(channel2_id)
- channel1.expects(:unsubscribe_from_channel)
- channel2.expects(:unsubscribe_from_channel)
-
- @subscriptions.unsubscribe_from_all
+ assert_called(channel1, :unsubscribe_from_channel) do
+ assert_called(channel2, :unsubscribe_from_channel) do
+ @subscriptions.unsubscribe_from_all
+ end
+ end
end
end
private
def subscribe_to_chat_channel(identifier = @chat_identifier)
- @subscriptions.execute_command 'command' => 'subscribe', 'identifier' => identifier
+ @subscriptions.execute_command "command" => "subscribe", "identifier" => identifier
assert_equal identifier, @subscriptions.identifiers.last
- @subscriptions.send :find, 'identifier' => identifier
+ @subscriptions.send :find, "identifier" => identifier
end
def setup_connection
- env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
+ env = Rack::MockRequest.env_for "/test", "HTTP_HOST" => "localhost", "HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket"
@connection = Connection.new(@server, env)
@subscriptions = ActionCable::Connection::Subscriptions.new(@connection)
diff --git a/actioncable/test/connection/test_case_test.rb b/actioncable/test/connection/test_case_test.rb
new file mode 100644
index 0000000000..3b19465d7b
--- /dev/null
+++ b/actioncable/test/connection/test_case_test.rb
@@ -0,0 +1,197 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class SimpleConnection < ActionCable::Connection::Base
+ identified_by :user_id
+
+ class << self
+ attr_accessor :disconnected_user_id
+ end
+
+ def connect
+ self.user_id = request.params[:user_id] || cookies[:user_id]
+ end
+
+ def disconnect
+ self.class.disconnected_user_id = user_id
+ end
+end
+
+class ConnectionSimpleTest < ActionCable::Connection::TestCase
+ tests SimpleConnection
+
+ def test_connected
+ connect
+
+ assert_nil connection.user_id
+ end
+
+ def test_url_params
+ connect "/cable?user_id=323"
+
+ assert_equal "323", connection.user_id
+ end
+
+ def test_params
+ connect params: { user_id: 323 }
+
+ assert_equal "323", connection.user_id
+ end
+
+ def test_plain_cookie
+ cookies["user_id"] = "456"
+
+ connect
+
+ assert_equal "456", connection.user_id
+ end
+
+ def test_disconnect
+ cookies["user_id"] = "456"
+
+ connect
+
+ assert_equal "456", connection.user_id
+
+ disconnect
+
+ assert_equal "456", SimpleConnection.disconnected_user_id
+ end
+end
+
+class Connection < ActionCable::Connection::Base
+ identified_by :current_user_id
+ identified_by :token
+
+ class << self
+ attr_accessor :disconnected_user_id
+ end
+
+ def connect
+ self.current_user_id = verify_user
+ self.token = request.headers["X-API-TOKEN"]
+ logger.add_tags("ActionCable")
+ end
+
+ private
+ def verify_user
+ cookies.signed[:user_id].presence || reject_unauthorized_connection
+ end
+end
+
+class ConnectionTest < ActionCable::Connection::TestCase
+ def test_connected_with_signed_cookies_and_headers
+ cookies.signed["user_id"] = "456"
+
+ connect headers: { "X-API-TOKEN" => "abc" }
+
+ assert_equal "abc", connection.token
+ assert_equal "456", connection.current_user_id
+ end
+
+ def test_connected_when_no_signed_cookies_set
+ cookies["user_id"] = "456"
+
+ assert_reject_connection { connect }
+ end
+
+ def test_connection_rejected
+ assert_reject_connection { connect }
+ end
+
+ def test_connection_rejected_assertion_message
+ error = assert_raises Minitest::Assertion do
+ assert_reject_connection { "Intentionally doesn't connect." }
+ end
+
+ assert_match(/Expected to reject connection/, error.message)
+ end
+end
+
+class EncryptedCookiesConnection < ActionCable::Connection::Base
+ identified_by :user_id
+
+ def connect
+ self.user_id = verify_user
+ end
+
+ private
+ def verify_user
+ cookies.encrypted[:user_id].presence || reject_unauthorized_connection
+ end
+end
+
+class EncryptedCookiesConnectionTest < ActionCable::Connection::TestCase
+ tests EncryptedCookiesConnection
+
+ def test_connected_with_encrypted_cookies
+ cookies.encrypted["user_id"] = "456"
+
+ connect
+
+ assert_equal "456", connection.user_id
+ end
+
+ def test_connection_rejected
+ assert_reject_connection { connect }
+ end
+end
+
+class SessionConnection < ActionCable::Connection::Base
+ identified_by :user_id
+
+ def connect
+ self.user_id = verify_user
+ end
+
+ private
+ def verify_user
+ request.session[:user_id].presence || reject_unauthorized_connection
+ end
+end
+
+class SessionConnectionTest < ActionCable::Connection::TestCase
+ tests SessionConnection
+
+ def test_connected_with_encrypted_cookies
+ connect session: { user_id: "789" }
+ assert_equal "789", connection.user_id
+ end
+
+ def test_connection_rejected
+ assert_reject_connection { connect }
+ end
+end
+
+class EnvConnection < ActionCable::Connection::Base
+ identified_by :user
+
+ def connect
+ self.user = verify_user
+ end
+
+ private
+ def verify_user
+ # Warden-like authentication
+ env["authenticator"]&.user || reject_unauthorized_connection
+ end
+end
+
+class EnvConnectionTest < ActionCable::Connection::TestCase
+ tests EnvConnection
+
+ def test_connected_with_env
+ authenticator = Class.new do
+ def user; "David"; end
+ end
+
+ connect env: { "authenticator" => authenticator.new }
+
+ assert_equal "David", connection.user
+ end
+
+ def test_connection_rejected
+ assert_reject_connection { connect }
+ end
+end
diff --git a/actioncable/test/javascript/src/test.js b/actioncable/test/javascript/src/test.js
new file mode 100644
index 0000000000..eea1c0a408
--- /dev/null
+++ b/actioncable/test/javascript/src/test.js
@@ -0,0 +1,6 @@
+import "./test_helpers/index"
+import "./unit/action_cable_test"
+import "./unit/connection_test"
+import "./unit/consumer_test"
+import "./unit/subscription_test"
+import "./unit/subscriptions_test"
diff --git a/actioncable/test/javascript/src/test_helpers/consumer_test_helper.js b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.js
new file mode 100644
index 0000000000..d1dabc9fc4
--- /dev/null
+++ b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.js
@@ -0,0 +1,58 @@
+import { WebSocket as MockWebSocket, Server as MockServer } from "mock-socket"
+import * as ActionCable from "../../../../app/javascript/action_cable/index"
+import {defer, testURL} from "./index"
+
+export default function(name, options, callback) {
+ if (options == null) { options = {} }
+ if (callback == null) {
+ callback = options
+ options = {}
+ }
+
+ if (options.url == null) { options.url = testURL }
+
+ return QUnit.test(name, function(assert) {
+ const doneAsync = assert.async()
+
+ ActionCable.adapters.WebSocket = MockWebSocket
+ const server = new MockServer(options.url)
+ const consumer = ActionCable.createConsumer(options.url)
+
+ server.on("connection", function() {
+ const clients = server.clients()
+ assert.equal(clients.length, 1)
+ assert.equal(clients[0].readyState, WebSocket.OPEN)
+ })
+
+ server.broadcastTo = function(subscription, data, callback) {
+ if (data == null) { data = {} }
+ 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))
+ defer(callback)
+ }
+
+ const done = function() {
+ consumer.disconnect()
+ server.close()
+ doneAsync()
+ }
+
+ const testData = {assert, consumer, server, done}
+
+ if (options.connect === false) {
+ callback(testData)
+ } else {
+ server.on("connection", function() {
+ testData.client = server.clients()[0]
+ callback(testData)
+ })
+ consumer.connect()
+ }
+ })
+}
diff --git a/actioncable/test/javascript/src/test_helpers/index.js b/actioncable/test/javascript/src/test_helpers/index.js
new file mode 100644
index 0000000000..0cd4e260b3
--- /dev/null
+++ b/actioncable/test/javascript/src/test_helpers/index.js
@@ -0,0 +1,10 @@
+import * as ActionCable from "../../../../app/javascript/action_cable/index"
+
+export const testURL = "ws://cable.example.com/"
+
+export function defer(callback) {
+ setTimeout(callback, 1)
+}
+
+const originalWebSocket = ActionCable.adapters.WebSocket
+QUnit.testDone(() => ActionCable.adapters.WebSocket = originalWebSocket)
diff --git a/actioncable/test/javascript/src/unit/action_cable_test.js b/actioncable/test/javascript/src/unit/action_cable_test.js
new file mode 100644
index 0000000000..c46f9878d2
--- /dev/null
+++ b/actioncable/test/javascript/src/unit/action_cable_test.js
@@ -0,0 +1,57 @@
+import * as ActionCable from "../../../../app/javascript/action_cable/index"
+import {testURL} from "../test_helpers/index"
+
+const {module, test} = QUnit
+
+module("ActionCable", () => {
+ module("Adapters", () => {
+ module("WebSocket", () => {
+ test("default is self.WebSocket", assert => {
+ assert.equal(ActionCable.adapters.WebSocket, self.WebSocket)
+ })
+ })
+
+ module("logger", () => {
+ test("default is self.console", assert => {
+ assert.equal(ActionCable.adapters.logger, self.console)
+ })
+ })
+ })
+
+ module("#createConsumer", () => {
+ test("uses specified URL", assert => {
+ const consumer = ActionCable.createConsumer(testURL)
+ assert.equal(consumer.url, testURL)
+ })
+
+ test("uses default URL", assert => {
+ const pattern = new RegExp(`${ActionCable.INTERNAL.default_mount_path}$`)
+ const consumer = ActionCable.createConsumer()
+ assert.ok(pattern.test(consumer.url), `Expected ${consumer.url} to match ${pattern}`)
+ })
+
+ test("uses URL from meta tag", assert => {
+ const element = document.createElement("meta")
+ element.setAttribute("name", "action-cable-url")
+ element.setAttribute("content", testURL)
+
+ document.head.appendChild(element)
+ const consumer = ActionCable.createConsumer()
+ document.head.removeChild(element)
+
+ assert.equal(consumer.url, testURL)
+ })
+
+ test("dynamically computes URL from function", assert => {
+ let dynamicURL = testURL
+ const generateURL = () => {
+ return dynamicURL
+ }
+ const consumer = ActionCable.createConsumer(generateURL)
+ assert.equal(consumer.url, testURL)
+
+ dynamicURL = `${testURL}foo`
+ assert.equal(consumer.url, `${testURL}foo`)
+ })
+ })
+})
diff --git a/actioncable/test/javascript/src/unit/connection_test.js b/actioncable/test/javascript/src/unit/connection_test.js
new file mode 100644
index 0000000000..9b1a975bfb
--- /dev/null
+++ b/actioncable/test/javascript/src/unit/connection_test.js
@@ -0,0 +1,28 @@
+import * as ActionCable from "../../../../app/javascript/action_cable/index"
+
+const {module, test} = QUnit
+
+module("ActionCable.Connection", () => {
+ module("#getState", () => {
+ test("uses the configured WebSocket adapter", assert => {
+ ActionCable.adapters.WebSocket = { foo: 1, BAR: "42" }
+ const connection = new ActionCable.Connection({})
+ connection.webSocket = {}
+ connection.webSocket.readyState = 1
+ assert.equal(connection.getState(), "foo")
+ connection.webSocket.readyState = "42"
+ assert.equal(connection.getState(), "bar")
+ })
+ })
+
+ module("#open", () => {
+ test("uses the configured WebSocket adapter", assert => {
+ const FakeWebSocket = function() {}
+ ActionCable.adapters.WebSocket = FakeWebSocket
+ const connection = new ActionCable.Connection({})
+ connection.monitor = { start() {} }
+ connection.open()
+ assert.equal(connection.webSocket instanceof FakeWebSocket, true)
+ })
+ })
+})
diff --git a/actioncable/test/javascript/src/unit/consumer_test.js b/actioncable/test/javascript/src/unit/consumer_test.js
new file mode 100644
index 0000000000..acc618bf0c
--- /dev/null
+++ b/actioncable/test/javascript/src/unit/consumer_test.js
@@ -0,0 +1,19 @@
+import consumerTest from "../test_helpers/consumer_test_helper"
+
+const {module} = QUnit
+
+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.js b/actioncable/test/javascript/src/unit/subscription_test.js
new file mode 100644
index 0000000000..bf32e5f27d
--- /dev/null
+++ b/actioncable/test/javascript/src/unit/subscription_test.js
@@ -0,0 +1,54 @@
+import consumerTest from "../test_helpers/consumer_test_helper"
+
+const {module} = QUnit
+
+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}) => {
+ const subscription = consumer.subscriptions.create("chat", {
+ connected() {
+ assert.ok(true)
+ done()
+ }
+ })
+
+ server.broadcastTo(subscription, {message_type: "confirmation"})
+ })
+
+ consumerTest("#disconnected callback", ({server, consumer, assert, done}) => {
+ const subscription = consumer.subscriptions.create("chat", {
+ disconnected() {
+ assert.ok(true)
+ done()
+ }
+ })
+
+ server.broadcastTo(subscription, {message_type: "confirmation"}, () => server.close())
+ })
+
+ consumerTest("#perform", ({consumer, server, assert, done}) => {
+ const subscription = consumer.subscriptions.create("chat", {
+ connected() {
+ this.perform({publish: "hi"})
+ }
+ })
+
+ server.on("message", (message) => {
+ const 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.js b/actioncable/test/javascript/src/unit/subscriptions_test.js
new file mode 100644
index 0000000000..33af5d4d82
--- /dev/null
+++ b/actioncable/test/javascript/src/unit/subscriptions_test.js
@@ -0,0 +1,31 @@
+import consumerTest from "../test_helpers/consumer_test_helper"
+
+const {module} = QUnit
+
+module("ActionCable.Subscriptions", () => {
+ consumerTest("create subscription with channel string", ({consumer, server, assert, done}) => {
+ const channel = "chat"
+
+ server.on("message", (message) => {
+ const 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}) => {
+ const channel = {channel: "chat", room: "action"}
+
+ server.on("message", (message) => {
+ const 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/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
index 3b4a7eaf90..860e79b821 100644
--- a/actioncable/test/server/broadcasting_test.rb
+++ b/actioncable/test/server/broadcasting_test.rb
@@ -1,10 +1,9 @@
-require "test_helper"
+# frozen_string_literal: true
-class BroadcastingTest < ActiveSupport::TestCase
- class TestServer
- include ActionCable::Server::Broadcasting
- end
+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
@@ -12,4 +11,48 @@ class BroadcastingTest < ActiveSupport::TestCase
assert_equal "test_queue", broadcaster.broadcasting
end
+
+ test "broadcast generates notification" do
+ 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
+
+ test "broadcaster from broadcaster_for generates notification" do
+ 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
diff --git a/actioncable/test/stubs/global_id.rb b/actioncable/test/stubs/global_id.rb
index 334f0d03e8..15fab6b8a7 100644
--- a/actioncable/test/stubs/global_id.rb
+++ b/actioncable/test/stubs/global_id.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class GlobalID
attr_reader :uri
delegate :to_param, :to_s, to: :uri
diff --git a/actioncable/test/stubs/room.rb b/actioncable/test/stubs/room.rb
index cd66a0b687..df7236f408 100644
--- a/actioncable/test/stubs/room.rb
+++ b/actioncable/test/stubs/room.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
class Room
attr_reader :id, :name
- def initialize(id, name='Campfire')
+ def initialize(id, name = "Campfire")
@id = id
@name = name
end
diff --git a/actioncable/test/stubs/test_adapter.rb b/actioncable/test/stubs/test_adapter.rb
index bbd142b287..3b25c9168f 100644
--- a/actioncable/test/stubs/test_adapter.rb
+++ b/actioncable/test/stubs/test_adapter.rb
@@ -1,10 +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
index 885450dda6..155c68e38c 100644
--- a/actioncable/test/stubs/test_connection.rb
+++ b/actioncable/test/stubs/test_connection.rb
@@ -1,7 +1,9 @@
-require 'stubs/user'
+# frozen_string_literal: true
+
+require "stubs/user"
class TestConnection
- attr_reader :identifiers, :logger, :current_user, :server, :transmissions
+ attr_reader :identifiers, :logger, :current_user, :server, :subscriptions, :transmissions
delegate :pubsub, to: :server
diff --git a/actioncable/test/stubs/test_server.rb b/actioncable/test/stubs/test_server.rb
index b86f422a13..0bc4625e28 100644
--- a/actioncable/test/stubs/test_server.rb
+++ b/actioncable/test/stubs/test_server.rb
@@ -1,4 +1,6 @@
-require 'ostruct'
+# frozen_string_literal: true
+
+require "ostruct"
class TestServer
include ActionCable::Server::Connections
@@ -10,14 +12,8 @@ class TestServer
@logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
@config = OpenStruct.new(log_tags: [], subscription_adapter: subscription_adapter)
- @config.use_faye = ENV['FAYE'].present?
- @config.client_socket_class = if @config.use_faye
- ActionCable::Connection::FayeClientSocket
- else
- ActionCable::Connection::ClientSocket
- end
-
- @mutex = Monitor.new
+
+ @mutex = Monitor.new
end
def pubsub
@@ -25,11 +21,9 @@ class TestServer
end
def event_loop
- @event_loop ||= if @config.use_faye
- ActionCable::Connection::FayeEventLoop.new
- else
- ActionCable::Connection::StreamEventLoop.new
- end
+ @event_loop ||= ActionCable::Connection::StreamEventLoop.new.tap do |loop|
+ loop.instance_variable_set(:@executor, Concurrent.global_io_executor)
+ end
end
def worker_pool
diff --git a/actioncable/test/stubs/user.rb b/actioncable/test/stubs/user.rb
index a66b4f87d5..7894d1d9ae 100644
--- a/actioncable/test/stubs/user.rb
+++ b/actioncable/test/stubs/user.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class User
attr_reader :name
diff --git a/actioncable/test/subscription_adapter/async_test.rb b/actioncable/test/subscription_adapter/async_test.rb
index 8f413f14c2..6e038259b5 100644
--- a/actioncable/test/subscription_adapter/async_test.rb
+++ b/actioncable/test/subscription_adapter/async_test.rb
@@ -1,5 +1,7 @@
-require 'test_helper'
-require_relative './common'
+# frozen_string_literal: true
+
+require "test_helper"
+require_relative "common"
class AsyncAdapterTest < ActionCable::TestCase
include CommonSubscriptionAdapterTest
@@ -12,6 +14,6 @@ class AsyncAdapterTest < ActionCable::TestCase
end
def cable_config
- { adapter: 'async' }
+ { adapter: "async" }
end
end
diff --git a/actioncable/test/subscription_adapter/base_test.rb b/actioncable/test/subscription_adapter/base_test.rb
index 256dce673f..999dc0cba1 100644
--- a/actioncable/test/subscription_adapter/base_test.rb
+++ b/actioncable/test/subscription_adapter/base_test.rb
@@ -1,5 +1,7 @@
-require 'test_helper'
-require 'stubs/test_server'
+# 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
@@ -15,59 +17,49 @@ class ActionCable::SubscriptionAdapter::BaseTest < ActionCable::TestCase
test "#broadcast returns NotImplementedError by default" do
assert_raises NotImplementedError do
- BrokenAdapter.new(@server).broadcast('channel', 'payload')
+ BrokenAdapter.new(@server).broadcast("channel", "payload")
end
end
test "#subscribe returns NotImplementedError by default" do
- callback = lambda { puts 'callback' }
- success_callback = lambda { puts 'success' }
+ callback = lambda { puts "callback" }
+ success_callback = lambda { puts "success" }
assert_raises NotImplementedError do
- BrokenAdapter.new(@server).subscribe('channel', callback, success_callback)
+ BrokenAdapter.new(@server).subscribe("channel", callback, success_callback)
end
end
test "#unsubscribe returns NotImplementedError by default" do
- callback = lambda { puts 'callback' }
+ callback = lambda { puts "callback" }
assert_raises NotImplementedError do
- BrokenAdapter.new(@server).unsubscribe('channel', callback)
+ 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
- broadcast = SuccessAdapter.new(@server).broadcast('channel', 'payload')
-
- assert_respond_to(SuccessAdapter.new(@server), :broadcast)
-
assert_nothing_raised do
- broadcast
+ SuccessAdapter.new(@server).broadcast("channel", "payload")
end
end
test "#subscribe is implemented" do
- callback = lambda { puts 'callback' }
- success_callback = lambda { puts 'success' }
- subscribe = SuccessAdapter.new(@server).subscribe('channel', callback, success_callback)
-
- assert_respond_to(SuccessAdapter.new(@server), :subscribe)
+ callback = lambda { puts "callback" }
+ success_callback = lambda { puts "success" }
assert_nothing_raised do
- subscribe
+ SuccessAdapter.new(@server).subscribe("channel", callback, success_callback)
end
end
test "#unsubscribe is implemented" do
- callback = lambda { puts 'callback' }
- unsubscribe = SuccessAdapter.new(@server).unsubscribe('channel', callback)
-
- assert_respond_to(SuccessAdapter.new(@server), :unsubscribe)
+ callback = lambda { puts "callback" }
assert_nothing_raised do
- unsubscribe
+ 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..475e6cfd3a
--- /dev/null
+++ b/actioncable/test/subscription_adapter/channel_prefix.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+module ChannelPrefixTest
+ def test_channel_prefix
+ server2 = ActionCable::Server::Base.new(config: ActionCable::Server::Configuration.new)
+ server2.config.cable = alt_cable_config
+ server2.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
+
+ 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
index 285c690df0..b3e9ae9d5c 100644
--- a/actioncable/test/subscription_adapter/common.rb
+++ b/actioncable/test/subscription_adapter/common.rb
@@ -1,8 +1,10 @@
-require 'test_helper'
-require 'concurrent'
+# frozen_string_literal: true
-require 'active_support/core_ext/hash/indifferent_access'
-require 'pathname'
+require "test_helper"
+require "concurrent"
+
+require "active_support/core_ext/hash/indifferent_access"
+require "pathname"
module CommonSubscriptionAdapterTest
WAIT_WHEN_EXPECTING_EVENT = 3
@@ -11,7 +13,7 @@ module CommonSubscriptionAdapterTest
def setup
server = ActionCable::Server::Base.new
server.config.cable = cable_config.with_indifferent_access
- server.config.use_faye = ENV['FAYE'].present?
+ server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
adapter_klass = server.config.pubsub_adapter
@@ -20,10 +22,9 @@ module CommonSubscriptionAdapterTest
end
def teardown
- [@rx_adapter, @tx_adapter].uniq.each(&:shutdown)
+ [@rx_adapter, @tx_adapter].uniq.compact.each(&:shutdown)
end
-
def subscribe_as_queue(channel, adapter = @rx_adapter)
queue = Queue.new
@@ -31,7 +32,7 @@ module CommonSubscriptionAdapterTest
subscribed = Concurrent::Event.new
adapter.subscribe(channel, callback, Proc.new { subscribed.set })
subscribed.wait(WAIT_WHEN_EXPECTING_EVENT)
- assert subscribed.set?
+ assert_predicate subscribed, :set?
yield queue
@@ -41,77 +42,90 @@ module CommonSubscriptionAdapterTest
adapter.unsubscribe(channel, callback) if subscribed.set?
end
-
def test_subscribe_and_unsubscribe
- subscribe_as_queue('channel') do |queue|
+ subscribe_as_queue("channel") do |queue|
end
end
def test_basic_broadcast
- subscribe_as_queue('channel') do |queue|
- @tx_adapter.broadcast('channel', 'hello world')
+ subscribe_as_queue("channel") do |queue|
+ @tx_adapter.broadcast("channel", "hello world")
- assert_equal 'hello world', queue.pop
+ assert_equal "hello world", queue.pop
end
end
def test_broadcast_after_unsubscribe
keep_queue = nil
- subscribe_as_queue('channel') do |queue|
+ subscribe_as_queue("channel") do |queue|
keep_queue = queue
- @tx_adapter.broadcast('channel', 'hello world')
+ @tx_adapter.broadcast("channel", "hello world")
- assert_equal 'hello world', queue.pop
+ assert_equal "hello world", queue.pop
end
- @tx_adapter.broadcast('channel', 'hello void')
+ @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')
+ 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
+ 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')
+ subscribe_as_queue("channel") do |queue|
+ subscribe_as_queue("channel") do |queue_2|
+ @tx_adapter.broadcast("channel", "hello")
- assert_equal 'hello', queue_2.pop
+ assert_equal "hello", queue_2.pop
end
- assert_equal 'hello', queue.pop
+ 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')
+ 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
+ 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')
+ subscribe_as_queue("channel") do |queue|
+ @tx_adapter.broadcast("other channel", "one")
+ @tx_adapter.broadcast("channel", "two")
+
+ assert_equal "two", queue.pop
+ end
+ end
- assert_equal 'two', queue.pop
+ 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/evented_redis_test.rb b/actioncable/test/subscription_adapter/evented_redis_test.rb
deleted file mode 100644
index 6d20e6ed78..0000000000
--- a/actioncable/test/subscription_adapter/evented_redis_test.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'test_helper'
-require_relative './common'
-
-class EventedRedisAdapterTest < ActionCable::TestCase
- include CommonSubscriptionAdapterTest
-
- def setup
- super
-
- # em-hiredis is warning-rich
- @previous_verbose, $VERBOSE = $VERBOSE, nil
- end
-
- def teardown
- $VERBOSE = @previous_verbose
- end
-
- def cable_config
- { adapter: 'evented_redis', url: 'redis://127.0.0.1:6379/12' }
- end
-end
diff --git a/actioncable/test/subscription_adapter/inline_test.rb b/actioncable/test/subscription_adapter/inline_test.rb
index 75ea51e6b3..6305626b2b 100644
--- a/actioncable/test/subscription_adapter/inline_test.rb
+++ b/actioncable/test/subscription_adapter/inline_test.rb
@@ -1,5 +1,7 @@
-require 'test_helper'
-require_relative './common'
+# frozen_string_literal: true
+
+require "test_helper"
+require_relative "common"
class InlineAdapterTest < ActionCable::TestCase
include CommonSubscriptionAdapterTest
@@ -12,6 +14,6 @@ class InlineAdapterTest < ActionCable::TestCase
end
def cable_config
- { adapter: 'inline' }
+ { adapter: "inline" }
end
end
diff --git a/actioncable/test/subscription_adapter/postgresql_test.rb b/actioncable/test/subscription_adapter/postgresql_test.rb
index 214352a0b2..4348eb1b1e 100644
--- a/actioncable/test/subscription_adapter/postgresql_test.rb
+++ b/actioncable/test/subscription_adapter/postgresql_test.rb
@@ -1,18 +1,22 @@
-require 'test_helper'
-require_relative './common'
+# frozen_string_literal: true
-require 'active_record'
+require "test_helper"
+require_relative "common"
+require_relative "channel_prefix"
+
+require "active_record"
class PostgresqlAdapterTest < ActionCable::TestCase
include CommonSubscriptionAdapterTest
+ include ChannelPrefixTest
def setup
- database_config = { 'adapter' => 'postgresql', 'database' => 'activerecord_unittest' }
- ar_tests = File.expand_path('../../../activerecord/test', __dir__)
+ 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['arunit']
+ 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
@@ -35,6 +39,29 @@ class PostgresqlAdapterTest < ActionCable::TestCase
end
def cable_config
- { adapter: 'postgresql' }
+ { 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
index 4f34dd86c9..35840a4036 100644
--- a/actioncable/test/subscription_adapter/redis_test.rb
+++ b/actioncable/test/subscription_adapter/redis_test.rb
@@ -1,16 +1,56 @@
-require 'test_helper'
-require_relative './common'
+# 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', url: 'redis://127.0.0.1:6379/12' }
+ { adapter: "redis", driver: "ruby" }.tap do |x|
+ if host = URI(ENV["REDIS_URL"] || "").hostname
+ x[:host] = host
+ end
+ end
end
end
class RedisAdapterTest::Hiredis < RedisAdapterTest
def cable_config
- super.merge(driver: 'hiredis')
+ 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: URI(ENV["REDIS_URL"] || "").hostname || "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
index de1ee96770..c924f1e475 100644
--- a/actioncable/test/test_helper.rb
+++ b/actioncable/test/test_helper.rb
@@ -1,70 +1,43 @@
-require 'action_cable'
-require 'active_support/testing/autorun'
+# frozen_string_literal: true
-require 'puma'
-require 'mocha/setup'
-require 'rack/mock'
+require "action_cable"
+require "active_support/testing/autorun"
+require "active_support/testing/method_call_assertions"
+
+require "puma"
+require "rack/mock"
begin
- require 'byebug'
+ require "byebug"
rescue LoadError
end
# Require all the stubs and models
-Dir[File.dirname(__FILE__) + '/stubs/*.rb'].each {|file| require file }
-
-if ENV['FAYE'].present?
- require 'faye/websocket'
- class << Faye::WebSocket
- remove_method :ensure_reactor_running
-
- # We don't want Faye to start the EM reactor in tests because it makes testing much harder.
- # We want to be able to start and stop EM loop in tests to make things simpler.
- def ensure_reactor_running
- # no-op
- end
- end
-end
+Dir[File.expand_path("stubs/*.rb", __dir__)].each { |file| require file }
-module EventMachineConcurrencyHelpers
- def wait_for_async
- EM.run_deferred_callbacks
- end
+# Set test adapter and logger
+ActionCable.server.config.cable = { "adapter" => "test" }
+ActionCable.server.config.logger = Logger.new(nil)
- def run_in_eventmachine
- failure = nil
- EM.run do
- begin
- yield
- rescue => ex
- failure = ex
- ensure
- wait_for_async
- EM.stop if EM.reactor_running?
- end
- end
- raise failure if failure
- end
-end
+class ActionCable::TestCase < ActiveSupport::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
-module ConcurrentRubyConcurrencyHelpers
def wait_for_async
- e = Concurrent.global_io_executor
- until e.completed_task_count == e.scheduled_task_count
- sleep 0.1
- end
+ wait_for_executor Concurrent.global_io_executor
end
def run_in_eventmachine
yield
wait_for_async
end
-end
-class ActionCable::TestCase < ActiveSupport::TestCase
- if ENV['FAYE'].present?
- include EventMachineConcurrencyHelpers
- else
- include ConcurrentRubyConcurrencyHelpers
+ 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..02eaefc4f8
--- /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 TransmittedDataTest < 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
index 654f49821e..f7dc428441 100644
--- a/actioncable/test/worker_test.rb
+++ b/actioncable/test/worker_test.rb
@@ -1,6 +1,8 @@
-require 'test_helper'
+# frozen_string_literal: true
-class WorkerTest < ActiveSupport::TestCase
+require "test_helper"
+
+class WorkerTest < ActionCable::TestCase
class Receiver
attr_accessor :last_action
@@ -9,7 +11,7 @@ class WorkerTest < ActiveSupport::TestCase
end
def process(message)
- @last_action = [ :process, message ]
+ @last_action = [ :process, message ]
end
def connection
@@ -33,22 +35,12 @@ class WorkerTest < ActiveSupport::TestCase
end
test "invoke" do
- @worker.invoke @receiver, :run
+ @worker.invoke @receiver, :run, connection: @receiver.connection
assert_equal :run, @receiver.last_action
end
test "invoke with arguments" do
- @worker.invoke @receiver, :process, "Hello"
+ @worker.invoke @receiver, :process, "Hello", connection: @receiver.connection
assert_equal [ :process, "Hello" ], @receiver.last_action
end
-
- test "running periodic timers with a proc" do
- @worker.run_periodic_timer @receiver, @receiver.method(:run)
- assert_equal :run, @receiver.last_action
- end
-
- test "running periodic timers with a method" do
- @worker.run_periodic_timer @receiver, :run
- assert_equal :run, @receiver.last_action
- end
end
diff --git a/actionmailbox/.gitignore b/actionmailbox/.gitignore
new file mode 100644
index 0000000000..739e00a2cb
--- /dev/null
+++ b/actionmailbox/.gitignore
@@ -0,0 +1,5 @@
+/test/dummy/db/*.sqlite3
+/test/dummy/db/*.sqlite3-journal
+/test/dummy/log/*.log
+/test/dummy/tmp/
+/tmp/
diff --git a/actionmailbox/CHANGELOG.md b/actionmailbox/CHANGELOG.md
new file mode 100644
index 0000000000..f5fb573090
--- /dev/null
+++ b/actionmailbox/CHANGELOG.md
@@ -0,0 +1,19 @@
+## Rails 6.0.0.beta3 (March 11, 2019) ##
+
+* No changes.
+
+
+## Rails 6.0.0.beta2 (February 25, 2019) ##
+
+* Allow skipping incineration of processed emails.
+
+ This can be done by setting `config.action_mailbox.incinerate` to `false`.
+
+ *Pratik Naik*
+
+
+## Rails 6.0.0.beta1 (January 18, 2019) ##
+
+* Added to Rails.
+
+ *DHH*
diff --git a/actionmailbox/MIT-LICENSE b/actionmailbox/MIT-LICENSE
new file mode 100644
index 0000000000..e28efa78e1
--- /dev/null
+++ b/actionmailbox/MIT-LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 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/actionmailbox/README.md b/actionmailbox/README.md
new file mode 100644
index 0000000000..9a47223d3b
--- /dev/null
+++ b/actionmailbox/README.md
@@ -0,0 +1,13 @@
+# Action Mailbox
+
+Action Mailbox routes incoming emails to controller-like mailboxes for processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill, Postmark, and SendGrid. You can also handle inbound mails directly via the built-in Exim, Postfix, and Qmail ingresses.
+
+The inbound emails are turned into `InboundEmail` records using Active Record and feature lifecycle tracking, storage of the original email on cloud storage via Active Storage, and responsible data handling with on-by-default incineration.
+
+These inbound emails are routed asynchronously using Active Job to one or several dedicated mailboxes, which are capable of interacting directly with the rest of your domain model.
+
+You can read more about Action Mailbox in the [Action Mailbox Basics](https://edgeguides.rubyonrails.org/action_mailbox_basics.html) guide.
+
+## License
+
+Action Mailbox is released under the [MIT License](https://opensource.org/licenses/MIT).
diff --git a/actionmailbox/Rakefile b/actionmailbox/Rakefile
new file mode 100644
index 0000000000..36aed17282
--- /dev/null
+++ b/actionmailbox/Rakefile
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "bundler/gem_tasks"
+require "rake/testtask"
+
+task :package
+
+Rake::TestTask.new do |t|
+ t.libs << "test"
+ t.pattern = "test/**/*_test.rb"
+ t.verbose = true
+end
+
+task default: :test
diff --git a/actionmailbox/actionmailbox.gemspec b/actionmailbox/actionmailbox.gemspec
new file mode 100644
index 0000000000..c38857f6d7
--- /dev/null
+++ b/actionmailbox/actionmailbox.gemspec
@@ -0,0 +1,38 @@
+# 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 = "actionmailbox"
+ s.version = version
+ s.summary = "Inbound email handling framework."
+ s.description = "Receive and process incoming emails in Rails applications."
+
+ s.required_ruby_version = ">= 2.5.0"
+
+ s.license = "MIT"
+
+ s.authors = ["David Heinemeier Hansson", "George Claghorn"]
+ s.email = ["david@loudthinking.com", "george@basecamp.com"]
+ s.homepage = "https://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}/actionmailbox",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actionmailbox/CHANGELOG.md"
+ }
+
+ # 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 "activerecord", version
+ s.add_dependency "activestorage", version
+ s.add_dependency "activejob", version
+ s.add_dependency "actionpack", version
+
+ s.add_dependency "mail", ">= 2.7.1"
+end
diff --git a/actionmailbox/app/controllers/action_mailbox/base_controller.rb b/actionmailbox/app/controllers/action_mailbox/base_controller.rb
new file mode 100644
index 0000000000..f0f1f555e6
--- /dev/null
+++ b/actionmailbox/app/controllers/action_mailbox/base_controller.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # The base class for all Action Mailbox ingress controllers.
+ class BaseController < ActionController::Base
+ skip_forgery_protection
+
+ before_action :ensure_configured
+
+ def self.prepare
+ # Override in concrete controllers to run code on load.
+ end
+
+ private
+ def ensure_configured
+ unless ActionMailbox.ingress == ingress_name
+ head :not_found
+ end
+ end
+
+ def ingress_name
+ self.class.name.remove(/\AActionMailbox::Ingresses::/, /::InboundEmailsController\z/).underscore.to_sym
+ end
+
+
+ def authenticate_by_password
+ if password.present?
+ http_basic_authenticate_or_request_with name: "actionmailbox", password: password, realm: "Action Mailbox"
+ else
+ raise ArgumentError, "Missing required ingress credentials"
+ end
+ end
+
+ def password
+ Rails.application.credentials.dig(:action_mailbox, :ingress_password) || ENV["RAILS_INBOUND_EMAIL_PASSWORD"]
+ end
+ end
+end
diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb
new file mode 100644
index 0000000000..e0a187054e
--- /dev/null
+++ b/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Ingests inbound emails from Amazon's Simple Email Service (SES).
+ #
+ # Requires the full RFC 822 message in the +content+ parameter. Authenticates requests by validating their signatures.
+ #
+ # Returns:
+ #
+ # - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
+ # - <tt>401 Unauthorized</tt> if the request's signature could not be validated
+ # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from SES
+ # - <tt>422 Unprocessable Entity</tt> if the request is missing the required +content+ parameter
+ # - <tt>500 Server Error</tt> if one of the Active Record database, the Active Storage service, or
+ # the Active Job backend is misconfigured or unavailable
+ #
+ # == Usage
+ #
+ # 1. Install the {aws-sdk-sns}[https://rubygems.org/gems/aws-sdk-sns] gem:
+ #
+ # # Gemfile
+ # gem "aws-sdk-sns", ">= 1.9.0", require: false
+ #
+ # 2. Tell Action Mailbox to accept emails from SES:
+ #
+ # # config/environments/production.rb
+ # config.action_mailbox.ingress = :amazon
+ #
+ # 3. {Configure SES}[https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html]
+ # to deliver emails to your application via POST requests to +/rails/action_mailbox/amazon/inbound_emails+.
+ # If your application lived at <tt>https://example.com</tt>, you would specify the fully-qualified URL
+ # <tt>https://example.com/rails/action_mailbox/amazon/inbound_emails</tt>.
+ class Ingresses::Amazon::InboundEmailsController < BaseController
+ before_action :authenticate
+
+ cattr_accessor :verifier
+
+ def self.prepare
+ self.verifier ||= begin
+ require "aws-sdk-sns"
+ Aws::SNS::MessageVerifier.new
+ end
+ end
+
+ def create
+ ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:content)
+ end
+
+ private
+ def authenticate
+ head :unauthorized unless verifier.authentic?(request.body)
+ end
+ end
+end
diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
new file mode 100644
index 0000000000..bf0fd562fe
--- /dev/null
+++ b/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Ingests inbound emails from Mailgun. Requires the following parameters:
+ #
+ # - +body-mime+: The full RFC 822 message
+ # - +timestamp+: The current time according to Mailgun as the number of seconds passed since the UNIX epoch
+ # - +token+: A randomly-generated, 50-character string
+ # - +signature+: A hexadecimal HMAC-SHA256 of the timestamp concatenated with the token, generated using the Mailgun API key
+ #
+ # Authenticates requests by validating their signatures.
+ #
+ # Returns:
+ #
+ # - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
+ # - <tt>401 Unauthorized</tt> if the request's signature could not be validated, or if its timestamp is more than 2 minutes old
+ # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Mailgun
+ # - <tt>422 Unprocessable Entity</tt> if the request is missing required parameters
+ # - <tt>500 Server Error</tt> if the Mailgun API key is missing, or one of the Active Record database,
+ # the Active Storage service, or the Active Job backend is misconfigured or unavailable
+ #
+ # == Usage
+ #
+ # 1. Give Action Mailbox your {Mailgun API key}[https://help.mailgun.com/hc/en-us/articles/203380100-Where-can-I-find-my-API-key-and-SMTP-credentials-]
+ # so it can authenticate requests to the Mailgun ingress.
+ #
+ # Use <tt>rails credentials:edit</tt> to add your API key to your application's encrypted credentials under
+ # +action_mailbox.mailgun_api_key+, where Action Mailbox will automatically find it:
+ #
+ # action_mailbox:
+ # mailgun_api_key: ...
+ #
+ # Alternatively, provide your API key in the +MAILGUN_INGRESS_API_KEY+ environment variable.
+ #
+ # 2. Tell Action Mailbox to accept emails from Mailgun:
+ #
+ # # config/environments/production.rb
+ # config.action_mailbox.ingress = :mailgun
+ #
+ # 3. {Configure Mailgun}[https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages]
+ # to forward inbound emails to +/rails/action_mailbox/mailgun/inbound_emails/mime+.
+ #
+ # If your application lived at <tt>https://example.com</tt>, you would specify the fully-qualified URL
+ # <tt>https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime</tt>.
+ class Ingresses::Mailgun::InboundEmailsController < ActionMailbox::BaseController
+ before_action :authenticate
+
+ def create
+ ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("body-mime")
+ end
+
+ private
+ def authenticate
+ head :unauthorized unless authenticated?
+ end
+
+ def authenticated?
+ if key.present?
+ Authenticator.new(
+ key: key,
+ timestamp: params.require(:timestamp),
+ token: params.require(:token),
+ signature: params.require(:signature)
+ ).authenticated?
+ else
+ raise ArgumentError, <<~MESSAGE.squish
+ Missing required Mailgun API key. Set action_mailbox.mailgun_api_key in your application's
+ encrypted credentials or provide the MAILGUN_INGRESS_API_KEY environment variable.
+ MESSAGE
+ end
+ end
+
+ def key
+ Rails.application.credentials.dig(:action_mailbox, :mailgun_api_key) || ENV["MAILGUN_INGRESS_API_KEY"]
+ end
+
+ class Authenticator
+ attr_reader :key, :timestamp, :token, :signature
+
+ def initialize(key:, timestamp:, token:, signature:)
+ @key, @timestamp, @token, @signature = key, Integer(timestamp), token, signature
+ end
+
+ def authenticated?
+ signed? && recent?
+ end
+
+ private
+ def signed?
+ ActiveSupport::SecurityUtils.secure_compare signature, expected_signature
+ end
+
+ # Allow for 2 minutes of drift between Mailgun time and local server time.
+ def recent?
+ Time.at(timestamp) >= 2.minutes.ago
+ end
+
+ def expected_signature
+ OpenSSL::HMAC.hexdigest OpenSSL::Digest::SHA256.new, key, "#{timestamp}#{token}"
+ end
+ end
+ end
+end
diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb
new file mode 100644
index 0000000000..2a3b1619b9
--- /dev/null
+++ b/actionmailbox/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Ingests inbound emails from Mandrill.
+ #
+ # Requires a +mandrill_events+ parameter containing a JSON array of Mandrill inbound email event objects.
+ # Each event is expected to have a +msg+ object containing a full RFC 822 message in its +raw_msg+ property.
+ #
+ # Returns:
+ #
+ # - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
+ # - <tt>401 Unauthorized</tt> if the request's signature could not be validated
+ # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Mandrill
+ # - <tt>422 Unprocessable Entity</tt> if the request is missing required parameters
+ # - <tt>500 Server Error</tt> if the Mandrill API key is missing, or one of the Active Record database,
+ # the Active Storage service, or the Active Job backend is misconfigured or unavailable
+ class Ingresses::Mandrill::InboundEmailsController < ActionMailbox::BaseController
+ before_action :authenticate
+
+ def create
+ raw_emails.each { |raw_email| ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email }
+ head :ok
+ rescue JSON::ParserError => error
+ logger.error error.message
+ head :unprocessable_entity
+ end
+
+ private
+ def raw_emails
+ events.select { |event| event["event"] == "inbound" }.collect { |event| event.dig("msg", "raw_msg") }
+ end
+
+ def events
+ JSON.parse params.require(:mandrill_events)
+ end
+
+
+ def authenticate
+ head :unauthorized unless authenticated?
+ end
+
+ def authenticated?
+ if key.present?
+ Authenticator.new(request, key).authenticated?
+ else
+ raise ArgumentError, <<~MESSAGE.squish
+ Missing required Mandrill API key. Set action_mailbox.mandrill_api_key in your application's
+ encrypted credentials or provide the MANDRILL_INGRESS_API_KEY environment variable.
+ MESSAGE
+ end
+ end
+
+ def key
+ Rails.application.credentials.dig(:action_mailbox, :mandrill_api_key) || ENV["MANDRILL_INGRESS_API_KEY"]
+ end
+
+ class Authenticator
+ attr_reader :request, :key
+
+ def initialize(request, key)
+ @request, @key = request, key
+ end
+
+ def authenticated?
+ ActiveSupport::SecurityUtils.secure_compare given_signature, expected_signature
+ end
+
+ private
+ def given_signature
+ request.headers["X-Mandrill-Signature"]
+ end
+
+ def expected_signature
+ Base64.strict_encode64 OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new, key, message)
+ end
+
+ def message
+ request.url + request.POST.sort.flatten.join
+ end
+ end
+ end
+end
diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb
new file mode 100644
index 0000000000..309085c82a
--- /dev/null
+++ b/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Ingests inbound emails from Postmark. Requires a +RawEmail+ parameter containing a full RFC 822 message.
+ #
+ # Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
+ # password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
+ #
+ # Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
+ # the Postmark ingress can learn its password. You should only use the Postmark ingress over HTTPS.
+ #
+ # Returns:
+ #
+ # - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
+ # - <tt>401 Unauthorized</tt> if the request's signature could not be validated
+ # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Postmark
+ # - <tt>422 Unprocessable Entity</tt> if the request is missing the required +RawEmail+ parameter
+ # - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
+ # the Active Storage service, or the Active Job backend is misconfigured or unavailable
+ #
+ # == Usage
+ #
+ # 1. Tell Action Mailbox to accept emails from Postmark:
+ #
+ # # config/environments/production.rb
+ # config.action_mailbox.ingress = :postmark
+ #
+ # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postmark ingress.
+ #
+ # Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under
+ # +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
+ #
+ # action_mailbox:
+ # ingress_password: ...
+ #
+ # Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
+ #
+ # 3. {Configure Postmark}[https://postmarkapp.com/manual#configure-your-inbound-webhook-url] to forward inbound emails
+ # to +/rails/action_mailbox/postmark/inbound_emails+ with the username +actionmailbox+ and the password you
+ # previously generated. If your application lived at <tt>https://example.com</tt>, you would configure your
+ # Postmark inbound webhook with the following fully-qualified URL:
+ #
+ # https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emails
+ #
+ # *NOTE:* When configuring your Postmark inbound webhook, be sure to check the box labeled *"Include raw email
+ # content in JSON payload"*. Action Mailbox needs the raw email content to work.
+ class Ingresses::Postmark::InboundEmailsController < ActionMailbox::BaseController
+ before_action :authenticate_by_password
+
+ def create
+ ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("RawEmail")
+ rescue ActionController::ParameterMissing => error
+ logger.error <<~MESSAGE
+ #{error.message}
+
+ When configuring your Postmark inbound webhook, be sure to check the box
+ labeled "Include raw email content in JSON payload".
+ MESSAGE
+ head :unprocessable_entity
+ end
+ end
+end
diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb
new file mode 100644
index 0000000000..2d91c968c8
--- /dev/null
+++ b/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Ingests inbound emails relayed from an SMTP server.
+ #
+ # Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
+ # password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
+ #
+ # Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
+ # the ingress can learn its password. You should only use this ingress over HTTPS.
+ #
+ # Returns:
+ #
+ # - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
+ # - <tt>401 Unauthorized</tt> if the request could not be authenticated
+ # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails relayed from an SMTP server
+ # - <tt>415 Unsupported Media Type</tt> if the request does not contain an RFC 822 message
+ # - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
+ # the Active Storage service, or the Active Job backend is misconfigured or unavailable
+ #
+ # == Usage
+ #
+ # 1. Tell Action Mailbox to accept emails from an SMTP relay:
+ #
+ # # config/environments/production.rb
+ # config.action_mailbox.ingress = :relay
+ #
+ # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the ingress.
+ #
+ # Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under
+ # +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
+ #
+ # action_mailbox:
+ # ingress_password: ...
+ #
+ # Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
+ #
+ # 3. Configure your SMTP server to pipe inbound emails to the appropriate ingress command, providing the +URL+ of the
+ # relay ingress and the +INGRESS_PASSWORD+ you previously generated.
+ #
+ # If your application lives at <tt>https://example.com</tt>, you would configure the Postfix SMTP server to pipe
+ # inbound emails to the following command:
+ #
+ # bin/rails action_mailbox:ingress:postfix URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=...
+ #
+ # Built-in ingress commands are available for these popular SMTP servers:
+ #
+ # - Exim (<tt>bin/rails action_mailbox:ingress:exim)
+ # - Postfix (<tt>bin/rails action_mailbox:ingress:postfix)
+ # - Qmail (<tt>bin/rails action_mailbox:ingress:qmail)
+ class Ingresses::Relay::InboundEmailsController < ActionMailbox::BaseController
+ before_action :authenticate_by_password, :require_valid_rfc822_message
+
+ def create
+ ActionMailbox::InboundEmail.create_and_extract_message_id! request.body.read
+ end
+
+ private
+ def require_valid_rfc822_message
+ unless request.content_type == "message/rfc822"
+ head :unsupported_media_type
+ end
+ end
+ end
+end
diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb
new file mode 100644
index 0000000000..58da33d85e
--- /dev/null
+++ b/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Ingests inbound emails from SendGrid. Requires an +email+ parameter containing a full RFC 822 message.
+ #
+ # Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the
+ # password is read from the application's encrypted credentials or an environment variable. See the Usage section below.
+ #
+ # Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to
+ # the SendGrid ingress can learn its password. You should only use the SendGrid ingress over HTTPS.
+ #
+ # Returns:
+ #
+ # - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox
+ # - <tt>401 Unauthorized</tt> if the request's signature could not be validated
+ # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from SendGrid
+ # - <tt>422 Unprocessable Entity</tt> if the request is missing the required +email+ parameter
+ # - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database,
+ # the Active Storage service, or the Active Job backend is misconfigured or unavailable
+ #
+ # == Usage
+ #
+ # 1. Tell Action Mailbox to accept emails from SendGrid:
+ #
+ # # config/environments/production.rb
+ # config.action_mailbox.ingress = :sendgrid
+ #
+ # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the SendGrid ingress.
+ #
+ # Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under
+ # +action_mailbox.ingress_password+, where Action Mailbox will automatically find it:
+ #
+ # action_mailbox:
+ # ingress_password: ...
+ #
+ # Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable.
+ #
+ # 3. {Configure SendGrid Inbound Parse}[https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/]
+ # to forward inbound emails to +/rails/action_mailbox/sendgrid/inbound_emails+ with the username +actionmailbox+ and
+ # the password you previously generated. If your application lived at <tt>https://example.com</tt>, you would
+ # configure SendGrid with the following fully-qualified URL:
+ #
+ # https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails
+ #
+ # *NOTE:* When configuring your SendGrid Inbound Parse webhook, be sure to check the box labeled *"Post the raw,
+ # full MIME message."* Action Mailbox needs the raw MIME message to work.
+ class Ingresses::Sendgrid::InboundEmailsController < ActionMailbox::BaseController
+ before_action :authenticate_by_password
+
+ def create
+ ActionMailbox::InboundEmail.create_and_extract_message_id! params.require(:email)
+ end
+ end
+end
diff --git a/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb b/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb
new file mode 100644
index 0000000000..d051dfe665
--- /dev/null
+++ b/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Rails
+ class Conductor::ActionMailbox::InboundEmailsController < Rails::Conductor::BaseController
+ def index
+ @inbound_emails = ActionMailbox::InboundEmail.order(created_at: :desc)
+ end
+
+ def new
+ end
+
+ def show
+ @inbound_email = ActionMailbox::InboundEmail.find(params[:id])
+ end
+
+ def create
+ inbound_email = create_inbound_email(new_mail)
+ redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
+ end
+
+ private
+ def new_mail
+ Mail.new(params.require(:mail).permit(:from, :to, :cc, :bcc, :in_reply_to, :subject, :body).to_h).tap do |mail|
+ params[:mail][:attachments].to_a.each do |attachment|
+ mail.add_file(filename: attachment.path, content: attachment.read)
+ end
+ end
+ end
+
+ def create_inbound_email(mail)
+ ActionMailbox::InboundEmail.create_and_extract_message_id!(mail.to_s)
+ end
+ end
+end
diff --git a/actionmailbox/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb b/actionmailbox/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb
new file mode 100644
index 0000000000..05b1ca9a39
--- /dev/null
+++ b/actionmailbox/app/controllers/rails/conductor/action_mailbox/reroutes_controller.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Rails
+ # Rerouting will run routing and processing on an email that has already been, or attempted to be, processed.
+ class Conductor::ActionMailbox::ReroutesController < Rails::Conductor::BaseController
+ def create
+ inbound_email = ActionMailbox::InboundEmail.find(params[:inbound_email_id])
+ reroute inbound_email
+
+ redirect_to main_app.rails_conductor_inbound_email_url(inbound_email)
+ end
+
+ private
+ def reroute(inbound_email)
+ inbound_email.pending!
+ inbound_email.route_later
+ end
+ end
+end
diff --git a/actionmailbox/app/controllers/rails/conductor/base_controller.rb b/actionmailbox/app/controllers/rails/conductor/base_controller.rb
new file mode 100644
index 0000000000..93c4e2a66e
--- /dev/null
+++ b/actionmailbox/app/controllers/rails/conductor/base_controller.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Rails
+ # TODO: Move this to Rails::Conductor gem
+ class Conductor::BaseController < ActionController::Base
+ layout "rails/conductor"
+ before_action :ensure_development_env
+
+ private
+ def ensure_development_env
+ head :forbidden unless Rails.env.development?
+ end
+ end
+end
diff --git a/actionmailbox/app/jobs/action_mailbox/incineration_job.rb b/actionmailbox/app/jobs/action_mailbox/incineration_job.rb
new file mode 100644
index 0000000000..351bd67c69
--- /dev/null
+++ b/actionmailbox/app/jobs/action_mailbox/incineration_job.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # You can configure when this +IncinerationJob+ will be run as a time-after-processing using the
+ # +config.action_mailbox.incinerate_after+ or +ActionMailbox.incinerate_after+ setting.
+ #
+ # Since this incineration is set for the future, it'll automatically ignore any <tt>InboundEmail</tt>s
+ # that have already been deleted and discard itself if so.
+ #
+ # You can disable incinerating processed emails by setting +config.action_mailbox.incinerate+ or
+ # +ActionMailbox.incinerate+ to +false+.
+ class IncinerationJob < ActiveJob::Base
+ queue_as { ActionMailbox.queues[:incineration] }
+
+ discard_on ActiveRecord::RecordNotFound
+
+ def self.schedule(inbound_email)
+ set(wait: ActionMailbox.incinerate_after).perform_later(inbound_email)
+ end
+
+ def perform(inbound_email)
+ inbound_email.incinerate
+ end
+ end
+end
diff --git a/actionmailbox/app/jobs/action_mailbox/routing_job.rb b/actionmailbox/app/jobs/action_mailbox/routing_job.rb
new file mode 100644
index 0000000000..4ddf6e4231
--- /dev/null
+++ b/actionmailbox/app/jobs/action_mailbox/routing_job.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Routing a new InboundEmail is an asynchronous operation, which allows the ingress controllers to quickly
+ # accept new incoming emails without being burdened to hang while they're actually being processed.
+ class RoutingJob < ActiveJob::Base
+ queue_as { ActionMailbox.queues[:routing] }
+
+ def perform(inbound_email)
+ inbound_email.route
+ end
+ end
+end
diff --git a/actionmailbox/app/models/action_mailbox/inbound_email.rb b/actionmailbox/app/models/action_mailbox/inbound_email.rb
new file mode 100644
index 0000000000..023de19ccc
--- /dev/null
+++ b/actionmailbox/app/models/action_mailbox/inbound_email.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require "mail"
+
+module ActionMailbox
+ # The +InboundEmail+ is an Active Record that keeps a reference to the raw email stored in Active Storage
+ # and tracks the status of processing. By default, incoming emails will go through the following lifecycle:
+ #
+ # * Pending: Just received by one of the ingress controllers and scheduled for routing.
+ # * Processing: During active processing, while a specific mailbox is running its #process method.
+ # * Delivered: Successfully processed by the specific mailbox.
+ # * Failed: An exception was raised during the specific mailbox's execution of the +#process+ method.
+ # * Bounced: Rejected processing by the specific mailbox and bounced to sender.
+ #
+ # Once the +InboundEmail+ has reached the status of being either +delivered+, +failed+, or +bounced+,
+ # it'll count as having been +#processed?+. Once processed, the +InboundEmail+ will be scheduled for
+ # automatic incineration at a later point.
+ #
+ # When working with an +InboundEmail+, you'll usually interact with the parsed version of the source,
+ # which is available as a +Mail+ object from +#mail+. But you can also access the raw source directly
+ # using the +#source+ method.
+ #
+ # Examples:
+ #
+ # inbound_email.mail.from # => 'david@loudthinking.com'
+ # inbound_email.source # Returns the full rfc822 source of the email as text
+ class InboundEmail < ActiveRecord::Base
+ self.table_name = "action_mailbox_inbound_emails"
+
+ include Incineratable, MessageId, Routable
+
+ has_one_attached :raw_email
+ enum status: %i[ pending processing delivered failed bounced ]
+
+ def mail
+ @mail ||= Mail.from_source(source)
+ end
+
+ def source
+ @source ||= raw_email.download
+ end
+
+ def processed?
+ delivered? || failed? || bounced?
+ end
+ end
+end
+
+ActiveSupport.run_load_hooks :action_mailbox_inbound_email, ActionMailbox::InboundEmail
diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb
new file mode 100644
index 0000000000..e7c8782f33
--- /dev/null
+++ b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+# Ensure that the +InboundEmail+ is automatically scheduled for later incineration if the status has been
+# changed to +processed+. The later incineration will be invoked at the time specified by the
+# +ActionMailbox.incinerate_after+ time using the +IncinerationJob+.
+module ActionMailbox::InboundEmail::Incineratable
+ extend ActiveSupport::Concern
+
+ included do
+ after_update_commit :incinerate_later, if: -> { ActionMailbox.incinerate && status_previously_changed? && processed? }
+ end
+
+ def incinerate_later
+ ActionMailbox::IncinerationJob.schedule self
+ end
+
+ def incinerate
+ Incineration.new(self).run
+ end
+end
diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/incineratable/incineration.rb b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable/incineration.rb
new file mode 100644
index 0000000000..dabc83fae6
--- /dev/null
+++ b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable/incineration.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Command class for carrying out the actual incineration of the +InboundMail+ that's been scheduled
+ # for removal. Before the incineration – which really is just a call to +#destroy!+ – is run, we verify
+ # that it's both eligible (by virtue of having already been processed) and time to do so (that is,
+ # the +InboundEmail+ was processed after the +incinerate_after+ time).
+ class InboundEmail::Incineratable::Incineration
+ def initialize(inbound_email)
+ @inbound_email = inbound_email
+ end
+
+ def run
+ @inbound_email.destroy! if due? && processed?
+ end
+
+ private
+ def due?
+ @inbound_email.updated_at < ActionMailbox.incinerate_after.ago.end_of_day
+ end
+
+ def processed?
+ @inbound_email.processed?
+ end
+ end
+end
diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb b/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb
new file mode 100644
index 0000000000..470b93ca20
--- /dev/null
+++ b/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+# The +Message-ID+ as specified by rfc822 is supposed to be a unique identifier for that individual email.
+# That makes it an ideal tracking token for debugging and forensics, just like +X-Request-Id+ does for
+# web request.
+#
+# If an inbound email does not, against the rfc822 mandate, specify a Message-ID, one will be generated
+# using the approach from <tt>Mail::MessageIdField</tt>.
+module ActionMailbox::InboundEmail::MessageId
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Create a new +InboundEmail+ from the raw +source+ of the email, which be uploaded as a Active Storage
+ # attachment called +raw_email+. Before the upload, extract the Message-ID from the +source+ and set
+ # it as an attribute on the new +InboundEmail+.
+ def create_and_extract_message_id!(source, **options)
+ message_checksum = Digest::SHA1.hexdigest(source)
+ message_id = extract_message_id(source) || generate_missing_message_id(message_checksum)
+
+ create! options.merge(message_id: message_id, message_checksum: message_checksum) do |inbound_email|
+ inbound_email.raw_email.attach io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822"
+ end
+ rescue ActiveRecord::RecordNotUnique
+ nil
+ end
+
+ private
+ def extract_message_id(source)
+ Mail.from_source(source).message_id rescue nil
+ end
+
+ def generate_missing_message_id(message_checksum)
+ Mail::MessageIdField.new("<#{message_checksum}@#{::Socket.gethostname}.mail>").message_id.tap do |message_id|
+ logger.warn "Message-ID couldn't be parsed or is missing. Generated a new Message-ID: #{message_id}"
+ end
+ end
+ end
+end
diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/routable.rb b/actionmailbox/app/models/action_mailbox/inbound_email/routable.rb
new file mode 100644
index 0000000000..39565df166
--- /dev/null
+++ b/actionmailbox/app/models/action_mailbox/inbound_email/routable.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# A newly received +InboundEmail+ will not be routed synchronously as part of ingress controller's receival.
+# Instead, the routing will be done asynchronously, using a +RoutingJob+, to ensure maximum parallel capacity.
+#
+# By default, all newly created +InboundEmail+ records that have the status of +pending+, which is the default,
+# will be scheduled for automatic, deferred routing.
+module ActionMailbox::InboundEmail::Routable
+ extend ActiveSupport::Concern
+
+ included do
+ after_create_commit :route_later, if: :pending?
+ end
+
+ # Enqueue a +RoutingJob+ for this +InboundEmail+.
+ def route_later
+ ActionMailbox::RoutingJob.perform_later self
+ end
+
+ # Route this +InboundEmail+ using the routing rules declared on the +ApplicationMailbox+.
+ def route
+ ApplicationMailbox.route self
+ end
+end
diff --git a/actionmailbox/app/views/layouts/rails/conductor.html.erb b/actionmailbox/app/views/layouts/rails/conductor.html.erb
new file mode 100644
index 0000000000..1cad6560c4
--- /dev/null
+++ b/actionmailbox/app/views/layouts/rails/conductor.html.erb
@@ -0,0 +1,8 @@
+<html>
+<head>
+ <title>Rails Conductor: <%= yield :title %></title>
+</head>
+<body>
+<%= yield %>
+</body>
+</html>
diff --git a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb
new file mode 100644
index 0000000000..19c53984e2
--- /dev/null
+++ b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/index.html.erb
@@ -0,0 +1,15 @@
+<% provide :title, "Deliver new inbound email" %>
+
+<h1>All inbound emails</h1>
+
+<table>
+ <tr><th>Message ID</th><th>Status</th></tr>
+ <% @inbound_emails.each do |inbound_email| %>
+ <tr>
+ <td><%= link_to inbound_email.message_id, main_app.rails_conductor_inbound_email_path(inbound_email) %></td>
+ <td><%= inbound_email.status %></td>
+ </tr>
+ <% end %>
+</table>
+
+<%= link_to "Deliver new inbound email", main_app.new_rails_conductor_inbound_email_path %> \ No newline at end of file
diff --git a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb
new file mode 100644
index 0000000000..a3c3d39064
--- /dev/null
+++ b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb
@@ -0,0 +1,47 @@
+<% provide :title, "Deliver new inbound email" %>
+
+<h1>Deliver new inbound email</h1>
+
+<%= form_with(url: main_app.rails_conductor_inbound_emails_path, scope: :mail, local: true) do |form| %>
+ <div>
+ <%= form.label :from, "From" %><br>
+ <%= form.text_field :from %>
+ </div>
+
+ <div>
+ <%= form.label :to, "To" %><br>
+ <%= form.text_field :to %>
+ </div>
+
+ <div>
+ <%= form.label :cc, "CC" %><br>
+ <%= form.text_field :cc %>
+ </div>
+
+ <div>
+ <%= form.label :bcc, "BCC" %><br>
+ <%= form.text_field :bcc %>
+ </div>
+
+ <div>
+ <%= form.label :in_reply_to, "In-Reply-To" %><br>
+ <%= form.text_field :in_reply_to %>
+ </div>
+
+ <div>
+ <%= form.label :subject, "Subject" %><br>
+ <%= form.text_field :subject %>
+ </div>
+
+ <div>
+ <%= form.label :body, "Body" %><br>
+ <%= form.text_area :body, size: "40x20" %>
+ </div>
+
+ <div>
+ <%= form.label :attachments, "Attachments" %><br>
+ <%= form.file_field :attachments, multiple: true %>
+ </div>
+
+ <%= form.submit "Deliver inbound email" %>
+<% end %>
diff --git a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb
new file mode 100644
index 0000000000..e761904196
--- /dev/null
+++ b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/show.html.erb
@@ -0,0 +1,15 @@
+<% provide :title, @inbound_email.message_id %>
+
+<h1><%= @inbound_email.message_id %>: <%= @inbound_email.status %></h1>
+
+<ul>
+ <li><%= button_to "Route again", main_app.rails_conductor_inbound_email_reroute_path(@inbound_email), method: :post %></li>
+ <li>Incinerate</li>
+</ul>
+
+<details>
+ <summary>Full email source</summary>
+ <pre><%= @inbound_email.source %></pre>
+</details>
+
+<%= link_to "Back to all inbound emails", main_app.rails_conductor_inbound_emails_path %> \ No newline at end of file
diff --git a/actionmailbox/bin/test b/actionmailbox/bin/test
new file mode 100755
index 0000000000..c53377cc97
--- /dev/null
+++ b/actionmailbox/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/actionmailbox/config/routes.rb b/actionmailbox/config/routes.rb
new file mode 100644
index 0000000000..517d2835af
--- /dev/null
+++ b/actionmailbox/config/routes.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+Rails.application.routes.draw do
+ scope "/rails/action_mailbox", module: "action_mailbox/ingresses" do
+ post "/amazon/inbound_emails" => "amazon/inbound_emails#create", as: :rails_amazon_inbound_emails
+ post "/mandrill/inbound_emails" => "mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails
+ post "/postmark/inbound_emails" => "postmark/inbound_emails#create", as: :rails_postmark_inbound_emails
+ post "/relay/inbound_emails" => "relay/inbound_emails#create", as: :rails_relay_inbound_emails
+ post "/sendgrid/inbound_emails" => "sendgrid/inbound_emails#create", as: :rails_sendgrid_inbound_emails
+
+ # Mailgun requires that a webhook's URL end in 'mime' for it to receive the raw contents of emails.
+ post "/mailgun/inbound_emails/mime" => "mailgun/inbound_emails#create", as: :rails_mailgun_inbound_emails
+ end
+
+ # TODO: Should these be mounted within the engine only?
+ scope "rails/conductor/action_mailbox/", module: "rails/conductor/action_mailbox" do
+ resources :inbound_emails, as: :rails_conductor_inbound_emails
+ post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute
+ end
+end
diff --git a/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb
new file mode 100644
index 0000000000..2bf4335808
--- /dev/null
+++ b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb
@@ -0,0 +1,13 @@
+class CreateActionMailboxTables < ActiveRecord::Migration[6.0]
+ def change
+ create_table :action_mailbox_inbound_emails do |t|
+ t.integer :status, default: 0, null: false
+ t.string :message_id, null: false
+ t.string :message_checksum, null: false
+
+ t.timestamps
+
+ t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox.rb b/actionmailbox/lib/action_mailbox.rb
new file mode 100644
index 0000000000..772dbd6529
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require "action_mailbox/mail_ext"
+
+module ActionMailbox
+ extend ActiveSupport::Autoload
+
+ autoload :Base
+ autoload :Router
+ autoload :TestCase
+
+ mattr_accessor :ingress
+ mattr_accessor :logger
+ mattr_accessor :incinerate, default: true
+ mattr_accessor :incinerate_after, default: 30.days
+ mattr_accessor :queues, default: {}
+end
diff --git a/actionmailbox/lib/action_mailbox/base.rb b/actionmailbox/lib/action_mailbox/base.rb
new file mode 100644
index 0000000000..ff8587acd1
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/base.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require "active_support/rescuable"
+
+require "action_mailbox/callbacks"
+require "action_mailbox/routing"
+
+module ActionMailbox
+ # The base class for all application mailboxes. Not intended to be inherited from directly. Inherit from
+ # +ApplicationMailbox+ instead, as that's where the app-specific routing is configured. This routing
+ # is specified in the following ways:
+ #
+ # class ApplicationMailbox < ActionMailbox::Base
+ # # Any of the recipients of the mail (whether to, cc, bcc) are matched against the regexp.
+ # routing /^replies@/i => :replies
+ #
+ # # Any of the recipients of the mail (whether to, cc, bcc) needs to be an exact match for the string.
+ # routing "help@example.com" => :help
+ #
+ # # Any callable (proc, lambda, etc) object is passed the inbound_email record and is a match if true.
+ # routing ->(inbound_email) { inbound_email.mail.to.size > 2 } => :multiple_recipients
+ #
+ # # Any object responding to #match? is called with the inbound_email record as an argument. Match if true.
+ # routing CustomAddress.new => :custom
+ #
+ # # Any inbound_email that has not been already matched will be sent to the BackstopMailbox.
+ # routing :all => :backstop
+ # end
+ #
+ # Application mailboxes need to overwrite the +#process+ method, which is invoked by the framework after
+ # callbacks have been run. The callbacks available are: +before_processing+, +after_processing+, and
+ # +around_processing+. The primary use case is ensure certain preconditions to processing are fulfilled
+ # using +before_processing+ callbacks.
+ #
+ # If a precondition fails to be met, you can halt the processing using the +#bounced!+ method,
+ # which will silently prevent any further processing, but not actually send out any bounce notice. You
+ # can also pair this behavior with the invocation of an Action Mailer class responsible for sending out
+ # an actual bounce email. This is done using the +#bounce_with+ method, which takes the mail object returned
+ # by an Action Mailer method, like so:
+ #
+ # class ForwardsMailbox < ApplicationMailbox
+ # before_processing :ensure_sender_is_a_user
+ #
+ # private
+ # def ensure_sender_is_a_user
+ # unless User.exist?(email_address: mail.from)
+ # bounce_with UserRequiredMailer.missing(inbound_email)
+ # end
+ # end
+ # end
+ #
+ # During the processing of the inbound email, the status will be tracked. Before processing begins,
+ # the email will normally have the +pending+ status. Once processing begins, just before callbacks
+ # and the +#process+ method is called, the status is changed to +processing+. If processing is allowed to
+ # complete, the status is changed to +delivered+. If a bounce is triggered, then +bounced+. If an unhandled
+ # exception is bubbled up, then +failed+.
+ #
+ # Exceptions can be handled at the class level using the familiar +Rescuable+ approach:
+ #
+ # class ForwardsMailbox < ApplicationMailbox
+ # rescue_from(ApplicationSpecificVerificationError) { bounced! }
+ # end
+ class Base
+ include ActiveSupport::Rescuable
+ include ActionMailbox::Callbacks, ActionMailbox::Routing
+
+ attr_reader :inbound_email
+ delegate :mail, :delivered!, :bounced!, to: :inbound_email
+
+ delegate :logger, to: ActionMailbox
+
+ def self.receive(inbound_email)
+ new(inbound_email).perform_processing
+ end
+
+ def initialize(inbound_email)
+ @inbound_email = inbound_email
+ end
+
+ def perform_processing #:nodoc:
+ track_status_of_inbound_email do
+ run_callbacks :process do
+ process
+ end
+ end
+ rescue => exception
+ # TODO: Include a reference to the inbound_email in the exception raised so error handling becomes easier
+ rescue_with_handler(exception) || raise
+ end
+
+ def process
+ # Overwrite in subclasses
+ end
+
+ def finished_processing? #:nodoc:
+ inbound_email.delivered? || inbound_email.bounced?
+ end
+
+
+ # Enqueues the given +message+ for delivery and changes the inbound email's status to +:bounced+.
+ def bounce_with(message)
+ inbound_email.bounced!
+ message.deliver_later
+ end
+
+ private
+ def track_status_of_inbound_email
+ inbound_email.processing!
+ yield
+ inbound_email.delivered! unless inbound_email.bounced?
+ rescue
+ inbound_email.failed!
+ raise
+ end
+ end
+end
+
+ActiveSupport.run_load_hooks :action_mailbox, ActionMailbox::Base
diff --git a/actionmailbox/lib/action_mailbox/callbacks.rb b/actionmailbox/lib/action_mailbox/callbacks.rb
new file mode 100644
index 0000000000..2b7212284b
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/callbacks.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "active_support/callbacks"
+
+module ActionMailbox
+ # Defines the callbacks related to processing.
+ module Callbacks
+ extend ActiveSupport::Concern
+ include ActiveSupport::Callbacks
+
+ TERMINATOR = ->(mailbox, chain) do
+ chain.call
+ mailbox.finished_processing?
+ end
+
+ included do
+ define_callbacks :process, terminator: TERMINATOR, skip_after_callbacks_if_terminated: true
+ end
+
+ class_methods do
+ def before_processing(*methods, &block)
+ set_callback(:process, :before, *methods, &block)
+ end
+
+ def after_processing(*methods, &block)
+ set_callback(:process, :after, *methods, &block)
+ end
+
+ def around_processing(*methods, &block)
+ set_callback(:process, :around, *methods, &block)
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/engine.rb b/actionmailbox/lib/action_mailbox/engine.rb
new file mode 100644
index 0000000000..039f04ac2f
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/engine.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "rails"
+require "action_controller/railtie"
+require "active_job/railtie"
+require "active_record/railtie"
+require "active_storage/engine"
+
+require "action_mailbox"
+
+module ActionMailbox
+ class Engine < Rails::Engine
+ isolate_namespace ActionMailbox
+ config.eager_load_namespaces << ActionMailbox
+
+ config.action_mailbox = ActiveSupport::OrderedOptions.new
+ config.action_mailbox.incinerate = true
+ config.action_mailbox.incinerate_after = 30.days
+
+ config.action_mailbox.queues = ActiveSupport::InheritableOptions.new \
+ incineration: :action_mailbox_incineration, routing: :action_mailbox_routing
+
+ initializer "action_mailbox.config" do
+ config.after_initialize do |app|
+ ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger
+ ActionMailbox.incinerate = app.config.action_mailbox.incinerate.nil? ? true : app.config.action_mailbox.incinerate
+ ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days
+ ActionMailbox.queues = app.config.action_mailbox.queues || {}
+ end
+ end
+
+ initializer "action_mailbox.ingress" do |app|
+ config.to_prepare do
+ if ActionMailbox.ingress = app.config.action_mailbox.ingress.presence
+ if ingress_controller_class = "ActionMailbox::Ingresses::#{ActionMailbox.ingress.to_s.classify}::InboundEmailsController".safe_constantize
+ ingress_controller_class.prepare
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/gem_version.rb b/actionmailbox/lib/action_mailbox/gem_version.rb
new file mode 100644
index 0000000000..a063553471
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/gem_version.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Returns the currently-loaded version of Action Mailbox 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 = "beta3"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/mail_ext.rb b/actionmailbox/lib/action_mailbox/mail_ext.rb
new file mode 100644
index 0000000000..c4d277a1f9
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/mail_ext.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+require "mail"
+
+# The hope is to upstream most of these basic additions to the Mail gem's Mail object. But until then, here they lay!
+Dir["#{File.expand_path(File.dirname(__FILE__))}/mail_ext/*"].each { |path| require "action_mailbox/mail_ext/#{File.basename(path)}" }
diff --git a/actionmailbox/lib/action_mailbox/mail_ext/address_equality.rb b/actionmailbox/lib/action_mailbox/mail_ext/address_equality.rb
new file mode 100644
index 0000000000..39a43b3468
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/mail_ext/address_equality.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Mail
+ class Address
+ def ==(other_address)
+ other_address.is_a?(Mail::Address) && to_s == other_address.to_s
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/mail_ext/address_wrapping.rb b/actionmailbox/lib/action_mailbox/mail_ext/address_wrapping.rb
new file mode 100644
index 0000000000..19eb624c1c
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/mail_ext/address_wrapping.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Mail
+ class Address
+ def self.wrap(address)
+ address.is_a?(Mail::Address) ? address : Mail::Address.new(address)
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/mail_ext/addresses.rb b/actionmailbox/lib/action_mailbox/mail_ext/addresses.rb
new file mode 100644
index 0000000000..5eab1feb3d
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/mail_ext/addresses.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Mail
+ class Message
+ def from_address
+ header[:from]&.address_list&.addresses&.first
+ end
+
+ def recipients_addresses
+ to_addresses + cc_addresses + bcc_addresses + x_original_to_addresses
+ end
+
+ def to_addresses
+ Array(header[:to]&.address_list&.addresses)
+ end
+
+ def cc_addresses
+ Array(header[:cc]&.address_list&.addresses)
+ end
+
+ def bcc_addresses
+ Array(header[:bcc]&.address_list&.addresses)
+ end
+
+ def x_original_to_addresses
+ Array(header[:x_original_to]).collect { |header| Mail::Address.new header.to_s }
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/mail_ext/from_source.rb b/actionmailbox/lib/action_mailbox/mail_ext/from_source.rb
new file mode 100644
index 0000000000..17b7fc80ad
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/mail_ext/from_source.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Mail
+ def self.from_source(source)
+ Mail.new Mail::Utilities.binary_unsafe_to_crlf(source.to_s)
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/mail_ext/recipients.rb b/actionmailbox/lib/action_mailbox/mail_ext/recipients.rb
new file mode 100644
index 0000000000..1f8a713218
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/mail_ext/recipients.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Mail
+ class Message
+ def recipients
+ Array(to) + Array(cc) + Array(bcc) + Array(header[:x_original_to]).map(&:to_s)
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/relayer.rb b/actionmailbox/lib/action_mailbox/relayer.rb
new file mode 100644
index 0000000000..e2890acb60
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/relayer.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require "action_mailbox/version"
+require "net/http"
+require "uri"
+
+module ActionMailbox
+ class Relayer
+ class Result < Struct.new(:status_code, :message)
+ def success?
+ !failure?
+ end
+
+ def failure?
+ transient_failure? || permanent_failure?
+ end
+
+ def transient_failure?
+ status_code.start_with?("4.")
+ end
+
+ def permanent_failure?
+ status_code.start_with?("5.")
+ end
+ end
+
+ CONTENT_TYPE = "message/rfc822"
+ USER_AGENT = "Action Mailbox relayer v#{ActionMailbox.version}"
+
+ attr_reader :uri, :username, :password
+
+ def initialize(url:, username: "actionmailbox", password:)
+ @uri, @username, @password = URI(url), username, password
+ end
+
+ def relay(source)
+ case response = post(source)
+ when Net::HTTPSuccess
+ Result.new "2.0.0", "Successfully relayed message to ingress"
+ when Net::HTTPUnauthorized
+ Result.new "4.7.0", "Invalid credentials for ingress"
+ else
+ Result.new "4.0.0", "HTTP #{response.code}"
+ end
+ rescue IOError, SocketError, SystemCallError => error
+ Result.new "4.4.2", "Network error relaying to ingress: #{error.message}"
+ rescue Timeout::Error
+ Result.new "4.4.2", "Timed out relaying to ingress"
+ rescue => error
+ Result.new "4.0.0", "Error relaying to ingress: #{error.message}"
+ end
+
+ private
+ def post(source)
+ client.post uri, source,
+ "Content-Type" => CONTENT_TYPE,
+ "User-Agent" => USER_AGENT,
+ "Authorization" => "Basic #{Base64.strict_encode64(username + ":" + password)}"
+ end
+
+ def client
+ @client ||= Net::HTTP.new(uri.host, uri.port).tap do |connection|
+ if uri.scheme == "https"
+ require "openssl"
+
+ connection.use_ssl = true
+ connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
+ end
+
+ connection.open_timeout = 1
+ connection.read_timeout = 10
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/router.rb b/actionmailbox/lib/action_mailbox/router.rb
new file mode 100644
index 0000000000..71370e409d
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/router.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Encapsulates the routes that live on the ApplicationMailbox and performs the actual routing when
+ # an inbound_email is received.
+ class Router
+ class RoutingError < StandardError; end
+
+ def initialize
+ @routes = []
+ end
+
+ def add_routes(routes)
+ routes.each do |(address, mailbox_name)|
+ add_route address, to: mailbox_name
+ end
+ end
+
+ def add_route(address, to:)
+ routes.append Route.new(address, to: to)
+ end
+
+ def route(inbound_email)
+ if mailbox = match_to_mailbox(inbound_email)
+ mailbox.receive(inbound_email)
+ else
+ inbound_email.bounced!
+
+ raise RoutingError
+ end
+ end
+
+ private
+ attr_reader :routes
+
+ def match_to_mailbox(inbound_email)
+ routes.detect { |route| route.match?(inbound_email) }.try(:mailbox_class)
+ end
+ end
+end
+
+require "action_mailbox/router/route"
diff --git a/actionmailbox/lib/action_mailbox/router/route.rb b/actionmailbox/lib/action_mailbox/router/route.rb
new file mode 100644
index 0000000000..7e98e83382
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/router/route.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # Encapsulates a route, which can then be matched against an inbound_email and provide a lookup of the matching
+ # mailbox class. See examples for the different route addresses and how to use them in the +ActionMailbox::Base+
+ # documentation.
+ class Router::Route
+ attr_reader :address, :mailbox_name
+
+ def initialize(address, to:)
+ @address, @mailbox_name = address, to
+
+ ensure_valid_address
+ end
+
+ def match?(inbound_email)
+ case address
+ when :all
+ true
+ when String
+ inbound_email.mail.recipients.any? { |recipient| address.casecmp?(recipient) }
+ when Regexp
+ inbound_email.mail.recipients.any? { |recipient| address.match?(recipient) }
+ when Proc
+ address.call(inbound_email)
+ else
+ address.match?(inbound_email)
+ end
+ end
+
+ def mailbox_class
+ "#{mailbox_name.to_s.camelize}Mailbox".constantize
+ end
+
+ private
+ def ensure_valid_address
+ unless [ Symbol, String, Regexp, Proc ].any? { |klass| address.is_a?(klass) } || address.respond_to?(:match?)
+ raise ArgumentError, "Expected a Symbol, String, Regexp, Proc, or matchable, got #{address.inspect}"
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/routing.rb b/actionmailbox/lib/action_mailbox/routing.rb
new file mode 100644
index 0000000000..58462a44c6
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/routing.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module ActionMailbox
+ # See +ActionMailbox::Base+ for how to specify routing.
+ module Routing
+ extend ActiveSupport::Concern
+
+ included do
+ cattr_accessor :router, default: ActionMailbox::Router.new
+ end
+
+ class_methods do
+ def routing(routes)
+ router.add_routes(routes)
+ end
+
+ def route(inbound_email)
+ router.route(inbound_email)
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/test_case.rb b/actionmailbox/lib/action_mailbox/test_case.rb
new file mode 100644
index 0000000000..5e78e428d3
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/test_case.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require "action_mailbox/test_helper"
+require "active_support/test_case"
+
+module ActionMailbox
+ class TestCase < ActiveSupport::TestCase
+ include ActionMailbox::TestHelper
+ end
+end
+
+ActiveSupport.run_load_hooks :action_mailbox_test_case, ActionMailbox::TestCase
diff --git a/actionmailbox/lib/action_mailbox/test_helper.rb b/actionmailbox/lib/action_mailbox/test_helper.rb
new file mode 100644
index 0000000000..0ec9152844
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/test_helper.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "mail"
+
+module ActionMailbox
+ module TestHelper
+ # Create an +InboundEmail+ record using an eml fixture in the format of message/rfc822
+ # referenced with +fixture_name+ located in +test/fixtures/files/fixture_name+.
+ def create_inbound_email_from_fixture(fixture_name, status: :processing)
+ create_inbound_email_from_source file_fixture(fixture_name).read, status: status
+ end
+
+ # Create an +InboundEmail+ by specifying it using +Mail.new+ options. Example:
+ #
+ # create_inbound_email_from_mail(from: "david@loudthinking.com", subject: "Hello!")
+ def create_inbound_email_from_mail(status: :processing, **mail_options)
+ create_inbound_email_from_source Mail.new(mail_options).to_s, status: status
+ end
+
+ # Create an +InboundEmail+ using the raw rfc822 +source+ as text.
+ def create_inbound_email_from_source(source, status: :processing)
+ ActionMailbox::InboundEmail.create_and_extract_message_id! source, status: status
+ end
+
+
+ # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_fixture+
+ # and immediately route it to processing.
+ def receive_inbound_email_from_fixture(*args)
+ create_inbound_email_from_fixture(*args).tap(&:route)
+ end
+
+ # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_mail+
+ # and immediately route it to processing.
+ def receive_inbound_email_from_mail(**kwargs)
+ create_inbound_email_from_mail(**kwargs).tap(&:route)
+ end
+
+ # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_source+
+ # and immediately route it to processing.
+ def receive_inbound_email_from_source(**kwargs)
+ create_inbound_email_from_source(**kwargs).tap(&:route)
+ end
+ end
+end
diff --git a/actionmailbox/lib/action_mailbox/version.rb b/actionmailbox/lib/action_mailbox/version.rb
new file mode 100644
index 0000000000..e65d27f5dd
--- /dev/null
+++ b/actionmailbox/lib/action_mailbox/version.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require_relative "gem_version"
+
+module ActionMailbox
+ # Returns the currently-loaded version of Action Mailbox as a <tt>Gem::Version</tt>.
+ def self.version
+ gem_version
+ end
+end
diff --git a/actionmailbox/lib/rails/generators/installer.rb b/actionmailbox/lib/rails/generators/installer.rb
new file mode 100644
index 0000000000..25cf528ef5
--- /dev/null
+++ b/actionmailbox/lib/rails/generators/installer.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+say "Copying application_mailbox.rb to app/mailboxes"
+copy_file "#{__dir__}/mailbox/templates/application_mailbox.rb", "app/mailboxes/application_mailbox.rb"
+
+environment <<~end_of_config, env: "production"
+ # Prepare the ingress controller used to receive mail
+ # config.action_mailbox.ingress = :amazon
+
+end_of_config
diff --git a/actionmailbox/lib/rails/generators/mailbox/USAGE b/actionmailbox/lib/rails/generators/mailbox/USAGE
new file mode 100644
index 0000000000..d679dd63ae
--- /dev/null
+++ b/actionmailbox/lib/rails/generators/mailbox/USAGE
@@ -0,0 +1,12 @@
+Description:
+============
+ Stubs out a new mailbox class in app/mailboxes and invokes your template
+ engine and test framework generators.
+
+Example:
+========
+ rails generate mailbox inbox
+
+ creates a InboxMailbox class and test:
+ Mailbox: app/mailboxes/inbox_mailbox.rb
+ Test: test/mailboxes/inbox_mailbox_test.rb
diff --git a/actionmailbox/lib/rails/generators/mailbox/mailbox_generator.rb b/actionmailbox/lib/rails/generators/mailbox/mailbox_generator.rb
new file mode 100644
index 0000000000..c2c403b8f6
--- /dev/null
+++ b/actionmailbox/lib/rails/generators/mailbox/mailbox_generator.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Rails
+ module Generators
+ class MailboxGenerator < NamedBase
+ source_root File.expand_path("templates", __dir__)
+
+ check_class_collision suffix: "Mailbox"
+
+ def create_mailbox_file
+ template "mailbox.rb", File.join("app/mailboxes", class_path, "#{file_name}_mailbox.rb")
+
+ in_root do
+ if behavior == :invoke && !File.exist?(application_mailbox_file_name)
+ template "application_mailbox.rb", application_mailbox_file_name
+ end
+ end
+ end
+
+ hook_for :test_framework
+
+ private
+ def file_name # :doc:
+ @_file_name ||= super.sub(/_mailbox\z/i, "")
+ end
+
+ def application_mailbox_file_name
+ "app/mailboxes/application_mailbox.rb"
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt b/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt
new file mode 100644
index 0000000000..ac22d03cd2
--- /dev/null
+++ b/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt
@@ -0,0 +1,3 @@
+class ApplicationMailbox < ActionMailbox::Base
+ # routing /something/i => :somewhere
+end
diff --git a/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt b/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt
new file mode 100644
index 0000000000..110b3b9d7e
--- /dev/null
+++ b/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt
@@ -0,0 +1,4 @@
+class <%= class_name %>Mailbox < ApplicationMailbox
+ def process
+ end
+end
diff --git a/actionmailbox/lib/rails/generators/test_unit/mailbox_generator.rb b/actionmailbox/lib/rails/generators/test_unit/mailbox_generator.rb
new file mode 100644
index 0000000000..2ec7d11a2f
--- /dev/null
+++ b/actionmailbox/lib/rails/generators/test_unit/mailbox_generator.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module TestUnit
+ module Generators
+ class MailboxGenerator < ::Rails::Generators::NamedBase
+ source_root File.expand_path("templates", __dir__)
+
+ check_class_collision suffix: "MailboxTest"
+
+ def create_test_files
+ template "mailbox_test.rb", File.join("test/mailboxes", class_path, "#{file_name}_mailbox_test.rb")
+ end
+
+ private
+ def file_name # :doc:
+ @_file_name ||= super.sub(/_mailbox\z/i, "")
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt b/actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt
new file mode 100644
index 0000000000..0b51f29fe4
--- /dev/null
+++ b/actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class <%= class_name %>MailboxTest < ActionMailbox::TestCase
+ # test "receive mail" do
+ # receive_inbound_email_from_mail \
+ # to: '"someone" <someone@example.com>',
+ # from: '"else" <else@example.com>',
+ # subject: "Hello world!",
+ # body: "Hello?"
+ # end
+end
diff --git a/actionmailbox/lib/tasks/ingress.rake b/actionmailbox/lib/tasks/ingress.rake
new file mode 100644
index 0000000000..43b613ea12
--- /dev/null
+++ b/actionmailbox/lib/tasks/ingress.rake
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+namespace :action_mailbox do
+ namespace :ingress do
+ task :environment do
+ require "active_support"
+ require "active_support/core_ext/object/blank"
+ require "action_mailbox/relayer"
+ end
+
+ desc "Relay an inbound email from Exim to Action Mailbox (URL and INGRESS_PASSWORD required)"
+ task exim: "action_mailbox:ingress:environment" do
+ url, password = ENV.values_at("URL", "INGRESS_PASSWORD")
+
+ if url.blank? || password.blank?
+ print "URL and INGRESS_PASSWORD are required"
+ exit 64 # EX_USAGE
+ end
+
+ ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result|
+ print result.message
+
+ case
+ when result.success?
+ exit 0
+ when result.transient_failure?
+ exit 75 # EX_TEMPFAIL
+ else
+ exit 69 # EX_UNAVAILABLE
+ end
+ end
+ end
+
+ desc "Relay an inbound email from Postfix to Action Mailbox (URL and INGRESS_PASSWORD required)"
+ task postfix: "action_mailbox:ingress:environment" do
+ url, password = ENV.values_at("URL", "INGRESS_PASSWORD")
+
+ if url.blank? || password.blank?
+ print "4.3.5 URL and INGRESS_PASSWORD are required"
+ exit 1
+ end
+
+ ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result|
+ print "#{result.status_code} #{result.message}"
+ exit result.success?
+ end
+ end
+
+ desc "Relay an inbound email from Qmail to Action Mailbox (URL and INGRESS_PASSWORD required)"
+ task qmail: "action_mailbox:ingress:environment" do
+ url, password = ENV.values_at("URL", "INGRESS_PASSWORD")
+
+ if url.blank? || password.blank?
+ print "URL and INGRESS_PASSWORD are required"
+ exit 111
+ end
+
+ ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result|
+ print result.message
+
+ case
+ when result.success?
+ exit 0
+ when result.transient_failure?
+ exit 111
+ else
+ exit 100
+ end
+ end
+ end
+ end
+end
diff --git a/actionmailbox/lib/tasks/install.rake b/actionmailbox/lib/tasks/install.rake
new file mode 100644
index 0000000000..0885e2d6a5
--- /dev/null
+++ b/actionmailbox/lib/tasks/install.rake
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+namespace :action_mailbox do
+ # Prevent migration installation task from showing up twice.
+ Rake::Task["install:migrations"].clear_comments
+
+ desc "Copy over the migration"
+ task install: %w[ environment run_installer copy_migrations ]
+
+ task :run_installer do
+ installer_template = File.expand_path("../rails/generators/installer.rb", __dir__)
+ system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{installer_template}"
+ end
+
+ task :copy_migrations do
+ Rake::Task["active_storage:install:migrations"].invoke
+ Rake::Task["railties:install:migrations"].reenable # Otherwise you can't run 2 migration copy tasks in one invocation
+ Rake::Task["action_mailbox:install:migrations"].invoke
+ end
+end
diff --git a/actionmailbox/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000..e10985553e
--- /dev/null
+++ b/actionmailbox/test/controllers/ingresses/amazon/inbound_emails_controller_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+ActionMailbox::Ingresses::Amazon::InboundEmailsController.verifier =
+ Module.new { def self.authentic?(message); true; end }
+
+class ActionMailbox::Ingresses::Amazon::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :amazon }
+
+ test "receiving an inbound email from Amazon" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_amazon_inbound_emails_url, params: { content: file_fixture("../files/welcome.eml").read }, as: :json
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+end
diff --git a/actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000..3f225b8953
--- /dev/null
+++ b/actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+ENV["MAILGUN_INGRESS_API_KEY"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL"
+
+class ActionMailbox::Ingresses::Mailgun::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :mailgun }
+
+ test "receiving an inbound email from Mailgun" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+
+ test "rejecting a delayed inbound email from Mailgun" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ travel_to "2018-10-09 15:26:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "rejecting a forged inbound email from Mailgun" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "Zx8mJBiGmiiyyfWnho3zKyjCg2pxLARoCuBM7X9AKCioShGiMX",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "raising when the configured Mailgun API key is nil" do
+ switch_key_to nil do
+ assert_raises ArgumentError do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+ end
+ end
+
+ test "raising when the configured Mailgun API key is blank" do
+ switch_key_to "" do
+ assert_raises ArgumentError do
+ travel_to "2018-10-09 15:15:00 EDT"
+ post rails_mailgun_inbound_emails_url, params: {
+ timestamp: 1539112500,
+ token: "7VwW7k6Ak7zcTwoSoNm7aTtbk1g67MKAnsYLfUB7PdszbgR5Xi",
+ signature: "ef24c5225322217bb065b80bb54eb4f9206d764e3e16abab07f0a64d1cf477cc",
+ "body-mime" => file_fixture("../files/welcome.eml").read
+ }
+ end
+ end
+ end
+
+ private
+ def switch_key_to(new_key)
+ previous_key, ENV["MAILGUN_INGRESS_API_KEY"] = ENV["MAILGUN_INGRESS_API_KEY"], new_key
+ yield
+ ensure
+ ENV["MAILGUN_INGRESS_API_KEY"] = previous_key
+ end
+end
diff --git a/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000..40f956c930
--- /dev/null
+++ b/actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+ENV["MANDRILL_INGRESS_API_KEY"] = "1l9Qf7lutEf7h73VXfBwhw"
+
+class ActionMailbox::Ingresses::Mandrill::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ ActionMailbox.ingress = :mandrill
+ @events = JSON.generate([{ event: "inbound", msg: { raw_msg: file_fixture("../files/welcome.eml").read } }])
+ end
+
+ test "receiving an inbound email from Mandrill" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_mandrill_inbound_emails_url,
+ headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events }
+ end
+
+ assert_response :ok
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+
+ test "rejecting a forged inbound email from Mandrill" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_mandrill_inbound_emails_url,
+ headers: { "X-Mandrill-Signature" => "forged" }, params: { mandrill_events: @events }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "raising when Mandrill API key is nil" do
+ switch_key_to nil do
+ assert_raises ArgumentError do
+ post rails_mandrill_inbound_emails_url,
+ headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events }
+ end
+ end
+ end
+
+ test "raising when Mandrill API key is blank" do
+ switch_key_to "" do
+ assert_raises ArgumentError do
+ post rails_mandrill_inbound_emails_url,
+ headers: { "X-Mandrill-Signature" => "gldscd2tAb/G+DmpiLcwukkLrC4=" }, params: { mandrill_events: @events }
+ end
+ end
+ end
+
+ private
+ def switch_key_to(new_key)
+ previous_key, ENV["MANDRILL_INGRESS_API_KEY"] = ENV["MANDRILL_INGRESS_API_KEY"], new_key
+ yield
+ ensure
+ ENV["MANDRILL_INGRESS_API_KEY"] = previous_key
+ end
+end
diff --git a/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000..11b579b39c
--- /dev/null
+++ b/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionMailbox::Ingresses::Postmark::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :postmark }
+
+ test "receiving an inbound email from Postmark" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read }
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+
+ test "rejecting when RawEmail param is missing" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: { From: "someone@example.com" }
+ end
+
+ assert_response :unprocessable_entity
+ end
+
+ test "rejecting an unauthorized inbound email from Postmark" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_postmark_inbound_emails_url, params: { RawEmail: file_fixture("../files/welcome.eml").read }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "raising when the configured password is nil" do
+ switch_password_to nil do
+ assert_raises ArgumentError do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read }
+ end
+ end
+ end
+
+ test "raising when the configured password is blank" do
+ switch_password_to "" do
+ assert_raises ArgumentError do
+ post rails_postmark_inbound_emails_url,
+ headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read }
+ end
+ end
+ end
+end
diff --git a/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000..67c5993f7f
--- /dev/null
+++ b/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionMailbox::Ingresses::Relay::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :relay }
+
+ test "receiving an inbound email relayed from an SMTP server" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
+ params: file_fixture("../files/welcome.eml").read
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+
+ test "rejecting an unauthorized inbound email" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_relay_inbound_emails_url, headers: { "Content-Type" => "message/rfc822" },
+ params: file_fixture("../files/welcome.eml").read
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "rejecting an inbound email of an unsupported media type" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "text/plain" },
+ params: file_fixture("../files/welcome.eml").read
+ end
+
+ assert_response :unsupported_media_type
+ end
+
+ test "raising when the configured password is nil" do
+ switch_password_to nil do
+ assert_raises ArgumentError do
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
+ params: file_fixture("../files/welcome.eml").read
+ end
+ end
+ end
+
+ test "raising when the configured password is blank" do
+ switch_password_to "" do
+ assert_raises ArgumentError do
+ post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" },
+ params: file_fixture("../files/welcome.eml").read
+ end
+ end
+ end
+end
diff --git a/actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000..59a87e9248
--- /dev/null
+++ b/actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionMailbox::Ingresses::Sendgrid::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ setup { ActionMailbox.ingress = :sendgrid }
+
+ test "receiving an inbound email from Sendgrid" do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_sendgrid_inbound_emails_url,
+ headers: { authorization: credentials }, params: { email: file_fixture("../files/welcome.eml").read }
+ end
+
+ assert_response :no_content
+
+ inbound_email = ActionMailbox::InboundEmail.last
+ assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+
+ test "rejecting an unauthorized inbound email from Sendgrid" do
+ assert_no_difference -> { ActionMailbox::InboundEmail.count } do
+ post rails_sendgrid_inbound_emails_url, params: { email: file_fixture("../files/welcome.eml").read }
+ end
+
+ assert_response :unauthorized
+ end
+
+ test "raising when the configured password is nil" do
+ switch_password_to nil do
+ assert_raises ArgumentError do
+ post rails_sendgrid_inbound_emails_url,
+ headers: { authorization: credentials }, params: { email: file_fixture("../files/welcome.eml").read }
+ end
+ end
+ end
+
+ test "raising when the configured password is blank" do
+ switch_password_to "" do
+ assert_raises ArgumentError do
+ post rails_sendgrid_inbound_emails_url,
+ headers: { authorization: credentials }, params: { email: file_fixture("../files/welcome.eml").read }
+ end
+ end
+ end
+end
diff --git a/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb
new file mode 100644
index 0000000000..fcd9ad839f
--- /dev/null
+++ b/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class Rails::Conductor::ActionMailbox::InboundEmailsControllerTest < ActionDispatch::IntegrationTest
+ test "create inbound email" do
+ with_rails_env("development") do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_conductor_inbound_emails_path, params: {
+ mail: {
+ from: "Jason Fried <jason@37signals.com>",
+ to: "Replies <replies@example.com>",
+ in_reply_to: "<4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>",
+ subject: "Hey there",
+ body: "How's it going?"
+ }
+ }
+ end
+
+ mail = ActionMailbox::InboundEmail.last.mail
+ assert_equal %w[ jason@37signals.com ], mail.from
+ assert_equal %w[ replies@example.com ], mail.to
+ assert_equal "4e6e35f5a38b4_479f13bb90078178@small-app-01.mail", mail.in_reply_to
+ assert_equal "Hey there", mail.subject
+ assert_equal "How's it going?", mail.body.decoded
+ end
+ end
+
+ test "create inbound email with attachments" do
+ with_rails_env("development") do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_conductor_inbound_emails_path, params: {
+ mail: {
+ from: "Jason Fried <jason@37signals.com>",
+ to: "Replies <replies@example.com>",
+ subject: "Let's debate some attachments",
+ body: "Let's talk about these images:",
+ attachments: [ fixture_file_upload("files/avatar1.jpeg"), fixture_file_upload("files/avatar2.jpeg") ]
+ }
+ }
+ end
+
+ mail = ActionMailbox::InboundEmail.last.mail
+ assert_equal "Let's talk about these images:", mail.text_part.decoded
+ assert_equal 2, mail.attachments.count
+ end
+ end
+
+ private
+ def with_rails_env(env)
+ old_rails_env = Rails.env
+ Rails.env = env
+ yield
+ ensure
+ Rails.env = old_rails_env
+ end
+end
diff --git a/actionmailbox/test/dummy/.babelrc b/actionmailbox/test/dummy/.babelrc
new file mode 100644
index 0000000000..ded31c0d80
--- /dev/null
+++ b/actionmailbox/test/dummy/.babelrc
@@ -0,0 +1,18 @@
+{
+ "presets": [
+ ["env", {
+ "modules": false,
+ "targets": {
+ "browsers": "> 1%",
+ "uglify": true
+ },
+ "useBuiltIns": true
+ }]
+ ],
+
+ "plugins": [
+ "syntax-dynamic-import",
+ "transform-object-rest-spread",
+ ["transform-class-properties", { "spec": true }]
+ ]
+}
diff --git a/actionmailbox/test/dummy/.postcssrc.yml b/actionmailbox/test/dummy/.postcssrc.yml
new file mode 100644
index 0000000000..150dac3c6c
--- /dev/null
+++ b/actionmailbox/test/dummy/.postcssrc.yml
@@ -0,0 +1,3 @@
+plugins:
+ postcss-import: {}
+ postcss-cssnext: {}
diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/actionmailbox/test/dummy/Rakefile
index e85f913914..e85f913914 100644
--- a/railties/lib/rails/generators/rails/app/templates/Rakefile
+++ b/actionmailbox/test/dummy/Rakefile
diff --git a/actionmailbox/test/dummy/app/assets/config/manifest.js b/actionmailbox/test/dummy/app/assets/config/manifest.js
new file mode 100644
index 0000000000..b16e53d6d5
--- /dev/null
+++ b/actionmailbox/test/dummy/app/assets/config/manifest.js
@@ -0,0 +1,3 @@
+//= link_tree ../images
+//= link_directory ../javascripts .js
+//= link_directory ../stylesheets .css
diff --git a/actionpack/test/tmp/.gitignore b/actionmailbox/test/dummy/app/assets/images/.keep
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/tmp/.gitignore
+++ b/actionmailbox/test/dummy/app/assets/images/.keep
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css b/actionmailbox/test/dummy/app/assets/stylesheets/application.css
index 0ebd7fe829..0ebd7fe829 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css
+++ b/actionmailbox/test/dummy/app/assets/stylesheets/application.css
diff --git a/actionmailbox/test/dummy/app/assets/stylesheets/scaffold.css b/actionmailbox/test/dummy/app/assets/stylesheets/scaffold.css
new file mode 100644
index 0000000000..cd4f3de38d
--- /dev/null
+++ b/actionmailbox/test/dummy/app/assets/stylesheets/scaffold.css
@@ -0,0 +1,80 @@
+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;
+}
+
+pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+}
+
+a {
+ color: #000;
+}
+
+a:visited {
+ color: #666;
+}
+
+a:hover {
+ color: #fff;
+ background-color: #000;
+}
+
+th {
+ padding-bottom: 5px;
+}
+
+td {
+ padding: 0 5px 7px;
+}
+
+div.field,
+div.actions {
+ margin-bottom: 10px;
+}
+
+#notice {
+ color: green;
+}
+
+.field_with_errors {
+ padding: 2px;
+ background-color: red;
+ display: table;
+}
+
+#error_explanation {
+ width: 450px;
+ border: 2px solid red;
+ padding: 7px 7px 0;
+ margin-bottom: 20px;
+ background-color: #f0f0f0;
+}
+
+#error_explanation h2 {
+ text-align: left;
+ font-weight: bold;
+ padding: 5px 5px 5px 15px;
+ font-size: 12px;
+ margin: -7px -7px 0;
+ background-color: #c00;
+ color: #fff;
+}
+
+#error_explanation ul li {
+ font-size: 12px;
+ list-style: square;
+}
+
+label {
+ display: block;
+}
diff --git a/actionmailbox/test/dummy/app/channels/application_cable/channel.rb b/actionmailbox/test/dummy/app/channels/application_cable/channel.rb
new file mode 100644
index 0000000000..d672697283
--- /dev/null
+++ b/actionmailbox/test/dummy/app/channels/application_cable/channel.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/actionmailbox/test/dummy/app/channels/application_cable/connection.rb b/actionmailbox/test/dummy/app/channels/application_cable/connection.rb
new file mode 100644
index 0000000000..0ff5442f47
--- /dev/null
+++ b/actionmailbox/test/dummy/app/channels/application_cable/connection.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ end
+end
diff --git a/actionmailbox/test/dummy/app/controllers/application_controller.rb b/actionmailbox/test/dummy/app/controllers/application_controller.rb
new file mode 100644
index 0000000000..09705d12ab
--- /dev/null
+++ b/actionmailbox/test/dummy/app/controllers/application_controller.rb
@@ -0,0 +1,2 @@
+class ApplicationController < ActionController::Base
+end
diff --git a/actionview/test/tmp/.gitkeep b/actionmailbox/test/dummy/app/controllers/concerns/.keep
index e69de29bb2..e69de29bb2 100644
--- a/actionview/test/tmp/.gitkeep
+++ b/actionmailbox/test/dummy/app/controllers/concerns/.keep
diff --git a/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb b/actionmailbox/test/dummy/app/helpers/application_helper.rb
index de6be7945c..de6be7945c 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb
+++ b/actionmailbox/test/dummy/app/helpers/application_helper.rb
diff --git a/activerecord/test/migrations/empty/.gitkeep b/actionmailbox/test/dummy/app/javascript/packs/application.js
index e69de29bb2..e69de29bb2 100644
--- a/activerecord/test/migrations/empty/.gitkeep
+++ b/actionmailbox/test/dummy/app/javascript/packs/application.js
diff --git a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb b/actionmailbox/test/dummy/app/jobs/application_job.rb
index a009ace51c..a009ace51c 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb
+++ b/actionmailbox/test/dummy/app/jobs/application_job.rb
diff --git a/actionmailbox/test/dummy/app/mailboxes/application_mailbox.rb b/actionmailbox/test/dummy/app/mailboxes/application_mailbox.rb
new file mode 100644
index 0000000000..be51eb3639
--- /dev/null
+++ b/actionmailbox/test/dummy/app/mailboxes/application_mailbox.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ApplicationMailbox < ActionMailbox::Base
+ # routing /something/i => :somewhere
+end
diff --git a/actionmailbox/test/dummy/app/mailboxes/messages_mailbox.rb b/actionmailbox/test/dummy/app/mailboxes/messages_mailbox.rb
new file mode 100644
index 0000000000..1a046ee13d
--- /dev/null
+++ b/actionmailbox/test/dummy/app/mailboxes/messages_mailbox.rb
@@ -0,0 +1,4 @@
+class MessagesMailbox < ApplicationMailbox
+ def process
+ end
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb b/actionmailbox/test/dummy/app/mailers/application_mailer.rb
index 286b2239d1..286b2239d1 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/mailers/application_mailer.rb
+++ b/actionmailbox/test/dummy/app/mailers/application_mailer.rb
diff --git a/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb b/actionmailbox/test/dummy/app/models/application_record.rb
index 10a4cba84d..10a4cba84d 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/models/application_record.rb
+++ b/actionmailbox/test/dummy/app/models/application_record.rb
diff --git a/actionmailbox/test/dummy/app/models/concerns/.keep b/actionmailbox/test/dummy/app/models/concerns/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionmailbox/test/dummy/app/models/concerns/.keep
diff --git a/actionmailbox/test/dummy/app/views/layouts/application.html.erb b/actionmailbox/test/dummy/app/views/layouts/application.html.erb
new file mode 100644
index 0000000000..f221f512b7
--- /dev/null
+++ b/actionmailbox/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_pack_tag 'application' %>
+ </head>
+
+ <body>
+ <%= yield %>
+ </body>
+</html>
diff --git a/actionmailbox/test/dummy/app/views/layouts/mailer.html.erb b/actionmailbox/test/dummy/app/views/layouts/mailer.html.erb
new file mode 100644
index 0000000000..cbd34d2e9d
--- /dev/null
+++ b/actionmailbox/test/dummy/app/views/layouts/mailer.html.erb
@@ -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/actionmailbox/test/dummy/app/views/layouts/mailer.text.erb b/actionmailbox/test/dummy/app/views/layouts/mailer.text.erb
new file mode 100644
index 0000000000..37f0bddbd7
--- /dev/null
+++ b/actionmailbox/test/dummy/app/views/layouts/mailer.text.erb
@@ -0,0 +1 @@
+<%= yield %>
diff --git a/actionmailbox/test/dummy/bin/bundle b/actionmailbox/test/dummy/bin/bundle
new file mode 100755
index 0000000000..f19acf5b5c
--- /dev/null
+++ b/actionmailbox/test/dummy/bin/bundle
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+load Gem.bin_path('bundler', 'bundle')
diff --git a/actionmailbox/test/dummy/bin/rails b/actionmailbox/test/dummy/bin/rails
new file mode 100755
index 0000000000..0739660237
--- /dev/null
+++ b/actionmailbox/test/dummy/bin/rails
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path('../config/application', __dir__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/actionmailbox/test/dummy/bin/rake b/actionmailbox/test/dummy/bin/rake
new file mode 100755
index 0000000000..17240489f6
--- /dev/null
+++ b/actionmailbox/test/dummy/bin/rake
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/actionmailbox/test/dummy/bin/setup b/actionmailbox/test/dummy/bin/setup
new file mode 100755
index 0000000000..94fd4d7977
--- /dev/null
+++ b/actionmailbox/test/dummy/bin/setup
@@ -0,0 +1,36 @@
+#!/usr/bin/env ruby
+require 'fileutils'
+include FileUtils
+
+# path to your application root.
+APP_ROOT = File.expand_path('..', __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+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')
+
+ # Install JavaScript dependencies if using Yarn
+ # system('bin/yarn')
+
+ # 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! 'bin/rails db:setup'
+
+ 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/actionmailbox/test/dummy/bin/update b/actionmailbox/test/dummy/bin/update
new file mode 100755
index 0000000000..58bfaed518
--- /dev/null
+++ b/actionmailbox/test/dummy/bin/update
@@ -0,0 +1,31 @@
+#!/usr/bin/env ruby
+require 'fileutils'
+include FileUtils
+
+# path to your application root.
+APP_ROOT = File.expand_path('..', __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+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')
+
+ # Install JavaScript dependencies if using Yarn
+ # system('bin/yarn')
+
+ puts "\n== Updating database =="
+ system! 'bin/rails db:migrate'
+
+ 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/actionmailbox/test/dummy/bin/yarn b/actionmailbox/test/dummy/bin/yarn
new file mode 100755
index 0000000000..460dd565b4
--- /dev/null
+++ b/actionmailbox/test/dummy/bin/yarn
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+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/actionmailbox/test/dummy/config.ru
index f7ba0b527b..f7ba0b527b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config.ru
+++ b/actionmailbox/test/dummy/config.ru
diff --git a/actionmailbox/test/dummy/config/application.rb b/actionmailbox/test/dummy/config/application.rb
new file mode 100644
index 0000000000..ea8ab1fc5e
--- /dev/null
+++ b/actionmailbox/test/dummy/config/application.rb
@@ -0,0 +1,17 @@
+require_relative 'boot'
+
+require 'rails/all'
+
+Bundler.require(*Rails.groups)
+
+module Dummy
+ class Application < Rails::Application
+ # Initialize configuration defaults for originally generated Rails version.
+ config.load_defaults 6.0
+
+ # 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.
+ end
+end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb b/actionmailbox/test/dummy/config/boot.rb
index c9aef85d40..c9aef85d40 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb
+++ b/actionmailbox/test/dummy/config/boot.rb
diff --git a/actionmailbox/test/dummy/config/cable.yml b/actionmailbox/test/dummy/config/cable.yml
new file mode 100644
index 0000000000..1cd0f8363e
--- /dev/null
+++ b/actionmailbox/test/dummy/config/cable.yml
@@ -0,0 +1,10 @@
+development:
+ adapter: async
+
+test:
+ adapter: async
+
+production:
+ adapter: redis
+ url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
+ channel_prefix: dummy_production
diff --git a/actionmailbox/test/dummy/config/database.yml b/actionmailbox/test/dummy/config/database.yml
new file mode 100644
index 0000000000..0d02f24980
--- /dev/null
+++ b/actionmailbox/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/railties/lib/rails/generators/rails/app/templates/config/environment.rb b/actionmailbox/test/dummy/config/environment.rb
index 426333bb46..426333bb46 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environment.rb
+++ b/actionmailbox/test/dummy/config/environment.rb
diff --git a/actionmailbox/test/dummy/config/environments/development.rb b/actionmailbox/test/dummy/config/environments/development.rb
new file mode 100644
index 0000000000..f09b9a00c4
--- /dev/null
+++ b/actionmailbox/test/dummy/config/environments/development.rb
@@ -0,0 +1,63 @@
+Rails.application.configure do
+ # Verifies that versions and hashed value of the package contents in the project's package.json
+ # config.webpacker.check_yarn_integrity = true
+ # 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.
+ # 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
+
+ # Store uploaded files on the local file system (see config/storage.yml for options)
+ config.active_storage.service = :local
+
+ # Don't care if the mailer can't send.
+ config.action_mailer.raise_delivery_errors = false
+
+ config.action_mailer.perform_caching = false
+
+ # 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
+
+ # Highlight code that triggered database queries in logs.
+ config.active_record.verbose_query_logs = true
+
+ # 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/actionmailbox/test/dummy/config/environments/production.rb b/actionmailbox/test/dummy/config/environments/production.rb
new file mode 100644
index 0000000000..1731582220
--- /dev/null
+++ b/actionmailbox/test/dummy/config/environments/production.rb
@@ -0,0 +1,99 @@
+Rails.application.configure do
+ # Prepare the ingress controller used to receive mail
+ # config.action_mailbox.ingress = :amazon
+
+ # Verifies that versions and hashed value of the package contents in the project's package.json
+ config.webpacker.check_yarn_integrity = false
+ # 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
+
+ # 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.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
+
+ # Compress JavaScripts and CSS.
+ config.assets.js_compressor = :uglifier
+ # 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
+
+ # Store uploaded files on the local file system (see config/storage.yml for options)
+ config.active_storage.service = :local
+
+ # 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
+
+ # 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 = "dummy_#{Rails.env}"
+
+ 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
+
+ # 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/actionmailbox/test/dummy/config/environments/test.rb b/actionmailbox/test/dummy/config/environments/test.rb
new file mode 100644
index 0000000000..0a38fd3ce9
--- /dev/null
+++ b/actionmailbox/test/dummy/config/environments/test.rb
@@ -0,0 +1,46 @@
+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.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
+
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+
+ # Store uploaded files on the local file system in a temporary directory
+ config.active_storage.service = :test
+
+ 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
+
+ # Print deprecation notices to the stderr.
+ config.active_support.deprecation = :stderr
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+end
diff --git a/actionmailbox/test/dummy/config/initializers/application_controller_renderer.rb b/actionmailbox/test/dummy/config/initializers/application_controller_renderer.rb
new file mode 100644
index 0000000000..89d2efab2b
--- /dev/null
+++ b/actionmailbox/test/dummy/config/initializers/application_controller_renderer.rb
@@ -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/actionmailbox/test/dummy/config/initializers/assets.rb b/actionmailbox/test/dummy/config/initializers/assets.rb
new file mode 100644
index 0000000000..4b828e80cb
--- /dev/null
+++ b/actionmailbox/test/dummy/config/initializers/assets.rb
@@ -0,0 +1,14 @@
+# 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/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb b/actionmailbox/test/dummy/config/initializers/backtrace_silencers.rb
index 59385cdf37..59385cdf37 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb
+++ b/actionmailbox/test/dummy/config/initializers/backtrace_silencers.rb
diff --git a/actionmailbox/test/dummy/config/initializers/content_security_policy.rb b/actionmailbox/test/dummy/config/initializers/content_security_policy.rb
new file mode 100644
index 0000000000..edde7f42b8
--- /dev/null
+++ b/actionmailbox/test/dummy/config/initializers/content_security_policy.rb
@@ -0,0 +1,22 @@
+# 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, :unsafe_inline
+
+# # Specify URI for violation reports
+# # policy.report_uri "/csp-violation-report-endpoint"
+# end
+
+# 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/actionmailbox/test/dummy/config/initializers/cookies_serializer.rb
index 5a6a32d371..5a6a32d371 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb
+++ b/actionmailbox/test/dummy/config/initializers/cookies_serializer.rb
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb b/actionmailbox/test/dummy/config/initializers/filter_parameter_logging.rb
index 4a994e1e7b..4a994e1e7b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb
+++ b/actionmailbox/test/dummy/config/initializers/filter_parameter_logging.rb
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb b/actionmailbox/test/dummy/config/initializers/inflections.rb
index ac033bf9dc..ac033bf9dc 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb
+++ b/actionmailbox/test/dummy/config/initializers/inflections.rb
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb b/actionmailbox/test/dummy/config/initializers/mime_types.rb
index dc1899682b..dc1899682b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb
+++ b/actionmailbox/test/dummy/config/initializers/mime_types.rb
diff --git a/actionmailbox/test/dummy/config/initializers/secret_token.rb b/actionmailbox/test/dummy/config/initializers/secret_token.rb
new file mode 100644
index 0000000000..ada68ba8a2
--- /dev/null
+++ b/actionmailbox/test/dummy/config/initializers/secret_token.rb
@@ -0,0 +1 @@
+Rails.application.config.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
diff --git a/actionmailbox/test/dummy/config/initializers/wrap_parameters.rb b/actionmailbox/test/dummy/config/initializers/wrap_parameters.rb
new file mode 100644
index 0000000000..bbfc3961bf
--- /dev/null
+++ b/actionmailbox/test/dummy/config/initializers/wrap_parameters.rb
@@ -0,0 +1,14 @@
+# 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/actionmailbox/test/dummy/config/locales/en.yml b/actionmailbox/test/dummy/config/locales/en.yml
new file mode 100644
index 0000000000..cf9b342d0a
--- /dev/null
+++ b/actionmailbox/test/dummy/config/locales/en.yml
@@ -0,0 +1,33 @@
+# Files in the config/locales directory are used for internationalization
+# and are automatically loaded by Rails. If you want to use locales other
+# than English, add the necessary files in this directory.
+#
+# To use the locales, use `I18n.t`:
+#
+# I18n.t 'hello'
+#
+# In views, this is aliased to just `t`:
+#
+# <%= t('hello') %>
+#
+# To use a different locale, set it with `I18n.locale`:
+#
+# I18n.locale = :es
+#
+# 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 https://guides.rubyonrails.org/i18n.html.
+
+en:
+ hello: "Hello world"
diff --git a/actionmailbox/test/dummy/config/puma.rb b/actionmailbox/test/dummy/config/puma.rb
new file mode 100644
index 0000000000..71ed69a895
--- /dev/null
+++ b/actionmailbox/test/dummy/config/puma.rb
@@ -0,0 +1,34 @@
+# 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.
+#
+threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
+threads threads_count, threads_count
+
+# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
+#
+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 web server 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/actionmailbox/test/dummy/config/routes.rb b/actionmailbox/test/dummy/config/routes.rb
new file mode 100644
index 0000000000..1fc667e242
--- /dev/null
+++ b/actionmailbox/test/dummy/config/routes.rb
@@ -0,0 +1,4 @@
+Rails.application.routes.draw do
+ resources :messages
+ # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
+end
diff --git a/actionmailbox/test/dummy/config/spring.rb b/actionmailbox/test/dummy/config/spring.rb
new file mode 100644
index 0000000000..9fa7863f99
--- /dev/null
+++ b/actionmailbox/test/dummy/config/spring.rb
@@ -0,0 +1,6 @@
+%w[
+ .ruby-version
+ .rbenv-vars
+ tmp/restart.txt
+ tmp/caching-dev.txt
+].each { |path| Spring.watch(path) }
diff --git a/actionmailbox/test/dummy/config/storage.yml b/actionmailbox/test/dummy/config/storage.yml
new file mode 100644
index 0000000000..53e562e0fc
--- /dev/null
+++ b/actionmailbox/test/dummy/config/storage.yml
@@ -0,0 +1,35 @@
+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
+# path: your_azure_storage_path
+# 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/actionmailbox/test/dummy/config/webpack/development.js b/actionmailbox/test/dummy/config/webpack/development.js
new file mode 100644
index 0000000000..81269f6513
--- /dev/null
+++ b/actionmailbox/test/dummy/config/webpack/development.js
@@ -0,0 +1,3 @@
+const environment = require('./environment')
+
+module.exports = environment.toWebpackConfig()
diff --git a/actionmailbox/test/dummy/config/webpack/environment.js b/actionmailbox/test/dummy/config/webpack/environment.js
new file mode 100644
index 0000000000..d16d9af743
--- /dev/null
+++ b/actionmailbox/test/dummy/config/webpack/environment.js
@@ -0,0 +1,3 @@
+const { environment } = require('@rails/webpacker')
+
+module.exports = environment
diff --git a/actionmailbox/test/dummy/config/webpack/production.js b/actionmailbox/test/dummy/config/webpack/production.js
new file mode 100644
index 0000000000..81269f6513
--- /dev/null
+++ b/actionmailbox/test/dummy/config/webpack/production.js
@@ -0,0 +1,3 @@
+const environment = require('./environment')
+
+module.exports = environment.toWebpackConfig()
diff --git a/actionmailbox/test/dummy/config/webpack/test.js b/actionmailbox/test/dummy/config/webpack/test.js
new file mode 100644
index 0000000000..81269f6513
--- /dev/null
+++ b/actionmailbox/test/dummy/config/webpack/test.js
@@ -0,0 +1,3 @@
+const environment = require('./environment')
+
+module.exports = environment.toWebpackConfig()
diff --git a/actionmailbox/test/dummy/config/webpacker.yml b/actionmailbox/test/dummy/config/webpacker.yml
new file mode 100644
index 0000000000..d3f24e1b4b
--- /dev/null
+++ b/actionmailbox/test/dummy/config/webpacker.yml
@@ -0,0 +1,65 @@
+# 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
+
+ # 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
+ - .png
+ - .svg
+ - .gif
+ - .jpeg
+ - .jpg
+
+development:
+ <<: *default
+ compile: 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/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb b/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb
new file mode 100644
index 0000000000..2bf4335808
--- /dev/null
+++ b/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb
@@ -0,0 +1,13 @@
+class CreateActionMailboxTables < ActiveRecord::Migration[6.0]
+ def change
+ create_table :action_mailbox_inbound_emails do |t|
+ t.integer :status, default: 0, null: false
+ t.string :message_id, null: false
+ t.string :message_checksum, null: false
+
+ t.timestamps
+
+ t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true
+ end
+ end
+end
diff --git a/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb b/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb
new file mode 100644
index 0000000000..0b2ce257c8
--- /dev/null
+++ b/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb
@@ -0,0 +1,27 @@
+# This migration comes from active_storage (originally 20170806125915)
+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/actionmailbox/test/dummy/db/schema.rb b/actionmailbox/test/dummy/db/schema.rb
new file mode 100644
index 0000000000..76c979bcec
--- /dev/null
+++ b/actionmailbox/test/dummy/db/schema.rb
@@ -0,0 +1,46 @@
+# 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.
+#
+# 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.
+
+ActiveRecord::Schema.define(version: 2018_02_12_164506) do
+
+ create_table "action_mailbox_inbound_emails", force: :cascade do |t|
+ t.integer "status", default: 0, null: false
+ t.string "message_id", null: false
+ t.string "message_checksum", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["message_id", "message_checksum"], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true
+ end
+
+ create_table "active_storage_attachments", force: :cascade do |t|
+ t.string "name", null: false
+ t.string "record_type", null: false
+ t.integer "record_id", null: false
+ t.integer "blob_id", null: false
+ t.datetime "created_at", null: false
+ t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
+ t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
+ end
+
+ create_table "active_storage_blobs", force: :cascade 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"], name: "index_active_storage_blobs_on_key", unique: true
+ end
+
+ add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
+end
diff --git a/actionmailbox/test/dummy/lib/assets/.keep b/actionmailbox/test/dummy/lib/assets/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionmailbox/test/dummy/lib/assets/.keep
diff --git a/actionmailbox/test/dummy/log/.keep b/actionmailbox/test/dummy/log/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionmailbox/test/dummy/log/.keep
diff --git a/actionmailbox/test/dummy/package.json b/actionmailbox/test/dummy/package.json
new file mode 100644
index 0000000000..d1f979fcae
--- /dev/null
+++ b/actionmailbox/test/dummy/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "dummy",
+ "private": true,
+ "dependencies": {
+ "@rails/webpacker": "^4.0.2",
+ "activetext": "file:../.."
+ },
+ "devDependencies": {
+ "webpack-dev-server": "^3.2.1"
+ }
+}
diff --git a/actionmailbox/test/dummy/public/404.html b/actionmailbox/test/dummy/public/404.html
new file mode 100644
index 0000000000..2be3af26fc
--- /dev/null
+++ b/actionmailbox/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/actionmailbox/test/dummy/public/422.html b/actionmailbox/test/dummy/public/422.html
new file mode 100644
index 0000000000..c08eac0d1d
--- /dev/null
+++ b/actionmailbox/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/actionmailbox/test/dummy/public/500.html b/actionmailbox/test/dummy/public/500.html
new file mode 100644
index 0000000000..78a030af22
--- /dev/null
+++ b/actionmailbox/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/actionmailbox/test/dummy/public/apple-touch-icon-precomposed.png b/actionmailbox/test/dummy/public/apple-touch-icon-precomposed.png
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionmailbox/test/dummy/public/apple-touch-icon-precomposed.png
diff --git a/actionmailbox/test/dummy/public/apple-touch-icon.png b/actionmailbox/test/dummy/public/apple-touch-icon.png
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionmailbox/test/dummy/public/apple-touch-icon.png
diff --git a/actionmailbox/test/dummy/public/favicon.ico b/actionmailbox/test/dummy/public/favicon.ico
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionmailbox/test/dummy/public/favicon.ico
diff --git a/actionmailbox/test/dummy/storage/.keep b/actionmailbox/test/dummy/storage/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actionmailbox/test/dummy/storage/.keep
diff --git a/actionmailbox/test/dummy/yarn.lock b/actionmailbox/test/dummy/yarn.lock
new file mode 100644
index 0000000000..ba7eb2c28e
--- /dev/null
+++ b/actionmailbox/test/dummy/yarn.lock
@@ -0,0 +1,7578 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
+ integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==
+ dependencies:
+ "@babel/highlight" "^7.0.0"
+
+"@babel/core@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.4.tgz#921a5a13746c21e32445bf0798680e9d11a6530b"
+ integrity sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/generator" "^7.3.4"
+ "@babel/helpers" "^7.2.0"
+ "@babel/parser" "^7.3.4"
+ "@babel/template" "^7.2.2"
+ "@babel/traverse" "^7.3.4"
+ "@babel/types" "^7.3.4"
+ convert-source-map "^1.1.0"
+ debug "^4.1.0"
+ json5 "^2.1.0"
+ lodash "^4.17.11"
+ resolve "^1.3.2"
+ semver "^5.4.1"
+ source-map "^0.5.0"
+
+"@babel/generator@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e"
+ integrity sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg==
+ dependencies:
+ "@babel/types" "^7.3.4"
+ jsesc "^2.5.1"
+ lodash "^4.17.11"
+ source-map "^0.5.0"
+ trim-right "^1.0.1"
+
+"@babel/helper-annotate-as-pure@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32"
+ integrity sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-builder-binary-assignment-operator-visitor@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz#6b69628dfe4087798e0c4ed98e3d4a6b2fbd2f5f"
+ integrity sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==
+ dependencies:
+ "@babel/helper-explode-assignable-expression" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-call-delegate@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz#6a957f105f37755e8645343d3038a22e1449cc4a"
+ integrity sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ==
+ dependencies:
+ "@babel/helper-hoist-variables" "^7.0.0"
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-create-class-features-plugin@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.4.tgz#092711a7a3ad8ea34de3e541644c2ce6af1f6f0c"
+ integrity sha512-uFpzw6L2omjibjxa8VGZsJUPL5wJH0zzGKpoz0ccBkzIa6C8kWNUbiBmQ0rgOKWlHJ6qzmfa6lTiGchiV8SC+g==
+ dependencies:
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-member-expression-to-functions" "^7.0.0"
+ "@babel/helper-optimise-call-expression" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-replace-supers" "^7.3.4"
+ "@babel/helper-split-export-declaration" "^7.0.0"
+
+"@babel/helper-define-map@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz#3b74caec329b3c80c116290887c0dd9ae468c20c"
+ integrity sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg==
+ dependencies:
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/types" "^7.0.0"
+ lodash "^4.17.10"
+
+"@babel/helper-explode-assignable-expression@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz#537fa13f6f1674df745b0c00ec8fe4e99681c8f6"
+ integrity sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==
+ dependencies:
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-function-name@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53"
+ integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==
+ dependencies:
+ "@babel/helper-get-function-arity" "^7.0.0"
+ "@babel/template" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-get-function-arity@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3"
+ integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-hoist-variables@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz#46adc4c5e758645ae7a45deb92bab0918c23bb88"
+ integrity sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-member-expression-to-functions@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz#8cd14b0a0df7ff00f009e7d7a436945f47c7a16f"
+ integrity sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-module-imports@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d"
+ integrity sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-module-transforms@^7.1.0":
+ version "7.2.2"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.2.2.tgz#ab2f8e8d231409f8370c883d20c335190284b963"
+ integrity sha512-YRD7I6Wsv+IHuTPkAmAS4HhY0dkPobgLftHp0cRGZSdrRvmZY8rFvae/GVu3bD00qscuvK3WPHB3YdNpBXUqrA==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@babel/helper-simple-access" "^7.1.0"
+ "@babel/helper-split-export-declaration" "^7.0.0"
+ "@babel/template" "^7.2.2"
+ "@babel/types" "^7.2.2"
+ lodash "^4.17.10"
+
+"@babel/helper-optimise-call-expression@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz#a2920c5702b073c15de51106200aa8cad20497d5"
+ integrity sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-plugin-utils@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250"
+ integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==
+
+"@babel/helper-regex@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.0.0.tgz#2c1718923b57f9bbe64705ffe5640ac64d9bdb27"
+ integrity sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg==
+ dependencies:
+ lodash "^4.17.10"
+
+"@babel/helper-remap-async-to-generator@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz#361d80821b6f38da75bd3f0785ece20a88c5fe7f"
+ integrity sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.0.0"
+ "@babel/helper-wrap-function" "^7.1.0"
+ "@babel/template" "^7.1.0"
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.3.4.tgz#a795208e9b911a6eeb08e5891faacf06e7013e13"
+ integrity sha512-pvObL9WVf2ADs+ePg0jrqlhHoxRXlOa+SHRHzAXIz2xkYuOHfGl+fKxPMaS4Fq+uje8JQPobnertBBvyrWnQ1A==
+ dependencies:
+ "@babel/helper-member-expression-to-functions" "^7.0.0"
+ "@babel/helper-optimise-call-expression" "^7.0.0"
+ "@babel/traverse" "^7.3.4"
+ "@babel/types" "^7.3.4"
+
+"@babel/helper-simple-access@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz#65eeb954c8c245beaa4e859da6188f39d71e585c"
+ integrity sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==
+ dependencies:
+ "@babel/template" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-split-export-declaration@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz#3aae285c0311c2ab095d997b8c9a94cad547d813"
+ integrity sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-wrap-function@^7.1.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa"
+ integrity sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==
+ dependencies:
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/template" "^7.1.0"
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.2.0"
+
+"@babel/helpers@^7.2.0":
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.3.1.tgz#949eec9ea4b45d3210feb7dc1c22db664c9e44b9"
+ integrity sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA==
+ dependencies:
+ "@babel/template" "^7.1.2"
+ "@babel/traverse" "^7.1.5"
+ "@babel/types" "^7.3.0"
+
+"@babel/highlight@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4"
+ integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==
+ dependencies:
+ chalk "^2.0.0"
+ esutils "^2.0.2"
+ js-tokens "^4.0.0"
+
+"@babel/parser@^7.2.2", "@babel/parser@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c"
+ integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ==
+
+"@babel/plugin-proposal-async-generator-functions@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e"
+ integrity sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-remap-async-to-generator" "^7.1.0"
+ "@babel/plugin-syntax-async-generators" "^7.2.0"
+
+"@babel/plugin-proposal-class-properties@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.4.tgz#410f5173b3dc45939f9ab30ca26684d72901405e"
+ integrity sha512-lUf8D3HLs4yYlAo8zjuneLvfxN7qfKv1Yzbj5vjqaqMJxgJA3Ipwp4VUJ+OrOdz53Wbww6ahwB8UhB2HQyLotA==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.3.4"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-proposal-json-strings@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317"
+ integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-syntax-json-strings" "^7.2.0"
+
+"@babel/plugin-proposal-object-rest-spread@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.4.tgz#47f73cf7f2a721aad5c0261205405c642e424654"
+ integrity sha512-j7VQmbbkA+qrzNqbKHrBsW3ddFnOeva6wzSe/zB7T+xaxGc+RCpwo44wCmRixAIGRoIpmVgvzFzNJqQcO3/9RA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
+
+"@babel/plugin-proposal-optional-catch-binding@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5"
+ integrity sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
+
+"@babel/plugin-proposal-unicode-property-regex@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz#abe7281fe46c95ddc143a65e5358647792039520"
+ integrity sha512-LvRVYb7kikuOtIoUeWTkOxQEV1kYvL5B6U3iWEGCzPNRus1MzJweFqORTj+0jkxozkTSYNJozPOddxmqdqsRpw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-regex" "^7.0.0"
+ regexpu-core "^4.2.0"
+
+"@babel/plugin-syntax-async-generators@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f"
+ integrity sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-dynamic-import@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612"
+ integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-json-strings@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470"
+ integrity sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-object-rest-spread@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e"
+ integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-optional-catch-binding@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz#a94013d6eda8908dfe6a477e7f9eda85656ecf5c"
+ integrity sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-arrow-functions@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550"
+ integrity sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-async-to-generator@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.3.4.tgz#4e45408d3c3da231c0e7b823f407a53a7eb3048c"
+ integrity sha512-Y7nCzv2fw/jEZ9f678MuKdMo99MFDJMT/PvD9LisrR5JDFcJH6vYeH6RnjVt3p5tceyGRvTtEN0VOlU+rgHZjA==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-remap-async-to-generator" "^7.1.0"
+
+"@babel/plugin-transform-block-scoped-functions@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz#5d3cc11e8d5ddd752aa64c9148d0db6cb79fd190"
+ integrity sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-block-scoping@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.3.4.tgz#5c22c339de234076eee96c8783b2fed61202c5c4"
+ integrity sha512-blRr2O8IOZLAOJklXLV4WhcEzpYafYQKSGT3+R26lWG41u/FODJuBggehtOwilVAcFu393v3OFj+HmaE6tVjhA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ lodash "^4.17.11"
+
+"@babel/plugin-transform-classes@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.3.4.tgz#dc173cb999c6c5297e0b5f2277fdaaec3739d0cc"
+ integrity sha512-J9fAvCFBkXEvBimgYxCjvaVDzL6thk0j0dBvCeZmIUDBwyt+nv6HfbImsSrWsYXfDNDivyANgJlFXDUWRTZBuA==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.0.0"
+ "@babel/helper-define-map" "^7.1.0"
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-optimise-call-expression" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-replace-supers" "^7.3.4"
+ "@babel/helper-split-export-declaration" "^7.0.0"
+ globals "^11.1.0"
+
+"@babel/plugin-transform-computed-properties@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz#83a7df6a658865b1c8f641d510c6f3af220216da"
+ integrity sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-destructuring@^7.2.0", "@babel/plugin-transform-destructuring@^7.3.2":
+ version "7.3.2"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.3.2.tgz#f2f5520be055ba1c38c41c0e094d8a461dd78f2d"
+ integrity sha512-Lrj/u53Ufqxl/sGxyjsJ2XNtNuEjDyjpqdhMNh5aZ+XFOdThL46KBj27Uem4ggoezSYBxKWAil6Hu8HtwqesYw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-dotall-regex@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.2.0.tgz#f0aabb93d120a8ac61e925ea0ba440812dbe0e49"
+ integrity sha512-sKxnyHfizweTgKZf7XsXu/CNupKhzijptfTM+bozonIuyVrLWVUvYjE2bhuSBML8VQeMxq4Mm63Q9qvcvUcciQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-regex" "^7.0.0"
+ regexpu-core "^4.1.3"
+
+"@babel/plugin-transform-duplicate-keys@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz#d952c4930f312a4dbfff18f0b2914e60c35530b3"
+ integrity sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-exponentiation-operator@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz#a63868289e5b4007f7054d46491af51435766008"
+ integrity sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==
+ dependencies:
+ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-for-of@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.2.0.tgz#ab7468befa80f764bb03d3cb5eef8cc998e1cad9"
+ integrity sha512-Kz7Mt0SsV2tQk6jG5bBv5phVbkd0gd27SgYD4hH1aLMJRchM0dzHaXvrWhVZ+WxAlDoAKZ7Uy3jVTW2mKXQ1WQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-function-name@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.2.0.tgz#f7930362829ff99a3174c39f0afcc024ef59731a"
+ integrity sha512-kWgksow9lHdvBC2Z4mxTsvc7YdY7w/V6B2vy9cTIPtLEE9NhwoWivaxdNM/S37elu5bqlLP/qOY906LukO9lkQ==
+ dependencies:
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-literals@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz#690353e81f9267dad4fd8cfd77eafa86aba53ea1"
+ integrity sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-modules-amd@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz#82a9bce45b95441f617a24011dc89d12da7f4ee6"
+ integrity sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-modules-commonjs@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.2.0.tgz#c4f1933f5991d5145e9cfad1dfd848ea1727f404"
+ integrity sha512-V6y0uaUQrQPXUrmj+hgnks8va2L0zcZymeU7TtWEgdRLNkceafKXEduv7QzgQAE4lT+suwooG9dC7LFhdRAbVQ==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-simple-access" "^7.1.0"
+
+"@babel/plugin-transform-modules-systemjs@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.3.4.tgz#813b34cd9acb6ba70a84939f3680be0eb2e58861"
+ integrity sha512-VZ4+jlGOF36S7TjKs8g4ojp4MEI+ebCQZdswWb/T9I4X84j8OtFAyjXjt/M16iIm5RIZn0UMQgg/VgIwo/87vw==
+ dependencies:
+ "@babel/helper-hoist-variables" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-modules-umd@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz#7678ce75169f0877b8eb2235538c074268dd01ae"
+ integrity sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-named-capturing-groups-regex@^7.3.0":
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.3.0.tgz#140b52985b2d6ef0cb092ef3b29502b990f9cd50"
+ integrity sha512-NxIoNVhk9ZxS+9lSoAQ/LM0V2UEvARLttEHUrRDGKFaAxOYQcrkN/nLRE+BbbicCAvZPl7wMP0X60HsHE5DtQw==
+ dependencies:
+ regexp-tree "^0.1.0"
+
+"@babel/plugin-transform-new-target@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz#ae8fbd89517fa7892d20e6564e641e8770c3aa4a"
+ integrity sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-object-super@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz#b35d4c10f56bab5d650047dad0f1d8e8814b6598"
+ integrity sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-replace-supers" "^7.1.0"
+
+"@babel/plugin-transform-parameters@^7.2.0":
+ version "7.3.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.3.3.tgz#3a873e07114e1a5bee17d04815662c8317f10e30"
+ integrity sha512-IrIP25VvXWu/VlBWTpsjGptpomtIkYrN/3aDp4UKm7xK6UxZY88kcJ1UwETbzHAlwN21MnNfwlar0u8y3KpiXw==
+ dependencies:
+ "@babel/helper-call-delegate" "^7.1.0"
+ "@babel/helper-get-function-arity" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-regenerator@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.3.4.tgz#1601655c362f5b38eead6a52631f5106b29fa46a"
+ integrity sha512-hvJg8EReQvXT6G9H2MvNPXkv9zK36Vxa1+csAVTpE1J3j0zlHplw76uudEbJxgvqZzAq9Yh45FLD4pk5mKRFQA==
+ dependencies:
+ regenerator-transform "^0.13.4"
+
+"@babel/plugin-transform-runtime@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.3.4.tgz#57805ac8c1798d102ecd75c03b024a5b3ea9b431"
+ integrity sha512-PaoARuztAdd5MgeVjAxnIDAIUet5KpogqaefQvPOmPYCxYoaPhautxDh3aO8a4xHsKgT/b9gSxR0BKK1MIewPA==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ resolve "^1.8.1"
+ semver "^5.5.1"
+
+"@babel/plugin-transform-shorthand-properties@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0"
+ integrity sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-spread@^7.2.0":
+ version "7.2.2"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz#3103a9abe22f742b6d406ecd3cd49b774919b406"
+ integrity sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-sticky-regex@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz#a1e454b5995560a9c1e0d537dfc15061fd2687e1"
+ integrity sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-regex" "^7.0.0"
+
+"@babel/plugin-transform-template-literals@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.2.0.tgz#d87ed01b8eaac7a92473f608c97c089de2ba1e5b"
+ integrity sha512-FkPix00J9A/XWXv4VoKJBMeSkyY9x/TqIh76wzcdfl57RJJcf8CehQ08uwfhCDNtRQYtHQKBTwKZDEyjE13Lwg==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-typeof-symbol@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz#117d2bcec2fbf64b4b59d1f9819894682d29f2b2"
+ integrity sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-unicode-regex@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz#4eb8db16f972f8abb5062c161b8b115546ade08b"
+ integrity sha512-m48Y0lMhrbXEJnVUaYly29jRXbQ3ksxPrS1Tg8t+MHqzXhtBYAvI51euOBaoAlZLPHsieY9XPVMf80a5x0cPcA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-regex" "^7.0.0"
+ regexpu-core "^4.1.3"
+
+"@babel/polyfill@^7.2.5":
+ version "7.2.5"
+ resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.2.5.tgz#6c54b964f71ad27edddc567d065e57e87ed7fa7d"
+ integrity sha512-8Y/t3MWThtMLYr0YNC/Q76tqN1w30+b0uQMeFUYauG2UGTR19zyUtFrAzT23zNtBxPp+LbE5E/nwV/q/r3y6ug==
+ dependencies:
+ core-js "^2.5.7"
+ regenerator-runtime "^0.12.0"
+
+"@babel/preset-env@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.4.tgz#887cf38b6d23c82f19b5135298bdb160062e33e1"
+ integrity sha512-2mwqfYMK8weA0g0uBKOt4FE3iEodiHy9/CW0b+nWXcbL+pGzLx8ESYc+j9IIxr6LTDHWKgPm71i9smo02bw+gA==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-proposal-async-generator-functions" "^7.2.0"
+ "@babel/plugin-proposal-json-strings" "^7.2.0"
+ "@babel/plugin-proposal-object-rest-spread" "^7.3.4"
+ "@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
+ "@babel/plugin-proposal-unicode-property-regex" "^7.2.0"
+ "@babel/plugin-syntax-async-generators" "^7.2.0"
+ "@babel/plugin-syntax-json-strings" "^7.2.0"
+ "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
+ "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
+ "@babel/plugin-transform-arrow-functions" "^7.2.0"
+ "@babel/plugin-transform-async-to-generator" "^7.3.4"
+ "@babel/plugin-transform-block-scoped-functions" "^7.2.0"
+ "@babel/plugin-transform-block-scoping" "^7.3.4"
+ "@babel/plugin-transform-classes" "^7.3.4"
+ "@babel/plugin-transform-computed-properties" "^7.2.0"
+ "@babel/plugin-transform-destructuring" "^7.2.0"
+ "@babel/plugin-transform-dotall-regex" "^7.2.0"
+ "@babel/plugin-transform-duplicate-keys" "^7.2.0"
+ "@babel/plugin-transform-exponentiation-operator" "^7.2.0"
+ "@babel/plugin-transform-for-of" "^7.2.0"
+ "@babel/plugin-transform-function-name" "^7.2.0"
+ "@babel/plugin-transform-literals" "^7.2.0"
+ "@babel/plugin-transform-modules-amd" "^7.2.0"
+ "@babel/plugin-transform-modules-commonjs" "^7.2.0"
+ "@babel/plugin-transform-modules-systemjs" "^7.3.4"
+ "@babel/plugin-transform-modules-umd" "^7.2.0"
+ "@babel/plugin-transform-named-capturing-groups-regex" "^7.3.0"
+ "@babel/plugin-transform-new-target" "^7.0.0"
+ "@babel/plugin-transform-object-super" "^7.2.0"
+ "@babel/plugin-transform-parameters" "^7.2.0"
+ "@babel/plugin-transform-regenerator" "^7.3.4"
+ "@babel/plugin-transform-shorthand-properties" "^7.2.0"
+ "@babel/plugin-transform-spread" "^7.2.0"
+ "@babel/plugin-transform-sticky-regex" "^7.2.0"
+ "@babel/plugin-transform-template-literals" "^7.2.0"
+ "@babel/plugin-transform-typeof-symbol" "^7.2.0"
+ "@babel/plugin-transform-unicode-regex" "^7.2.0"
+ browserslist "^4.3.4"
+ invariant "^2.2.2"
+ js-levenshtein "^1.1.3"
+ semver "^5.3.0"
+
+"@babel/runtime@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83"
+ integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==
+ dependencies:
+ regenerator-runtime "^0.12.0"
+
+"@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2":
+ version "7.2.2"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907"
+ integrity sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/parser" "^7.2.2"
+ "@babel/types" "^7.2.2"
+
+"@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06"
+ integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/generator" "^7.3.4"
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-split-export-declaration" "^7.0.0"
+ "@babel/parser" "^7.3.4"
+ "@babel/types" "^7.3.4"
+ debug "^4.1.0"
+ globals "^11.1.0"
+ lodash "^4.17.11"
+
+"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed"
+ integrity sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==
+ dependencies:
+ esutils "^2.0.2"
+ lodash "^4.17.11"
+ to-fast-properties "^2.0.0"
+
+"@csstools/convert-colors@^1.4.0":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
+ integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==
+
+"@rails/webpacker@^4.0.2":
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-4.0.2.tgz#2c2e96527500b060a84159098449ddb1615c65e8"
+ integrity sha512-TDj/+UHnWaEg8X21E3cGKvptm3BbW1aUtOAXtrYwpK9tkiWq+Dc40Gm2RIZW7rU3jxDDBZgPRiqvr5B0dorIVw==
+ dependencies:
+ "@babel/core" "^7.3.4"
+ "@babel/plugin-proposal-class-properties" "^7.3.4"
+ "@babel/plugin-proposal-object-rest-spread" "^7.3.4"
+ "@babel/plugin-syntax-dynamic-import" "^7.2.0"
+ "@babel/plugin-transform-destructuring" "^7.3.2"
+ "@babel/plugin-transform-regenerator" "^7.3.4"
+ "@babel/plugin-transform-runtime" "^7.3.4"
+ "@babel/polyfill" "^7.2.5"
+ "@babel/preset-env" "^7.3.4"
+ "@babel/runtime" "^7.3.4"
+ babel-loader "^8.0.5"
+ babel-plugin-dynamic-import-node "^2.2.0"
+ babel-plugin-macros "^2.5.0"
+ case-sensitive-paths-webpack-plugin "^2.2.0"
+ compression-webpack-plugin "^2.0.0"
+ css-loader "^2.1.0"
+ file-loader "^3.0.1"
+ flatted "^2.0.0"
+ glob "^7.1.3"
+ js-yaml "^3.12.2"
+ mini-css-extract-plugin "^0.5.0"
+ node-sass "^4.11.0"
+ optimize-css-assets-webpack-plugin "^5.0.1"
+ path-complete-extname "^1.0.0"
+ pnp-webpack-plugin "^1.3.1"
+ postcss-flexbugs-fixes "^4.1.0"
+ postcss-import "^12.0.1"
+ postcss-loader "^3.0.0"
+ postcss-preset-env "^6.6.0"
+ postcss-safe-parser "^4.0.1"
+ sass-loader "^7.1.0"
+ style-loader "^0.23.1"
+ terser-webpack-plugin "^1.2.3"
+ webpack "^4.29.6"
+ webpack-assets-manifest "^3.1.1"
+ webpack-cli "^3.2.3"
+ webpack-sources "^1.3.0"
+
+"@types/q@^1.5.1":
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.1.tgz#48fd98c1561fe718b61733daed46ff115b496e18"
+ integrity sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA==
+
+"@webassemblyjs/ast@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359"
+ integrity sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==
+ dependencies:
+ "@webassemblyjs/helper-module-context" "1.8.5"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.5"
+ "@webassemblyjs/wast-parser" "1.8.5"
+
+"@webassemblyjs/floating-point-hex-parser@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz#1ba926a2923613edce496fd5b02e8ce8a5f49721"
+ integrity sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==
+
+"@webassemblyjs/helper-api-error@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz#c49dad22f645227c5edb610bdb9697f1aab721f7"
+ integrity sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==
+
+"@webassemblyjs/helper-buffer@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz#fea93e429863dd5e4338555f42292385a653f204"
+ integrity sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==
+
+"@webassemblyjs/helper-code-frame@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz#9a740ff48e3faa3022b1dff54423df9aa293c25e"
+ integrity sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==
+ dependencies:
+ "@webassemblyjs/wast-printer" "1.8.5"
+
+"@webassemblyjs/helper-fsm@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz#ba0b7d3b3f7e4733da6059c9332275d860702452"
+ integrity sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==
+
+"@webassemblyjs/helper-module-context@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz#def4b9927b0101dc8cbbd8d1edb5b7b9c82eb245"
+ integrity sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ mamacro "^0.0.3"
+
+"@webassemblyjs/helper-wasm-bytecode@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz#537a750eddf5c1e932f3744206551c91c1b93e61"
+ integrity sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==
+
+"@webassemblyjs/helper-wasm-section@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz#74ca6a6bcbe19e50a3b6b462847e69503e6bfcbf"
+ integrity sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-buffer" "1.8.5"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.5"
+ "@webassemblyjs/wasm-gen" "1.8.5"
+
+"@webassemblyjs/ieee754@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz#712329dbef240f36bf57bd2f7b8fb9bf4154421e"
+ integrity sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==
+ dependencies:
+ "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.5.tgz#044edeb34ea679f3e04cd4fd9824d5e35767ae10"
+ integrity sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==
+ dependencies:
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.5.tgz#a8bf3b5d8ffe986c7c1e373ccbdc2a0915f0cedc"
+ integrity sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==
+
+"@webassemblyjs/wasm-edit@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz#962da12aa5acc1c131c81c4232991c82ce56e01a"
+ integrity sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-buffer" "1.8.5"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.5"
+ "@webassemblyjs/helper-wasm-section" "1.8.5"
+ "@webassemblyjs/wasm-gen" "1.8.5"
+ "@webassemblyjs/wasm-opt" "1.8.5"
+ "@webassemblyjs/wasm-parser" "1.8.5"
+ "@webassemblyjs/wast-printer" "1.8.5"
+
+"@webassemblyjs/wasm-gen@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz#54840766c2c1002eb64ed1abe720aded714f98bc"
+ integrity sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.5"
+ "@webassemblyjs/ieee754" "1.8.5"
+ "@webassemblyjs/leb128" "1.8.5"
+ "@webassemblyjs/utf8" "1.8.5"
+
+"@webassemblyjs/wasm-opt@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz#b24d9f6ba50394af1349f510afa8ffcb8a63d264"
+ integrity sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-buffer" "1.8.5"
+ "@webassemblyjs/wasm-gen" "1.8.5"
+ "@webassemblyjs/wasm-parser" "1.8.5"
+
+"@webassemblyjs/wasm-parser@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz#21576f0ec88b91427357b8536383668ef7c66b8d"
+ integrity sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-api-error" "1.8.5"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.5"
+ "@webassemblyjs/ieee754" "1.8.5"
+ "@webassemblyjs/leb128" "1.8.5"
+ "@webassemblyjs/utf8" "1.8.5"
+
+"@webassemblyjs/wast-parser@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz#e10eecd542d0e7bd394f6827c49f3df6d4eefb8c"
+ integrity sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/floating-point-hex-parser" "1.8.5"
+ "@webassemblyjs/helper-api-error" "1.8.5"
+ "@webassemblyjs/helper-code-frame" "1.8.5"
+ "@webassemblyjs/helper-fsm" "1.8.5"
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/wast-printer@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz#114bbc481fd10ca0e23b3560fa812748b0bae5bc"
+ integrity sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/wast-parser" "1.8.5"
+ "@xtuc/long" "4.2.2"
+
+"@xtuc/ieee754@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+ integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+ integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+ integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
+accepts@~1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f"
+ integrity sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=
+ dependencies:
+ mime-types "~2.1.16"
+ negotiator "0.6.1"
+
+acorn-dynamic-import@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948"
+ integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==
+
+acorn@^6.0.5:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f"
+ integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==
+
+"activetext@file:../..":
+ version "0.0.0"
+
+ajv-errors@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
+ integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==
+
+ajv-keywords@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be"
+ integrity sha1-rCsnk5xUPpXSwG5/f1wnvkqlQ74=
+
+ajv@^4.9.1:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+ integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=
+ dependencies:
+ co "^4.6.0"
+ json-stable-stringify "^1.0.1"
+
+ajv@^6.1.0:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.1.1.tgz#978d597fbc2b7d0e5a5c3ddeb149a682f2abfa0e"
+ integrity sha1-l41Zf7wrfQ5aXD3esUmmgvKr+g4=
+ dependencies:
+ fast-deep-equal "^1.0.0"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.3.0"
+
+ajv@^6.5.5:
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
+ integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
+ dependencies:
+ fast-deep-equal "^2.0.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+alphanum-sort@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
+ integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
+
+amdefine@>=0.0.4:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
+ integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=
+
+ansi-colors@^3.0.0:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
+ integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==
+
+ansi-html@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
+ integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4=
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+ integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+ integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+ integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
+
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+anymatch@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+ integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
+ dependencies:
+ micromatch "^3.1.4"
+ normalize-path "^2.1.1"
+
+aproba@^1.0.3, aproba@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+ integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+
+are-we-there-yet@~1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
+ integrity sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+argparse@^1.0.7:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
+ integrity sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=
+ dependencies:
+ sprintf-js "~1.0.2"
+
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+ integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+ integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+ integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
+array-find-index@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
+ integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+ integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
+
+array-flatten@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296"
+ integrity sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=
+
+array-union@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+ integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=
+ 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"
+ integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+ integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
+asn1.js@^4.0.0:
+ version "4.9.2"
+ resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a"
+ integrity sha512-b/OsSjvWEo8Pi8H0zsDd2P6Uqo2TK2pH8gNLSJtNLM2Db0v2QaAZ0pBQJXVjAn4gBuugeVDr7s63ZogpUIwWDg==
+ dependencies:
+ bn.js "^4.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+asn1@~0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+ integrity sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+ integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+assert-plus@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
+ integrity sha1-104bh+ev/A24qttwIfP+SBAasjQ=
+
+assert@^1.1.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
+ integrity sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=
+ dependencies:
+ util "0.10.3"
+
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+ integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+
+async-each@^1.0.0, async-each@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+ integrity sha1-GdOGodntxufByF04iu28xW0zYC0=
+
+async-foreach@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
+ integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=
+
+async@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+ integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+atob@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d"
+ integrity sha1-GcenYEc3dEaPILLS0DNyrX1Mv10=
+
+autoprefixer@^9.4.9:
+ version "9.4.10"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.4.10.tgz#e1be61fc728bacac8f4252ed242711ec0dcc6a7b"
+ integrity sha512-XR8XZ09tUrrSzgSlys4+hy5r2/z4Jp7Ag3pHm31U4g/CTccYPOVe19AkaJ4ey/vRd1sfj+5TtuD6I0PXtutjvQ==
+ dependencies:
+ browserslist "^4.4.2"
+ caniuse-lite "^1.0.30000940"
+ normalize-range "^0.1.2"
+ num2fraction "^1.2.2"
+ postcss "^7.0.14"
+ postcss-value-parser "^3.3.1"
+
+aws-sign2@~0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
+ integrity sha1-FDQt0428yU0OW4fXY81jYSwOeU8=
+
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+ integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.2.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+ integrity sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=
+
+aws4@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
+ integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
+
+babel-loader@^8.0.5:
+ version "8.0.5"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.5.tgz#225322d7509c2157655840bba52e46b6c2f2fe33"
+ integrity sha512-NTnHnVRd2JnRqPC0vW+iOQWU5pchDbYXsG2E6DMXEpMfUcQKclF9gmf3G3ZMhzG7IG9ji4coL0cm+FxeWxDpnw==
+ dependencies:
+ find-cache-dir "^2.0.0"
+ loader-utils "^1.0.2"
+ mkdirp "^0.5.1"
+ util.promisify "^1.0.0"
+
+babel-plugin-dynamic-import-node@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.2.0.tgz#c0adfb07d95f4a4495e9aaac6ec386c4d7c2524e"
+ integrity sha512-fP899ELUnTaBcIzmrW7nniyqqdYWrWuJUyPWHxFa/c7r7hS6KC8FscNfLlBNIoPSc55kYMGEEKjPjJGCLbE1qA==
+ dependencies:
+ object.assign "^4.1.0"
+
+babel-plugin-macros@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.5.0.tgz#01f4d3b50ed567a67b80a30b9da066e94f4097b6"
+ integrity sha512-BWw0lD0kVZAXRD3Od1kMrdmfudqzDzYv2qrN3l2ISR1HVp1EgLKfbOrYV9xmY5k3qx3RIu5uPAUZZZHpo0o5Iw==
+ dependencies:
+ cosmiconfig "^5.0.5"
+ resolve "^1.8.1"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+ integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+base64-js@^1.0.2:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
+ integrity sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==
+
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+ dependencies:
+ cache-base "^1.0.1"
+ class-utils "^0.3.5"
+ component-emitter "^1.2.1"
+ define-property "^1.0.0"
+ isobject "^3.0.1"
+ mixin-deep "^1.2.0"
+ pascalcase "^0.1.1"
+
+batch@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
+ integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+ integrity sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=
+ dependencies:
+ tweetnacl "^0.14.3"
+
+big.js@^3.1.3:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
+ integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==
+
+big.js@^5.2.2:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
+ integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
+
+binary-extensions@^1.0.0:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
+ integrity sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=
+
+block-stream@*:
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+ integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=
+ dependencies:
+ inherits "~2.0.0"
+
+bluebird@^3.5.3:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7"
+ integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
+ integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
+
+body-parser@1.18.2:
+ version "1.18.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
+ integrity sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=
+ dependencies:
+ bytes "3.0.0"
+ content-type "~1.0.4"
+ debug "2.6.9"
+ depd "~1.1.1"
+ http-errors "~1.6.2"
+ iconv-lite "0.4.19"
+ on-finished "~2.3.0"
+ qs "6.5.1"
+ raw-body "2.3.2"
+ type-is "~1.6.15"
+
+bonjour@^3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5"
+ integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU=
+ dependencies:
+ array-flatten "^2.1.0"
+ deep-equal "^1.0.1"
+ dns-equal "^1.0.0"
+ dns-txt "^2.0.2"
+ multicast-dns "^6.0.1"
+ multicast-dns-service-types "^1.1.0"
+
+boolbase@^1.0.0, boolbase@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+ integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
+boom@2.x.x:
+ version "2.10.1"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
+ integrity sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=
+ dependencies:
+ hoek "2.x.x"
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.0.tgz#a46941cb5fb492156b3d6a656e06c35364e3e66e"
+ integrity sha512-P4O8UQRdGiMLWSizsApmXVQDBS6KCt7dSexgLKBmH5Hr1CZq7vsnscFh8oR1sP1ab1Zj0uCHCEzZeV6SfUf3rA==
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
+braces@^2.3.1, braces@^2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+ integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
+brorand@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+ integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f"
+ integrity sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg==
+ dependencies:
+ buffer-xor "^1.0.3"
+ cipher-base "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.3"
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+browserify-cipher@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a"
+ integrity sha1-mYgkSHS/XtTijalWZtzWasj8Njo=
+ dependencies:
+ browserify-aes "^1.0.4"
+ browserify-des "^1.0.0"
+ evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd"
+ integrity sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=
+ dependencies:
+ cipher-base "^1.0.1"
+ des.js "^1.0.0"
+ inherits "^2.0.1"
+
+browserify-rsa@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
+ integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=
+ dependencies:
+ bn.js "^4.1.0"
+ randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
+ integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=
+ dependencies:
+ bn.js "^4.1.1"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.2"
+ elliptic "^6.0.0"
+ inherits "^2.0.1"
+ parse-asn1 "^5.0.0"
+
+browserify-zlib@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+ integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==
+ dependencies:
+ pako "~1.0.5"
+
+browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.2.tgz#6ea8a74d6464bb0bd549105f659b41197d8f0ba2"
+ integrity sha512-ISS/AIAiHERJ3d45Fz0AVYKkgcy+F/eJHzKEvv1j0wwKGKD9T3BrwKr/5g45L+Y4XIK5PlTqefHciRFcfE1Jxg==
+ dependencies:
+ caniuse-lite "^1.0.30000939"
+ electron-to-chromium "^1.3.113"
+ node-releases "^1.1.8"
+
+buffer-from@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+ integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
+buffer-indexof@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c"
+ integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==
+
+buffer-xor@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+ integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
+
+buffer@^4.3.0:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
+ integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+ isarray "^1.0.0"
+
+builtin-modules@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+ integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
+
+builtin-status-codes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+ integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
+
+bytes@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
+ integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
+
+cacache@^11.0.2, cacache@^11.2.0:
+ version "11.3.2"
+ resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.2.tgz#2d81e308e3d258ca38125b676b98b2ac9ce69bfa"
+ integrity sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg==
+ dependencies:
+ bluebird "^3.5.3"
+ chownr "^1.1.1"
+ figgy-pudding "^3.5.1"
+ glob "^7.1.3"
+ graceful-fs "^4.1.15"
+ lru-cache "^5.1.1"
+ mississippi "^3.0.0"
+ mkdirp "^0.5.1"
+ move-concurrently "^1.0.1"
+ promise-inflight "^1.0.1"
+ rimraf "^2.6.2"
+ ssri "^6.0.1"
+ unique-filename "^1.1.1"
+ y18n "^4.0.0"
+
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+ dependencies:
+ collection-visit "^1.0.0"
+ component-emitter "^1.2.1"
+ get-value "^2.0.6"
+ has-value "^1.0.0"
+ isobject "^3.0.1"
+ set-value "^2.0.0"
+ to-object-path "^0.3.0"
+ union-value "^1.0.0"
+ unset-value "^1.0.0"
+
+caller-callsite@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134"
+ integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=
+ dependencies:
+ callsites "^2.0.0"
+
+caller-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4"
+ integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=
+ dependencies:
+ caller-callsite "^2.0.0"
+
+callsites@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
+ integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=
+
+camelcase-keys@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
+ integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc=
+ dependencies:
+ camelcase "^2.0.0"
+ map-obj "^1.0.0"
+
+camelcase@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
+ integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=
+
+camelcase@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
+ integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo=
+
+camelcase@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+ integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
+
+camelcase@^5.0.0, camelcase@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.2.0.tgz#e7522abda5ed94cc0489e1b8466610e88404cf45"
+ integrity sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ==
+
+caniuse-api@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
+ integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+ dependencies:
+ browserslist "^4.0.0"
+ caniuse-lite "^1.0.0"
+ lodash.memoize "^4.1.2"
+ lodash.uniq "^4.5.0"
+
+caniuse-lite@^1.0.0:
+ version "1.0.30000808"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000808.tgz#7d759b5518529ea08b6705a19e70dbf401628ffc"
+ integrity sha512-vT0JLmHdvq1UVbYXioxCXHYdNw55tyvi+IUWyX0Zeh1OFQi2IllYtm38IJnSgHWCv/zUnX1hdhy3vMJvuTNSqw==
+
+caniuse-lite@^1.0.30000939, caniuse-lite@^1.0.30000940:
+ version "1.0.30000942"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000942.tgz#454139b28274bce70bfe1d50c30970df7430c6e4"
+ integrity sha512-wLf+IhZUy2rfz48tc40OH7jHjXjnvDFEYqBHluINs/6MgzoNLPf25zhE4NOVzqxLKndf+hau81sAW0RcGHIaBQ==
+
+case-sensitive-paths-webpack-plugin@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.2.0.tgz#3371ef6365ef9c25fa4b81c16ace0e9c7dc58c3e"
+ integrity sha512-u5ElzokS8A1pm9vM3/iDgTcI3xqHxuCao94Oz8etI3cf0Tio0p8izkDYbTIn09uP3yUUr6+veaE6IkjnTYS46g==
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+ integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
+chalk@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+ integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
+ 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, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chokidar@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.1.tgz#6e67e9998fe10e8f651e975ca62460456ff8e297"
+ integrity sha512-rv5iP8ENhpqvDWr677rAXcB+SMoPQ1urd4ch79+PhM4lQwbATdJUQK69t0lJIKNB+VXpqxt5V1gvqs59XEPKnw==
+ dependencies:
+ anymatch "^2.0.0"
+ async-each "^1.0.0"
+ braces "^2.3.0"
+ glob-parent "^3.1.0"
+ inherits "^2.0.1"
+ is-binary-path "^1.0.0"
+ is-glob "^4.0.0"
+ normalize-path "^2.1.1"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.0.0"
+ upath "1.0.0"
+ optionalDependencies:
+ fsevents "^1.0.0"
+
+chokidar@^2.0.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.2.tgz#9c23ea40b01638439e0513864d362aeacc5ad058"
+ integrity sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==
+ dependencies:
+ anymatch "^2.0.0"
+ async-each "^1.0.1"
+ braces "^2.3.2"
+ glob-parent "^3.1.0"
+ inherits "^2.0.3"
+ is-binary-path "^1.0.0"
+ is-glob "^4.0.0"
+ normalize-path "^3.0.0"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.2.1"
+ upath "^1.1.0"
+ optionalDependencies:
+ fsevents "^1.2.7"
+
+chownr@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
+ integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==
+
+chrome-trace-event@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz#45a91bd2c20c9411f0963b5aaeb9a1b95e09cc48"
+ integrity sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==
+ dependencies:
+ tslib "^1.9.0"
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+ integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
+cliui@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+ integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wrap-ansi "^2.0.0"
+
+cliui@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
+ integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==
+ dependencies:
+ string-width "^2.1.1"
+ strip-ansi "^4.0.0"
+ wrap-ansi "^2.0.0"
+
+clone-deep@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713"
+ integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==
+ dependencies:
+ for-own "^1.0.0"
+ is-plain-object "^2.0.4"
+ kind-of "^6.0.0"
+ shallow-clone "^1.0.0"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+ integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+
+coa@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3"
+ integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==
+ dependencies:
+ "@types/q" "^1.5.1"
+ chalk "^2.4.1"
+ q "^1.1.2"
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+ integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+ dependencies:
+ map-visit "^1.0.0"
+ object-visit "^1.0.0"
+
+color-convert@^1.9.0, color-convert@^1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
+ integrity sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==
+ dependencies:
+ color-name "^1.1.1"
+
+color-name@^1.0.0, color-name@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-string@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.2.tgz#26e45814bc3c9a7cbd6751648a41434514a773a9"
+ integrity sha1-JuRYFLw8mny9Z1FkikFDRRSnc6k=
+ dependencies:
+ color-name "^1.0.0"
+ simple-swizzle "^0.2.2"
+
+color@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/color/-/color-3.1.0.tgz#d8e9fb096732875774c84bf922815df0308d0ffc"
+ integrity sha512-CwyopLkuRYO5ei2EpzpIh6LqJMt6Mt+jZhO5VI5f/wJLZriXQE32/SSqzmrh+QB+AZT81Cj8yv+7zwToW8ahZg==
+ dependencies:
+ color-convert "^1.9.1"
+ color-string "^1.5.2"
+
+combined-stream@^1.0.5, combined-stream@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
+ integrity sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=
+ dependencies:
+ delayed-stream "~1.0.0"
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
+ integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@~2.17.1:
+ version "2.17.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
+ integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
+
+commondir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+ integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
+
+component-emitter@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+ integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
+
+compressible@~2.0.11:
+ version "2.0.12"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.12.tgz#c59a5c99db76767e9876500e271ef63b3493bd66"
+ integrity sha1-xZpcmdt2dn6YdlAOJx72OzSTvWY=
+ dependencies:
+ mime-db ">= 1.30.0 < 2"
+
+compression-webpack-plugin@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-2.0.0.tgz#46476350c1eb27f783dccc79ac2f709baa2cffbc"
+ integrity sha512-bDgd7oTUZC8EkRx8j0sjyCfeiO+e5sFcfgaFcjVhfQf5lLya7oY2BczxcJ7IUuVjz5m6fy8IECFmVFew3xLk8Q==
+ dependencies:
+ cacache "^11.2.0"
+ find-cache-dir "^2.0.0"
+ neo-async "^2.5.0"
+ schema-utils "^1.0.0"
+ serialize-javascript "^1.4.0"
+ webpack-sources "^1.0.1"
+
+compression@^1.5.2:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.1.tgz#eff2603efc2e22cf86f35d2eb93589f9875373db"
+ integrity sha1-7/JgPvwuIs+G810uuTWJ+YdTc9s=
+ dependencies:
+ accepts "~1.3.4"
+ bytes "3.0.0"
+ compressible "~2.0.11"
+ debug "2.6.9"
+ on-headers "~1.0.1"
+ safe-buffer "5.1.1"
+ vary "~1.1.2"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+concat-stream@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+ integrity sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+connect-history-api-fallback@^1.3.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a"
+ integrity sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=
+
+console-browserify@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
+ integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=
+ dependencies:
+ date-now "^0.1.4"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+ integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
+
+constants-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+ integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=
+
+content-disposition@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
+ integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ=
+
+content-type@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+ integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+
+convert-source-map@^1.1.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
+ integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==
+ dependencies:
+ safe-buffer "~5.1.1"
+
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+ integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
+
+cookie@0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
+ integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
+
+copy-concurrently@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
+ integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==
+ dependencies:
+ aproba "^1.1.1"
+ fs-write-stream-atomic "^1.0.8"
+ iferr "^0.1.5"
+ mkdirp "^0.5.1"
+ rimraf "^2.5.4"
+ run-queue "^1.0.0"
+
+copy-descriptor@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+ integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+
+core-js@^2.5.7:
+ version "2.6.5"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895"
+ integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==
+
+core-util-is@1.0.2, 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"
+ integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+cosmiconfig@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-4.0.0.tgz#760391549580bbd2df1e562bc177b13c290972dc"
+ integrity sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==
+ dependencies:
+ is-directory "^0.3.1"
+ js-yaml "^3.9.0"
+ parse-json "^4.0.0"
+ require-from-string "^2.0.1"
+
+cosmiconfig@^5.0.0, cosmiconfig@^5.0.5:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.1.0.tgz#6c5c35e97f37f985061cdf653f114784231185cf"
+ integrity sha512-kCNPvthka8gvLtzAxQXvWo4FxqRB+ftRZyPZNuab5ngvM9Y7yw7hbEysglptLgpkGX9nAOKTBVkHUAe8xtYR6Q==
+ dependencies:
+ import-fresh "^2.0.0"
+ is-directory "^0.3.1"
+ js-yaml "^3.9.0"
+ lodash.get "^4.4.2"
+ parse-json "^4.0.0"
+
+create-ecdh@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
+ integrity sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=
+ dependencies:
+ bn.js "^4.1.0"
+ elliptic "^6.0.0"
+
+create-hash@^1.1.0, create-hash@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd"
+ integrity sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=
+ dependencies:
+ cipher-base "^1.0.1"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06"
+ integrity sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=
+ dependencies:
+ cipher-base "^1.0.3"
+ create-hash "^1.1.0"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+cross-spawn@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
+ integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI=
+ dependencies:
+ lru-cache "^4.0.1"
+ which "^1.2.9"
+
+cross-spawn@^6.0.0, cross-spawn@^6.0.5:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+cryptiles@2.x.x:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
+ integrity sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=
+ dependencies:
+ boom "2.x.x"
+
+crypto-browserify@^3.11.0:
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
+ integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==
+ dependencies:
+ browserify-cipher "^1.0.0"
+ browserify-sign "^4.0.0"
+ create-ecdh "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.0"
+ diffie-hellman "^5.0.0"
+ inherits "^2.0.1"
+ pbkdf2 "^3.0.3"
+ public-encrypt "^4.0.0"
+ randombytes "^2.0.0"
+ randomfill "^1.0.3"
+
+css-blank-pseudo@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5"
+ integrity sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w==
+ dependencies:
+ postcss "^7.0.5"
+
+css-color-names@0.0.4, css-color-names@^0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
+ integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=
+
+css-declaration-sorter@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22"
+ integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==
+ dependencies:
+ postcss "^7.0.1"
+ timsort "^0.3.0"
+
+css-has-pseudo@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee"
+ integrity sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ==
+ dependencies:
+ postcss "^7.0.6"
+ postcss-selector-parser "^5.0.0-rc.4"
+
+css-loader@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea"
+ integrity sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w==
+ dependencies:
+ camelcase "^5.2.0"
+ icss-utils "^4.1.0"
+ loader-utils "^1.2.3"
+ normalize-path "^3.0.0"
+ postcss "^7.0.14"
+ postcss-modules-extract-imports "^2.0.0"
+ postcss-modules-local-by-default "^2.0.6"
+ postcss-modules-scope "^2.1.0"
+ postcss-modules-values "^2.0.0"
+ postcss-value-parser "^3.3.0"
+ schema-utils "^1.0.0"
+
+css-prefers-color-scheme@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4"
+ integrity sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg==
+ dependencies:
+ postcss "^7.0.5"
+
+css-select-base-adapter@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
+ integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
+
+css-select@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.0.2.tgz#ab4386cec9e1f668855564b17c3733b43b2a5ede"
+ integrity sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==
+ dependencies:
+ boolbase "^1.0.0"
+ css-what "^2.1.2"
+ domutils "^1.7.0"
+ nth-check "^1.0.2"
+
+css-tree@1.0.0-alpha.28:
+ version "1.0.0-alpha.28"
+ resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.28.tgz#8e8968190d886c9477bc8d61e96f61af3f7ffa7f"
+ integrity sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==
+ dependencies:
+ mdn-data "~1.1.0"
+ source-map "^0.5.3"
+
+css-tree@1.0.0-alpha.29:
+ version "1.0.0-alpha.29"
+ resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39"
+ integrity sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==
+ dependencies:
+ mdn-data "~1.1.0"
+ source-map "^0.5.3"
+
+css-unit-converter@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996"
+ integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=
+
+css-url-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/css-url-regex/-/css-url-regex-1.1.0.tgz#83834230cc9f74c457de59eebd1543feeb83b7ec"
+ integrity sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=
+
+css-what@^2.1.2:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
+ integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
+
+cssdb@^4.3.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0"
+ integrity sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ==
+
+cssesc@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703"
+ integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==
+
+cssesc@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+ integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+cssnano-preset-default@^4.0.7:
+ version "4.0.7"
+ resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76"
+ integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==
+ dependencies:
+ css-declaration-sorter "^4.0.1"
+ cssnano-util-raw-cache "^4.0.1"
+ postcss "^7.0.0"
+ postcss-calc "^7.0.1"
+ postcss-colormin "^4.0.3"
+ postcss-convert-values "^4.0.1"
+ postcss-discard-comments "^4.0.2"
+ postcss-discard-duplicates "^4.0.2"
+ postcss-discard-empty "^4.0.1"
+ postcss-discard-overridden "^4.0.1"
+ postcss-merge-longhand "^4.0.11"
+ postcss-merge-rules "^4.0.3"
+ postcss-minify-font-values "^4.0.2"
+ postcss-minify-gradients "^4.0.2"
+ postcss-minify-params "^4.0.2"
+ postcss-minify-selectors "^4.0.2"
+ postcss-normalize-charset "^4.0.1"
+ postcss-normalize-display-values "^4.0.2"
+ postcss-normalize-positions "^4.0.2"
+ postcss-normalize-repeat-style "^4.0.2"
+ postcss-normalize-string "^4.0.2"
+ postcss-normalize-timing-functions "^4.0.2"
+ postcss-normalize-unicode "^4.0.1"
+ postcss-normalize-url "^4.0.1"
+ postcss-normalize-whitespace "^4.0.2"
+ postcss-ordered-values "^4.1.2"
+ postcss-reduce-initial "^4.0.3"
+ postcss-reduce-transforms "^4.0.2"
+ postcss-svgo "^4.0.2"
+ postcss-unique-selectors "^4.0.1"
+
+cssnano-util-get-arguments@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f"
+ integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=
+
+cssnano-util-get-match@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d"
+ integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=
+
+cssnano-util-raw-cache@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282"
+ integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==
+ dependencies:
+ postcss "^7.0.0"
+
+cssnano-util-same-parent@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3"
+ integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==
+
+cssnano@^4.1.0:
+ version "4.1.10"
+ resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2"
+ integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==
+ dependencies:
+ cosmiconfig "^5.0.0"
+ cssnano-preset-default "^4.0.7"
+ is-resolvable "^1.0.0"
+ postcss "^7.0.0"
+
+csso@^3.5.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b"
+ integrity sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==
+ dependencies:
+ css-tree "1.0.0-alpha.29"
+
+currently-unhandled@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
+ integrity sha1-mI3zP+qxke95mmE2nddsF635V+o=
+ dependencies:
+ array-find-index "^1.0.1"
+
+cyclist@~0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
+ integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+ dependencies:
+ assert-plus "^1.0.0"
+
+date-now@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+ integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=
+
+debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+debug@^3.2.5, debug@^3.2.6:
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+ integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+ dependencies:
+ ms "^2.1.1"
+
+debug@^4.1.0, debug@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+ integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+ dependencies:
+ ms "^2.1.1"
+
+decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+ integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+decamelize@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7"
+ integrity sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==
+ dependencies:
+ xregexp "4.0.0"
+
+decode-uri-component@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+ integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+
+deep-equal@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
+ integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
+
+deep-extend@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+ integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
+deep-extend@~0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
+ integrity sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=
+
+default-gateway@^4.0.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b"
+ integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==
+ dependencies:
+ execa "^1.0.0"
+ ip-regex "^2.1.0"
+
+define-properties@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94"
+ integrity sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=
+ dependencies:
+ foreach "^2.0.5"
+ object-keys "^1.0.8"
+
+define-properties@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ dependencies:
+ object-keys "^1.0.12"
+
+define-property@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+ integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+ dependencies:
+ is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+ integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+ dependencies:
+ is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+ integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+ dependencies:
+ is-descriptor "^1.0.2"
+ isobject "^3.0.1"
+
+del@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5"
+ integrity sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=
+ dependencies:
+ globby "^6.1.0"
+ is-path-cwd "^1.0.0"
+ is-path-in-cwd "^1.0.0"
+ p-map "^1.1.1"
+ pify "^3.0.0"
+ rimraf "^2.2.8"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+ integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
+
+depd@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
+ integrity sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=
+
+depd@~1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+ integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+
+des.js@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
+ integrity sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=
+ dependencies:
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+destroy@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+ integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+
+detect-file@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
+ integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
+
+detect-libc@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+ integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
+
+detect-node@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
+ integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
+
+diffie-hellman@^5.0.0:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
+ integrity sha1-tYNXOScM/ias9jIJn97SoH8gnl4=
+ dependencies:
+ bn.js "^4.1.0"
+ miller-rabin "^4.0.0"
+ randombytes "^2.0.0"
+
+dns-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
+ integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0=
+
+dns-packet@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a"
+ integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==
+ dependencies:
+ ip "^1.1.0"
+ safe-buffer "^5.0.1"
+
+dns-txt@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6"
+ integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=
+ dependencies:
+ buffer-indexof "^1.0.0"
+
+dom-serializer@0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+ integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
+ dependencies:
+ domelementtype "^1.3.0"
+ entities "^1.1.1"
+
+domain-browser@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
+ integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
+
+domelementtype@1, domelementtype@^1.3.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+ integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+
+domutils@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+ integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+dot-prop@^4.1.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
+ integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==
+ dependencies:
+ is-obj "^1.0.0"
+
+duplexify@^3.4.2, duplexify@^3.5.3:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.3.tgz#8b5818800df92fd0125b27ab896491912858243e"
+ integrity sha512-g8ID9OroF9hKt2POf8YLayy+9594PzmM3scI00/uBXocX3TWNgoB67hjzkFe9ITAbQOne/lLdBxHXvYUM4ZgGA==
+ dependencies:
+ end-of-stream "^1.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+ stream-shift "^1.0.0"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
+ integrity sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=
+ dependencies:
+ jsbn "~0.1.0"
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+ integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+
+electron-to-chromium@^1.3.113:
+ version "1.3.113"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9"
+ integrity sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g==
+
+elliptic@^6.0.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
+ integrity sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=
+ dependencies:
+ bn.js "^4.4.0"
+ brorand "^1.0.1"
+ hash.js "^1.0.0"
+ hmac-drbg "^1.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.0"
+
+emojis-list@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
+ integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
+
+encodeurl@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+ integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
+ integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==
+ dependencies:
+ once "^1.4.0"
+
+enhanced-resolve@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f"
+ integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==
+ dependencies:
+ graceful-fs "^4.1.2"
+ memory-fs "^0.4.0"
+ tapable "^1.0.0"
+
+entities@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+ integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+
+errno@^0.1.3, errno@^0.1.4:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.6.tgz#c386ce8a6283f14fc09563b71560908c9bf53026"
+ integrity sha512-IsORQDpaaSwcDP4ZZnHxgE85werpo34VYn1Ud3mq+eUsF593faR8oCZNXrROVkpFu2TsbrNhHin0aUrTsQ9vNw==
+ dependencies:
+ prr "~1.0.1"
+
+error-ex@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ integrity sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=
+ dependencies:
+ is-arrayish "^0.2.1"
+
+error-ex@^1.3.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+ integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es-abstract@^1.12.0, es-abstract@^1.5.1:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
+ integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==
+ dependencies:
+ es-to-primitive "^1.2.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ is-callable "^1.1.4"
+ is-regex "^1.0.4"
+ object-keys "^1.0.12"
+
+es-to-primitive@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
+ integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+ integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+
+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"
+ integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+eslint-scope@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.2.tgz#5f10cd6cabb1965bf479fa65745673439e21cb0e"
+ integrity sha512-5q1+B/ogmHl8+paxtOKx38Z8LtWkVGuNt3+GQNErqwLl6ViNp/gdJGMCjZNxZ8j/VYjDNZ2Fo+eQc1TAVPIzbg==
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+esprima@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+ integrity sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==
+
+esrecurse@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
+ integrity sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=
+ dependencies:
+ estraverse "^4.1.0"
+ object-assign "^4.0.1"
+
+estraverse@^4.1.0, estraverse@^4.1.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+ integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+ integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=
+
+etag@~1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+ integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+
+eventemitter3@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
+ integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==
+
+events@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+ integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
+
+eventsource@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0"
+ integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==
+ dependencies:
+ original "^1.0.0"
+
+evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+ integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==
+ dependencies:
+ md5.js "^1.3.4"
+ safe-buffer "^5.1.1"
+
+execa@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+ integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+ dependencies:
+ cross-spawn "^6.0.0"
+ get-stream "^4.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+ dependencies:
+ debug "^2.3.3"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ posix-character-classes "^0.1.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+expand-tilde@^2.0.0, expand-tilde@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
+ integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=
+ dependencies:
+ homedir-polyfill "^1.0.1"
+
+express@^4.16.2:
+ version "4.16.2"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c"
+ integrity sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=
+ dependencies:
+ accepts "~1.3.4"
+ array-flatten "1.1.1"
+ body-parser "1.18.2"
+ content-disposition "0.5.2"
+ content-type "~1.0.4"
+ cookie "0.3.1"
+ cookie-signature "1.0.6"
+ debug "2.6.9"
+ depd "~1.1.1"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ finalhandler "1.1.0"
+ fresh "0.5.2"
+ merge-descriptors "1.0.1"
+ methods "~1.1.2"
+ on-finished "~2.3.0"
+ parseurl "~1.3.2"
+ path-to-regexp "0.1.7"
+ proxy-addr "~2.0.2"
+ qs "6.5.1"
+ range-parser "~1.2.0"
+ safe-buffer "5.1.1"
+ send "0.16.1"
+ serve-static "1.13.1"
+ setprototypeof "1.1.0"
+ statuses "~1.3.1"
+ type-is "~1.6.15"
+ utils-merge "1.0.1"
+ vary "~1.1.2"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+ integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+ dependencies:
+ assign-symbols "^1.0.0"
+ is-extendable "^1.0.1"
+
+extend@~3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
+ integrity sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=
+
+extend@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+ integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+extglob@^2.0.2, extglob@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+ dependencies:
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ expand-brackets "^2.1.4"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+extsprintf@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+ integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+ integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+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"
+ integrity sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=
+
+fast-deep-equal@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
+ integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
+
+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"
+ integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
+
+faye-websocket@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
+ integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=
+ dependencies:
+ websocket-driver ">=0.5.1"
+
+faye-websocket@~0.11.1:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38"
+ integrity sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=
+ dependencies:
+ websocket-driver ">=0.5.1"
+
+figgy-pudding@^3.5.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
+ integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==
+
+file-loader@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa"
+ integrity sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw==
+ dependencies:
+ loader-utils "^1.0.2"
+ schema-utils "^1.0.0"
+
+fill-range@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+ integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
+finalhandler@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5"
+ integrity sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ parseurl "~1.3.2"
+ statuses "~1.3.1"
+ unpipe "~1.0.0"
+
+find-cache-dir@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d"
+ integrity sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA==
+ dependencies:
+ commondir "^1.0.1"
+ make-dir "^1.0.0"
+ pkg-dir "^3.0.0"
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=
+ dependencies:
+ path-exists "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+find-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+ integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+ dependencies:
+ locate-path "^3.0.0"
+
+findup-sync@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc"
+ integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=
+ dependencies:
+ detect-file "^1.0.0"
+ is-glob "^3.1.0"
+ micromatch "^3.0.4"
+ resolve-dir "^1.0.1"
+
+flatted@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916"
+ integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==
+
+flatten@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
+ integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=
+
+flush-write-stream@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417"
+ integrity sha1-yBuQ2HRnZvGmCaRoCZRsRd2K5Bc=
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.0.4"
+
+follow-redirects@^1.0.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76"
+ integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==
+ dependencies:
+ debug "^3.2.6"
+
+for-in@^0.1.3:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
+ integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=
+
+for-in@^1.0.1, for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+ integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+
+for-own@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b"
+ integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=
+ dependencies:
+ for-in "^1.0.1"
+
+foreach@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
+ integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+ integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@~2.1.1:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
+ integrity sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.5"
+ mime-types "^2.1.12"
+
+form-data@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+ integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.6"
+ mime-types "^2.1.12"
+
+forwarded@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
+ integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
+
+fragment-cache@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+ integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+ dependencies:
+ map-cache "^0.2.2"
+
+fresh@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+ integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+
+from2@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
+ integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+
+fs-minipass@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
+ integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==
+ dependencies:
+ minipass "^2.2.1"
+
+fs-write-stream-atomic@^1.0.8:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
+ integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=
+ dependencies:
+ graceful-fs "^4.1.2"
+ iferr "^0.1.5"
+ imurmurhash "^0.1.4"
+ readable-stream "1 || 2"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@^1.0.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8"
+ integrity sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==
+ dependencies:
+ nan "^2.3.0"
+ node-pre-gyp "^0.6.39"
+
+fsevents@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4"
+ integrity sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==
+ dependencies:
+ nan "^2.9.2"
+ node-pre-gyp "^0.10.0"
+
+fstream-ignore@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
+ integrity sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=
+ dependencies:
+ fstream "^1.0.0"
+ inherits "2"
+ minimatch "^3.0.0"
+
+fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
+ integrity sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=
+ dependencies:
+ graceful-fs "^4.1.2"
+ inherits "~2.0.0"
+ mkdirp ">=0.5 0"
+ rimraf "2"
+
+function-bind@^1.0.2, function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+gaze@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105"
+ integrity sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=
+ dependencies:
+ globule "^1.0.0"
+
+get-caller-file@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+ integrity sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=
+
+get-stdin@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
+ integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
+
+get-stream@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+ integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+ dependencies:
+ pump "^3.0.0"
+
+get-value@^2.0.3, get-value@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+ integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob-parent@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+ integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=
+ dependencies:
+ is-glob "^3.1.0"
+ path-dirname "^1.0.0"
+
+glob@^6.0.4:
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
+ integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=
+ dependencies:
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "2 || 3"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.1.1:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==
+ 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"
+
+glob@^7.1.3:
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
+ integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
+ 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"
+
+global-modules@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
+ integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==
+ dependencies:
+ global-prefix "^1.0.1"
+ is-windows "^1.0.1"
+ resolve-dir "^1.0.0"
+
+global-prefix@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe"
+ integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=
+ dependencies:
+ expand-tilde "^2.0.2"
+ homedir-polyfill "^1.0.1"
+ ini "^1.3.4"
+ is-windows "^1.0.1"
+ which "^1.2.14"
+
+globals@^11.1.0:
+ version "11.11.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.11.0.tgz#dcf93757fa2de5486fbeed7118538adf789e9c2e"
+ integrity sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==
+
+globby@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
+ integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=
+ dependencies:
+ array-union "^1.0.1"
+ glob "^7.0.3"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+globule@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09"
+ integrity sha1-HcScaCLdnoovoAuiopUAboZkvQk=
+ dependencies:
+ glob "~7.1.1"
+ lodash "~4.17.4"
+ minimatch "~3.0.2"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.2:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+ integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=
+
+graceful-fs@^4.1.15:
+ version "4.1.15"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
+ integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
+
+handle-thing@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754"
+ integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==
+
+har-schema@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
+ integrity sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+ integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
+ integrity sha1-M0gdDxu/9gDdID11gSpqX7oALio=
+ dependencies:
+ ajv "^4.9.1"
+ har-schema "^1.0.5"
+
+har-validator@~5.1.0:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+ integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+ dependencies:
+ ajv "^6.5.5"
+ har-schema "^2.0.0"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+ integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+ integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+ integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
+
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+ dependencies:
+ get-value "^2.0.3"
+ has-values "^0.1.4"
+ isobject "^2.0.0"
+
+has-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+ integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+ dependencies:
+ get-value "^2.0.6"
+ has-values "^1.0.0"
+ isobject "^3.0.0"
+
+has-values@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+ integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+has@^1.0.0, has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+has@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
+ integrity sha1-hGFzP1OLCDfJNh45qauelwTcLyg=
+ dependencies:
+ function-bind "^1.0.2"
+
+hash-base@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1"
+ integrity sha1-ZuodhW206KVHDK32/OI65SRO8uE=
+ dependencies:
+ inherits "^2.0.1"
+
+hash-base@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918"
+ integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
+ integrity sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.0"
+
+hawk@3.1.3, hawk@~3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
+ integrity sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=
+ dependencies:
+ boom "2.x.x"
+ cryptiles "2.x.x"
+ hoek "2.x.x"
+ sntp "1.x.x"
+
+hex-color-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
+ integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
+
+hmac-drbg@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+ integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
+ dependencies:
+ hash.js "^1.0.3"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.1"
+
+hoek@2.x.x:
+ version "2.16.3"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
+ integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=
+
+homedir-polyfill@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
+ integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==
+ dependencies:
+ parse-passwd "^1.0.0"
+
+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"
+ integrity sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==
+
+hpack.js@^2.1.6:
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
+ integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=
+ dependencies:
+ inherits "^2.0.1"
+ obuf "^1.0.0"
+ readable-stream "^2.0.1"
+ wbuf "^1.1.0"
+
+hsl-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e"
+ integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=
+
+hsla-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
+ integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
+
+html-comment-regex@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
+ integrity sha1-ZouTd26q5V696POtRkswekljYl4=
+
+html-entities@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
+ integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=
+
+http-deceiver@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
+ integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=
+
+http-errors@1.6.2, http-errors@~1.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
+ integrity sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=
+ dependencies:
+ depd "1.1.1"
+ inherits "2.0.3"
+ setprototypeof "1.0.3"
+ statuses ">= 1.3.1 < 2"
+
+http-parser-js@>=0.4.0:
+ version "0.4.10"
+ resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4"
+ integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=
+
+http-proxy-middleware@^0.19.1:
+ version "0.19.1"
+ resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a"
+ integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==
+ dependencies:
+ http-proxy "^1.17.0"
+ is-glob "^4.0.0"
+ lodash "^4.17.11"
+ micromatch "^3.1.10"
+
+http-proxy@^1.17.0:
+ version "1.17.0"
+ resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a"
+ integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==
+ dependencies:
+ eventemitter3 "^3.0.0"
+ follow-redirects "^1.0.0"
+ requires-port "^1.0.0"
+
+http-signature@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
+ integrity sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=
+ dependencies:
+ assert-plus "^0.2.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+https-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
+ integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
+
+iconv-lite@0.4.19:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+ integrity sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==
+
+iconv-lite@^0.4.4:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+icss-replace-symbols@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
+ integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=
+
+icss-utils@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.0.tgz#339dbbffb9f8729a243b701e1c29d4cc58c52f0e"
+ integrity sha512-3DEun4VOeMvSczifM3F2cKQrDQ5Pj6WKhkOq6HD4QTnDUAq8MQRxy5TX6Sy1iY6WPBe4gQ3p5vTECjbIkglkkQ==
+ dependencies:
+ postcss "^7.0.14"
+
+ieee754@^1.1.4:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
+ integrity sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=
+
+iferr@^0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
+ integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
+
+ignore-walk@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
+ integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==
+ dependencies:
+ minimatch "^3.0.4"
+
+import-cwd@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
+ integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=
+ dependencies:
+ import-from "^2.1.0"
+
+import-fresh@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
+ integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY=
+ dependencies:
+ caller-path "^2.0.0"
+ resolve-from "^3.0.0"
+
+import-from@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1"
+ integrity sha1-M1238qev/VOqpHHUuAId7ja387E=
+ dependencies:
+ resolve-from "^3.0.0"
+
+import-local@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
+ integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==
+ dependencies:
+ pkg-dir "^3.0.0"
+ resolve-cwd "^2.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+ integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
+
+in-publish@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51"
+ integrity sha1-4g/146KvwmkDILbcVSaCqcf631E=
+
+indent-string@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
+ integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=
+ dependencies:
+ repeating "^2.0.0"
+
+indexes-of@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+ integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
+
+indexof@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
+ integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+ integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
+inherits@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+ integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
+
+ini@^1.3.4, ini@~1.3.0:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
+ integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+
+internal-ip@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.2.0.tgz#46e81b638d84c338e5c67e42b1a17db67d0814fa"
+ integrity sha512-ZY8Rk+hlvFeuMmG5uH1MXhhdeMntmIaxaInvAmzMq/SHV8rv4Kh+6GiQNNDQd0wZFrcO+FiTBo8lui/osKOyJw==
+ dependencies:
+ default-gateway "^4.0.1"
+ ipaddr.js "^1.9.0"
+
+interpret@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
+ integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
+
+invariant@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
+ integrity sha1-nh9WrArNtr8wMwbzOL47IErmA2A=
+ dependencies:
+ loose-envify "^1.0.0"
+
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+ integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
+
+invert-kv@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
+ integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
+
+ip-regex@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
+ integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
+
+ip@^1.1.0, ip@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
+ integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
+
+ipaddr.js@1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0"
+ integrity sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=
+
+ipaddr.js@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
+ integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==
+
+is-absolute-url@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
+ integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=
+
+is-accessor-descriptor@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+ integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+ integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+ dependencies:
+ kind-of "^6.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"
+ integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
+
+is-arrayish@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.1.tgz#c2dfc386abaa0c3e33c48db3fe87059e69065efd"
+ integrity sha1-wt/DhquqDD4zxI2z/ocFnmkGXv0=
+
+is-binary-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+ integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=
+ dependencies:
+ binary-extensions "^1.0.0"
+
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+ integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
+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"
+ integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74=
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-callable@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
+ integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==
+
+is-color-stop@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345"
+ integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=
+ dependencies:
+ css-color-names "^0.0.4"
+ hex-color-regex "^1.1.0"
+ hsl-regex "^1.0.0"
+ hsla-regex "^1.0.0"
+ rgb-regex "^1.0.1"
+ rgba-regex "^1.0.0"
+
+is-data-descriptor@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+ integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+ integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+ dependencies:
+ kind-of "^6.0.0"
+
+is-date-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+ integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=
+
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+ dependencies:
+ is-accessor-descriptor "^0.1.6"
+ is-data-descriptor "^0.1.4"
+ kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+ integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+ dependencies:
+ is-accessor-descriptor "^1.0.0"
+ is-data-descriptor "^1.0.0"
+ kind-of "^6.0.2"
+
+is-directory@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
+ integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+ integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+ dependencies:
+ is-plain-object "^2.0.4"
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
+ 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"
+ integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+is-glob@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+ integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
+ dependencies:
+ is-extglob "^2.1.0"
+
+is-glob@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0"
+ integrity sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-obj@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
+ integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
+
+is-odd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-1.0.0.tgz#3b8a932eb028b3775c39bb09e91767accdb69088"
+ integrity sha1-O4qTLrAos3dcObsJ6RdnrM22kIg=
+ dependencies:
+ is-number "^3.0.0"
+
+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"
+ integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=
+
+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"
+ integrity sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=
+ 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"
+ integrity sha1-jvW33lBDej/cprToZe96pVy0gDY=
+ dependencies:
+ path-is-inside "^1.0.1"
+
+is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ dependencies:
+ isobject "^3.0.1"
+
+is-regex@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
+ integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=
+ dependencies:
+ has "^1.0.1"
+
+is-resolvable@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
+ integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
+
+is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+ integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+
+is-svg@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75"
+ integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==
+ dependencies:
+ html-comment-regex "^1.1.0"
+
+is-symbol@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38"
+ integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==
+ dependencies:
+ has-symbols "^1.0.0"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+ integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-utf8@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+ integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
+
+is-windows@^1.0.1, is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+ integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+is-wsl@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+ integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+
+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"
+ integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+ dependencies:
+ isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+ integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+js-base64@^2.1.8:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582"
+ integrity sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw==
+
+js-levenshtein@^1.1.3:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
+ integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
+
+js-tokens@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+ integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
+
+js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@^3.12.0, js-yaml@^3.12.2, js-yaml@^3.9.0:
+ version "3.12.2"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.2.tgz#ef1d067c5a9d9cb65bd72f285b5d8105c77f14fc"
+ integrity sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q==
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+ integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+jsesc@^2.5.1:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+ integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+
+jsesc@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+ integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
+
+json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+ integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+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"
+ integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+ integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+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"
+ integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=
+ dependencies:
+ jsonify "~0.0.0"
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+ integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+json3@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
+ integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=
+
+json5@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+ integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=
+
+json5@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
+ integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+ dependencies:
+ minimist "^1.2.0"
+
+json5@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850"
+ integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==
+ dependencies:
+ minimist "^1.2.0"
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+ integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+killable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b"
+ integrity sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms=
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+ 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"
+ integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0, kind-of@^5.0.2:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+ integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+ integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
+
+last-call-webpack-plugin@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555"
+ integrity sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==
+ dependencies:
+ lodash "^4.17.5"
+ webpack-sources "^1.1.0"
+
+lazy-cache@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264"
+ integrity sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=
+ dependencies:
+ set-getter "^0.1.0"
+
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=
+ dependencies:
+ invert-kv "^1.0.0"
+
+lcid@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
+ integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==
+ dependencies:
+ invert-kv "^2.0.0"
+
+load-json-file@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+ integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+
+loader-runner@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
+ integrity sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=
+
+loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
+ integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=
+ dependencies:
+ big.js "^3.1.3"
+ emojis-list "^2.0.0"
+ json5 "^0.5.0"
+
+loader-utils@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
+ integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
+ dependencies:
+ big.js "^5.2.2"
+ emojis-list "^2.0.0"
+ json5 "^1.0.1"
+
+locate-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+ integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+ dependencies:
+ p-locate "^3.0.0"
+ path-exists "^3.0.0"
+
+lodash._reinterpolate@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
+ integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
+
+lodash.assign@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
+ integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=
+
+lodash.clonedeep@^4.3.2:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+ integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
+
+lodash.get@^4.0, lodash.get@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+ integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
+
+lodash.has@^4.0:
+ version "4.5.2"
+ resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862"
+ integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=
+
+lodash.memoize@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+ integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
+lodash.mergewith@^4.6.0:
+ version "4.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
+ integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==
+
+lodash.tail@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
+ integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=
+
+lodash.template@^4.2.4:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0"
+ integrity sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=
+ dependencies:
+ lodash._reinterpolate "~3.0.0"
+ lodash.templatesettings "^4.0.0"
+
+lodash.templatesettings@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316"
+ integrity sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=
+ dependencies:
+ lodash._reinterpolate "~3.0.0"
+
+lodash.uniq@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+ integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+
+lodash@3.x:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
+ integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
+
+lodash@^4.0.0, lodash@~4.17.4:
+ version "4.17.5"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
+ integrity sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==
+
+lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5:
+ version "4.17.11"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
+ integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
+
+loglevel@^1.4.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa"
+ integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=
+
+loose-envify@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+ integrity sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=
+ dependencies:
+ js-tokens "^3.0.0"
+
+loud-rejection@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
+ integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=
+ dependencies:
+ currently-unhandled "^0.4.1"
+ signal-exit "^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"
+ integrity sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+lru-cache@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
+make-dir@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51"
+ integrity sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA==
+ dependencies:
+ pify "^3.0.0"
+
+mamacro@^0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4"
+ integrity sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==
+
+map-age-cleaner@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
+ integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
+ dependencies:
+ p-defer "^1.0.0"
+
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+ integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+
+map-obj@^1.0.0, map-obj@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+ integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
+
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+ dependencies:
+ object-visit "^1.0.0"
+
+md5.js@^1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d"
+ integrity sha1-6b296UogpawYsENA/Fdk1bCdkB0=
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
+mdn-data@~1.1.0:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01"
+ integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+ integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+
+mem@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-4.1.0.tgz#aeb9be2d21f47e78af29e4ac5978e8afa2ca5b8a"
+ integrity sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==
+ dependencies:
+ map-age-cleaner "^0.1.1"
+ mimic-fn "^1.0.0"
+ p-is-promise "^2.0.0"
+
+memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
+ integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=
+ dependencies:
+ errno "^0.1.3"
+ readable-stream "^2.0.1"
+
+meow@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
+ integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=
+ dependencies:
+ camelcase-keys "^2.0.0"
+ decamelize "^1.1.2"
+ loud-rejection "^1.0.0"
+ map-obj "^1.0.1"
+ minimist "^1.1.3"
+ normalize-package-data "^2.3.4"
+ object-assign "^4.0.1"
+ read-pkg-up "^1.0.1"
+ redent "^1.0.0"
+ trim-newlines "^1.0.0"
+
+merge-descriptors@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+ integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
+
+methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+ integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
+
+micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.8:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
+micromatch@^3.1.4:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.5.tgz#d05e168c206472dfbca985bfef4f57797b4cd4ba"
+ integrity sha512-ykttrLPQrz1PUJcXjwsTUjGoPJ64StIGNE2lGVD1c9CuguJ+L7/navsE8IcDNndOoCMvYV0qc/exfVbMHkUhvA==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.0"
+ define-property "^1.0.0"
+ extend-shallow "^2.0.1"
+ extglob "^2.0.2"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.0"
+ nanomatch "^1.2.5"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+miller-rabin@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+ integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==
+ dependencies:
+ bn.js "^4.0.0"
+ brorand "^1.0.1"
+
+"mime-db@>= 1.30.0 < 2":
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.32.0.tgz#485b3848b01a3cda5f968b4882c0771e58e09414"
+ integrity sha512-+ZWo/xZN40Tt6S+HyakUxnSOgff+JEdaneLWIm0Z6LmpCn5DMcZntLyUY5c/rTDog28LhXLKOUZKoTxTCAdBVw==
+
+mime-db@~1.30.0:
+ version "1.30.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
+ integrity sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=
+
+mime-db@~1.38.0:
+ version "1.38.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad"
+ integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==
+
+mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7:
+ version "2.1.17"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
+ integrity sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=
+ dependencies:
+ mime-db "~1.30.0"
+
+mime-types@~2.1.19:
+ version "2.1.22"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd"
+ integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==
+ dependencies:
+ mime-db "~1.38.0"
+
+mime@1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
+ integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
+
+mime@^2.3.1:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.0.tgz#e051fd881358585f3279df333fe694da0bcffdd6"
+ integrity sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==
+
+mimic-fn@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
+ integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
+
+mini-css-extract-plugin@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0"
+ integrity sha512-IuaLjruM0vMKhUUT51fQdQzBYTX49dLj8w68ALEAe2A4iYNpIC4eMac67mt3NzycvjOlf07/kYxJDc0RTl1Wqw==
+ dependencies:
+ loader-utils "^1.1.0"
+ schema-utils "^1.0.0"
+ webpack-sources "^1.1.0"
+
+minimalistic-assert@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
+ integrity sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=
+
+minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+ integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
+
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+ 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"
+ integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
+
+minimist@^1.1.3, minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+ integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
+
+minipass@^2.2.1, minipass@^2.3.4:
+ version "2.3.5"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
+ integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==
+ dependencies:
+ safe-buffer "^5.1.2"
+ yallist "^3.0.0"
+
+minizlib@^1.1.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614"
+ integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==
+ dependencies:
+ minipass "^2.2.1"
+
+mississippi@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
+ integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==
+ dependencies:
+ concat-stream "^1.5.0"
+ duplexify "^3.4.2"
+ end-of-stream "^1.1.0"
+ flush-write-stream "^1.0.0"
+ from2 "^2.1.0"
+ parallel-transform "^1.1.0"
+ pump "^3.0.0"
+ pumpify "^1.3.3"
+ stream-each "^1.1.0"
+ through2 "^2.0.0"
+
+mixin-deep@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
+ integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
+mixin-object@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e"
+ integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=
+ dependencies:
+ for-in "^0.1.3"
+ is-extendable "^0.1.1"
+
+mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
+ dependencies:
+ minimist "0.0.8"
+
+move-concurrently@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
+ integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=
+ dependencies:
+ aproba "^1.1.1"
+ copy-concurrently "^1.0.0"
+ fs-write-stream-atomic "^1.0.8"
+ mkdirp "^0.5.1"
+ rimraf "^2.5.4"
+ run-queue "^1.0.3"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+ integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+
+multicast-dns-service-types@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
+ integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=
+
+multicast-dns@^6.0.1:
+ version "6.2.3"
+ resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229"
+ integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==
+ dependencies:
+ dns-packet "^1.3.1"
+ thunky "^1.0.2"
+
+nan@^2.10.0, nan@^2.9.2:
+ version "2.12.1"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
+ integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
+
+nan@^2.3.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
+ integrity sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=
+
+nanomatch@^1.2.5:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79"
+ integrity sha512-/5ldsnyurvEw7wNpxLFgjVvBLMta43niEYOy0CJ4ntcYSbx6bugRUTQeFb4BR/WanEL1o3aQgHuVLHQaB6tOqg==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ is-odd "^1.0.0"
+ kind-of "^5.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+nanomatch@^1.2.9:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+ integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ fragment-cache "^0.2.1"
+ is-windows "^1.0.2"
+ kind-of "^6.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+needle@^2.2.1:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e"
+ integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==
+ dependencies:
+ debug "^2.1.2"
+ iconv-lite "^0.4.4"
+ sax "^1.2.4"
+
+negotiator@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
+ integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=
+
+neo-async@^2.5.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835"
+ integrity sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==
+
+nice-try@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+ integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+node-forge@0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300"
+ integrity sha1-naYR6giYL0uUIGs760zJZl8gwwA=
+
+node-gyp@^3.8.0:
+ version "3.8.0"
+ resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
+ integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==
+ dependencies:
+ fstream "^1.0.0"
+ glob "^7.0.3"
+ graceful-fs "^4.1.2"
+ mkdirp "^0.5.0"
+ nopt "2 || 3"
+ npmlog "0 || 1 || 2 || 3 || 4"
+ osenv "0"
+ request "^2.87.0"
+ rimraf "2"
+ semver "~5.3.0"
+ tar "^2.0.0"
+ which "1"
+
+node-libs-browser@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
+ integrity sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==
+ dependencies:
+ assert "^1.1.1"
+ browserify-zlib "^0.2.0"
+ buffer "^4.3.0"
+ console-browserify "^1.1.0"
+ constants-browserify "^1.0.0"
+ crypto-browserify "^3.11.0"
+ domain-browser "^1.1.1"
+ events "^1.0.0"
+ https-browserify "^1.0.0"
+ os-browserify "^0.3.0"
+ path-browserify "0.0.0"
+ process "^0.11.10"
+ punycode "^1.2.4"
+ querystring-es3 "^0.2.0"
+ readable-stream "^2.3.3"
+ stream-browserify "^2.0.1"
+ stream-http "^2.7.2"
+ string_decoder "^1.0.0"
+ timers-browserify "^2.0.4"
+ tty-browserify "0.0.0"
+ url "^0.11.0"
+ util "^0.10.3"
+ vm-browserify "0.0.4"
+
+node-pre-gyp@^0.10.0:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
+ integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==
+ dependencies:
+ detect-libc "^1.0.2"
+ mkdirp "^0.5.1"
+ needle "^2.2.1"
+ nopt "^4.0.1"
+ npm-packlist "^1.1.6"
+ npmlog "^4.0.2"
+ rc "^1.2.7"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^4"
+
+node-pre-gyp@^0.6.39:
+ version "0.6.39"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649"
+ integrity sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==
+ dependencies:
+ detect-libc "^1.0.2"
+ hawk "3.1.3"
+ mkdirp "^0.5.1"
+ nopt "^4.0.1"
+ npmlog "^4.0.2"
+ rc "^1.1.7"
+ request "2.81.0"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^2.2.1"
+ tar-pack "^3.4.0"
+
+node-releases@^1.1.8:
+ version "1.1.9"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.9.tgz#70d0985ec4bf7de9f08fc481f5dae111889ca482"
+ integrity sha512-oic3GT4OtbWWKfRolz5Syw0Xus0KRFxeorLNj0s93ofX6PWyuzKjsiGxsCtWktBwwmTF6DdRRf2KreGqeOk5KA==
+ dependencies:
+ semver "^5.3.0"
+
+node-sass@^4.11.0:
+ version "4.11.0"
+ resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a"
+ integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==
+ dependencies:
+ async-foreach "^0.1.3"
+ chalk "^1.1.1"
+ cross-spawn "^3.0.0"
+ gaze "^1.0.0"
+ get-stdin "^4.0.1"
+ glob "^7.0.3"
+ in-publish "^2.0.0"
+ lodash.assign "^4.2.0"
+ lodash.clonedeep "^4.3.2"
+ lodash.mergewith "^4.6.0"
+ meow "^3.7.0"
+ mkdirp "^0.5.1"
+ nan "^2.10.0"
+ node-gyp "^3.8.0"
+ npmlog "^4.0.0"
+ request "^2.88.0"
+ sass-graph "^2.2.4"
+ stdout-stream "^1.4.0"
+ "true-case-path" "^1.0.2"
+
+"nopt@2 || 3":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
+ integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k=
+ dependencies:
+ abbrev "1"
+
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==
+ 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.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+normalize-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-range@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+ integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
+
+normalize-url@^3.0.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
+ integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==
+
+npm-bundled@^1.0.1:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd"
+ integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==
+
+npm-packlist@^1.1.6:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc"
+ integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==
+ dependencies:
+ ignore-walk "^3.0.1"
+ npm-bundled "^1.0.1"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
+ dependencies:
+ path-key "^2.0.0"
+
+"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
+nth-check@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+ integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+ dependencies:
+ boolbase "~1.0.0"
+
+num2fraction@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+ integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
+
+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"
+ integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
+
+oauth-sign@~0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
+ integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=
+
+oauth-sign@~0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+ integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
+object-assign@^4.0.1, object-assign@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+ integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
+object-keys@^1.0.11, object-keys@^1.0.12:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.0.tgz#11bd22348dd2e096a045ab06f6c85bcc340fa032"
+ integrity sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==
+
+object-keys@^1.0.8:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
+ integrity sha1-xUYBd4rVYPEULODgG8yotW0TQm0=
+
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+ dependencies:
+ isobject "^3.0.0"
+
+object.assign@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+ integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ object-keys "^1.0.11"
+
+object.getownpropertydescriptors@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
+ integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.1"
+
+object.pick@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+ integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+ dependencies:
+ isobject "^3.0.1"
+
+object.values@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
+ integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+obuf@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e"
+ integrity sha1-EEEktsYCxnlogaBCVB0220OlJk4=
+
+obuf@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
+ integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+ integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+ dependencies:
+ ee-first "1.1.1"
+
+on-headers@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
+ integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=
+
+once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+ dependencies:
+ wrappy "1"
+
+opn@^5.1.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225"
+ integrity sha512-Jd/GpzPyHF4P2/aNOVmS3lfMSWV9J7cOhCG1s08XCEAsPkB7lp6ddiU0J7XzyQRDUh8BqJ7PchfINjR8jyofRQ==
+ dependencies:
+ is-wsl "^1.1.0"
+
+optimize-css-assets-webpack-plugin@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.1.tgz#9eb500711d35165b45e7fd60ba2df40cb3eb9159"
+ integrity sha512-Rqm6sSjWtx9FchdP0uzTQDc7GXDKnwVEGoSxjezPkzMewx7gEWE9IMUYKmigTRC4U3RaNSwYVnUDLuIdtTpm0A==
+ dependencies:
+ cssnano "^4.1.0"
+ last-call-webpack-plugin "^3.0.0"
+
+original@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f"
+ integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==
+ dependencies:
+ url-parse "^1.4.3"
+
+os-browserify@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
+ integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+ integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
+
+os-locale@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
+ integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=
+ dependencies:
+ lcid "^1.0.0"
+
+os-locale@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
+ integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
+ dependencies:
+ execa "^1.0.0"
+ lcid "^2.0.0"
+ mem "^4.0.0"
+
+os-tmpdir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+ integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+
+osenv@0, osenv@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
+ integrity sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
+p-defer@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
+ integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+ integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
+
+p-is-promise@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.0.0.tgz#7554e3d572109a87e1f3f53f6a7d85d1b194f4c5"
+ integrity sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==
+
+p-limit@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2"
+ integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+ integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+ dependencies:
+ p-limit "^2.0.0"
+
+p-map@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
+ integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==
+
+p-try@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
+ integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==
+
+pako@~1.0.5:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
+ integrity sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==
+
+parallel-transform@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06"
+ integrity sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=
+ dependencies:
+ cyclist "~0.2.2"
+ inherits "^2.0.3"
+ readable-stream "^2.1.5"
+
+parse-asn1@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712"
+ integrity sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=
+ dependencies:
+ asn1.js "^4.0.0"
+ browserify-aes "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.0"
+ pbkdf2 "^3.0.3"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=
+ dependencies:
+ error-ex "^1.2.0"
+
+parse-json@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+ integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
+ dependencies:
+ error-ex "^1.3.1"
+ json-parse-better-errors "^1.0.1"
+
+parse-passwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
+ integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
+
+parseurl@~1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
+ integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+ integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+
+path-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
+ integrity sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=
+
+path-complete-extname@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/path-complete-extname/-/path-complete-extname-1.0.0.tgz#f889985dc91000c815515c0bfed06c5acda0752b"
+ integrity sha512-CVjiWcMRdGU8ubs08YQVzhutOR5DEfO97ipRIlOGMK5Bek5nQySknBpuxVAVJ36hseTNs+vdIcv57ZrWxH7zvg==
+
+path-dirname@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+ integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
+
+path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=
+ 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"
+ integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+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"
+ integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-is-inside@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+ integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
+
+path-key@^2.0.0, path-key@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+ integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
+path-parse@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+ integrity sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=
+
+path-parse@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+ integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+
+path-to-regexp@0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+ integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+
+path-type@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+ integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+pbkdf2@^3.0.3:
+ version "3.0.14"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade"
+ integrity sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==
+ dependencies:
+ create-hash "^1.1.2"
+ create-hmac "^1.1.4"
+ ripemd160 "^2.0.1"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+performance-now@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+ integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+ integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+pify@^2.0.0, pify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+ integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+ integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o=
+ 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"
+ integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
+
+pkg-dir@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
+ integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==
+ dependencies:
+ find-up "^3.0.0"
+
+pnp-webpack-plugin@^1.3.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.4.1.tgz#e8f8c683b496a71c0d200e664c4bb399a9c9585e"
+ integrity sha512-S4kz+5rvWvD0w1O63eTJeXIxW4JHK0wPRMO7GmPhbZXJnTePcfrWZlni4BoglIf7pLSY18xtqo3MSnVkoAFXKg==
+ dependencies:
+ ts-pnp "^1.0.0"
+
+portfinder@^1.0.9:
+ version "1.0.13"
+ resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
+ integrity sha1-uzLs2HwnEErm7kS1o8y/Drsa7ek=
+ dependencies:
+ async "^1.5.2"
+ debug "^2.2.0"
+ mkdirp "0.5.x"
+
+posix-character-classes@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+ integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+
+postcss-attribute-case-insensitive@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.1.tgz#b2a721a0d279c2f9103a36331c88981526428cc7"
+ integrity sha512-L2YKB3vF4PetdTIthQVeT+7YiSzMoNMLLYxPXXppOOP7NoazEAy45sh2LvJ8leCQjfBcfkYQs8TtCcQjeZTp8A==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-selector-parser "^5.0.0"
+
+postcss-calc@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.1.tgz#36d77bab023b0ecbb9789d84dcb23c4941145436"
+ integrity sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==
+ dependencies:
+ css-unit-converter "^1.1.1"
+ postcss "^7.0.5"
+ postcss-selector-parser "^5.0.0-rc.4"
+ postcss-value-parser "^3.3.1"
+
+postcss-color-functional-notation@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz#5efd37a88fbabeb00a2966d1e53d98ced93f74e0"
+ integrity sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-color-gray@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz#532a31eb909f8da898ceffe296fdc1f864be8547"
+ integrity sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw==
+ dependencies:
+ "@csstools/convert-colors" "^1.4.0"
+ postcss "^7.0.5"
+ postcss-values-parser "^2.0.0"
+
+postcss-color-hex-alpha@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.2.tgz#e9b1886bb038daed33f6394168c210b40bb4fdb6"
+ integrity sha512-8bIOzQMGdZVifoBQUJdw+yIY00omBd2EwkJXepQo9cjp1UOHHHoeRDeSzTP6vakEpaRc6GAIOfvcQR7jBYaG5Q==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-color-mod-function@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz#816ba145ac11cc3cb6baa905a75a49f903e4d31d"
+ integrity sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ==
+ dependencies:
+ "@csstools/convert-colors" "^1.4.0"
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-color-rebeccapurple@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz#c7a89be872bb74e45b1e3022bfe5748823e6de77"
+ integrity sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-colormin@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381"
+ integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==
+ dependencies:
+ browserslist "^4.0.0"
+ color "^3.0.0"
+ has "^1.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-convert-values@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f"
+ integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==
+ dependencies:
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-custom-media@^7.0.7:
+ version "7.0.7"
+ resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.7.tgz#bbc698ed3089ded61aad0f5bfb1fb48bf6969e73"
+ integrity sha512-bWPCdZKdH60wKOTG4HKEgxWnZVjAIVNOJDvi3lkuTa90xo/K0YHa2ZnlKLC5e2qF8qCcMQXt0yzQITBp8d0OFA==
+ dependencies:
+ postcss "^7.0.5"
+
+postcss-custom-properties@^8.0.9:
+ version "8.0.9"
+ resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.9.tgz#8943870528a6eae4c8e8d285b6ccc9fd1f97e69c"
+ integrity sha512-/Lbn5GP2JkKhgUO2elMs4NnbUJcvHX4AaF5nuJDaNkd2chYW1KA5qtOGGgdkBEWcXtKSQfHXzT7C6grEVyb13w==
+ dependencies:
+ postcss "^7.0.5"
+ postcss-values-parser "^2.0.0"
+
+postcss-custom-selectors@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz#64858c6eb2ecff2fb41d0b28c9dd7b3db4de7fba"
+ integrity sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-selector-parser "^5.0.0-rc.3"
+
+postcss-dir-pseudo-class@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz#6e3a4177d0edb3abcc85fdb6fbb1c26dabaeaba2"
+ integrity sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-selector-parser "^5.0.0-rc.3"
+
+postcss-discard-comments@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033"
+ integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-discard-duplicates@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb"
+ integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-discard-empty@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765"
+ integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-discard-overridden@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57"
+ integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-double-position-gradients@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz#fc927d52fddc896cb3a2812ebc5df147e110522e"
+ integrity sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA==
+ dependencies:
+ postcss "^7.0.5"
+ postcss-values-parser "^2.0.0"
+
+postcss-env-function@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-2.0.2.tgz#0f3e3d3c57f094a92c2baf4b6241f0b0da5365d7"
+ integrity sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-flexbugs-fixes@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.1.0.tgz#e094a9df1783e2200b7b19f875dcad3b3aff8b20"
+ integrity sha512-jr1LHxQvStNNAHlgco6PzY308zvLklh7SJVYuWUwyUQncofaAlD2l+P/gxKHOdqWKe7xJSkVLFF/2Tp+JqMSZA==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-focus-visible@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e"
+ integrity sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-focus-within@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz#763b8788596cee9b874c999201cdde80659ef680"
+ integrity sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-font-variant@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-4.0.0.tgz#71dd3c6c10a0d846c5eda07803439617bbbabacc"
+ integrity sha512-M8BFYKOvCrI2aITzDad7kWuXXTm0YhGdP9Q8HanmN4EF1Hmcgs1KK5rSHylt/lUJe8yLxiSwWAHdScoEiIxztg==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-gap-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz#431c192ab3ed96a3c3d09f2ff615960f902c1715"
+ integrity sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-image-set-function@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288"
+ integrity sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-import@^12.0.1:
+ version "12.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153"
+ integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==
+ dependencies:
+ postcss "^7.0.1"
+ postcss-value-parser "^3.2.3"
+ read-cache "^1.0.0"
+ resolve "^1.1.7"
+
+postcss-initial@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.0.tgz#1772512faf11421b791fb2ca6879df5f68aa0517"
+ integrity sha512-WzrqZ5nG9R9fUtrA+we92R4jhVvEB32IIRTzfIG/PLL8UV4CvbF1ugTEHEFX6vWxl41Xt5RTCJPEZkuWzrOM+Q==
+ dependencies:
+ lodash.template "^4.2.4"
+ postcss "^7.0.2"
+
+postcss-lab-function@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz#bb51a6856cd12289ab4ae20db1e3821ef13d7d2e"
+ integrity sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg==
+ dependencies:
+ "@csstools/convert-colors" "^1.4.0"
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-load-config@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.0.0.tgz#f1312ddbf5912cd747177083c5ef7a19d62ee484"
+ integrity sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ==
+ dependencies:
+ cosmiconfig "^4.0.0"
+ import-cwd "^2.0.0"
+
+postcss-loader@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d"
+ integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==
+ dependencies:
+ loader-utils "^1.1.0"
+ postcss "^7.0.0"
+ postcss-load-config "^2.0.0"
+ schema-utils "^1.0.0"
+
+postcss-logical@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-3.0.0.tgz#2495d0f8b82e9f262725f75f9401b34e7b45d5b5"
+ integrity sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-media-minmax@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5"
+ integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-merge-longhand@^4.0.11:
+ version "4.0.11"
+ resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24"
+ integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==
+ dependencies:
+ css-color-names "0.0.4"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+ stylehacks "^4.0.0"
+
+postcss-merge-rules@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650"
+ integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==
+ dependencies:
+ browserslist "^4.0.0"
+ caniuse-api "^3.0.0"
+ cssnano-util-same-parent "^4.0.0"
+ postcss "^7.0.0"
+ postcss-selector-parser "^3.0.0"
+ vendors "^1.0.0"
+
+postcss-minify-font-values@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6"
+ integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==
+ dependencies:
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-minify-gradients@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471"
+ integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==
+ dependencies:
+ cssnano-util-get-arguments "^4.0.0"
+ is-color-stop "^1.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-minify-params@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874"
+ integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==
+ dependencies:
+ alphanum-sort "^1.0.0"
+ browserslist "^4.0.0"
+ cssnano-util-get-arguments "^4.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+ uniqs "^2.0.0"
+
+postcss-minify-selectors@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8"
+ integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==
+ dependencies:
+ alphanum-sort "^1.0.0"
+ has "^1.0.0"
+ postcss "^7.0.0"
+ postcss-selector-parser "^3.0.0"
+
+postcss-modules-extract-imports@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e"
+ integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==
+ dependencies:
+ postcss "^7.0.5"
+
+postcss-modules-local-by-default@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz#dd9953f6dd476b5fd1ef2d8830c8929760b56e63"
+ integrity sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA==
+ dependencies:
+ postcss "^7.0.6"
+ postcss-selector-parser "^6.0.0"
+ postcss-value-parser "^3.3.1"
+
+postcss-modules-scope@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz#ad3f5bf7856114f6fcab901b0502e2a2bc39d4eb"
+ integrity sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A==
+ dependencies:
+ postcss "^7.0.6"
+ postcss-selector-parser "^6.0.0"
+
+postcss-modules-values@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz#479b46dc0c5ca3dc7fa5270851836b9ec7152f64"
+ integrity sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w==
+ dependencies:
+ icss-replace-symbols "^1.1.0"
+ postcss "^7.0.6"
+
+postcss-nesting@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.0.tgz#6e26a770a0c8fcba33782a6b6f350845e1a448f6"
+ integrity sha512-WSsbVd5Ampi3Y0nk/SKr5+K34n52PqMqEfswu6RtU4r7wA8vSD+gM8/D9qq4aJkHImwn1+9iEFTbjoWsQeqtaQ==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-normalize-charset@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4"
+ integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-normalize-display-values@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a"
+ integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==
+ dependencies:
+ cssnano-util-get-match "^4.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-positions@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f"
+ integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==
+ dependencies:
+ cssnano-util-get-arguments "^4.0.0"
+ has "^1.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-repeat-style@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c"
+ integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==
+ dependencies:
+ cssnano-util-get-arguments "^4.0.0"
+ cssnano-util-get-match "^4.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-string@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c"
+ integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==
+ dependencies:
+ has "^1.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-timing-functions@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9"
+ integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==
+ dependencies:
+ cssnano-util-get-match "^4.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-unicode@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb"
+ integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==
+ dependencies:
+ browserslist "^4.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-url@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1"
+ integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==
+ dependencies:
+ is-absolute-url "^2.0.0"
+ normalize-url "^3.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-whitespace@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82"
+ integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==
+ dependencies:
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-ordered-values@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee"
+ integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==
+ dependencies:
+ cssnano-util-get-arguments "^4.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-overflow-shorthand@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz#31ecf350e9c6f6ddc250a78f0c3e111f32dd4c30"
+ integrity sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-page-break@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-2.0.0.tgz#add52d0e0a528cabe6afee8b46e2abb277df46bf"
+ integrity sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-place@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-4.0.1.tgz#e9f39d33d2dc584e46ee1db45adb77ca9d1dcc62"
+ integrity sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-preset-env@^6.6.0:
+ version "6.6.0"
+ resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.6.0.tgz#642e7d962e2bdc2e355db117c1eb63952690ed5b"
+ integrity sha512-I3zAiycfqXpPIFD6HXhLfWXIewAWO8emOKz+QSsxaUZb9Dp8HbF5kUf+4Wy/AxR33o+LRoO8blEWCHth0ZsCLA==
+ dependencies:
+ autoprefixer "^9.4.9"
+ browserslist "^4.4.2"
+ caniuse-lite "^1.0.30000939"
+ css-blank-pseudo "^0.1.4"
+ css-has-pseudo "^0.10.0"
+ css-prefers-color-scheme "^3.1.1"
+ cssdb "^4.3.0"
+ postcss "^7.0.14"
+ postcss-attribute-case-insensitive "^4.0.1"
+ postcss-color-functional-notation "^2.0.1"
+ postcss-color-gray "^5.0.0"
+ postcss-color-hex-alpha "^5.0.2"
+ postcss-color-mod-function "^3.0.3"
+ postcss-color-rebeccapurple "^4.0.1"
+ postcss-custom-media "^7.0.7"
+ postcss-custom-properties "^8.0.9"
+ postcss-custom-selectors "^5.1.2"
+ postcss-dir-pseudo-class "^5.0.0"
+ postcss-double-position-gradients "^1.0.0"
+ postcss-env-function "^2.0.2"
+ postcss-focus-visible "^4.0.0"
+ postcss-focus-within "^3.0.0"
+ postcss-font-variant "^4.0.0"
+ postcss-gap-properties "^2.0.0"
+ postcss-image-set-function "^3.0.1"
+ postcss-initial "^3.0.0"
+ postcss-lab-function "^2.0.1"
+ postcss-logical "^3.0.0"
+ postcss-media-minmax "^4.0.0"
+ postcss-nesting "^7.0.0"
+ postcss-overflow-shorthand "^2.0.0"
+ postcss-page-break "^2.0.0"
+ postcss-place "^4.0.1"
+ postcss-pseudo-class-any-link "^6.0.0"
+ postcss-replace-overflow-wrap "^3.0.0"
+ postcss-selector-matches "^4.0.0"
+ postcss-selector-not "^4.0.0"
+
+postcss-pseudo-class-any-link@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz#2ed3eed393b3702879dec4a87032b210daeb04d1"
+ integrity sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-selector-parser "^5.0.0-rc.3"
+
+postcss-reduce-initial@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df"
+ integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==
+ dependencies:
+ browserslist "^4.0.0"
+ caniuse-api "^3.0.0"
+ has "^1.0.0"
+ postcss "^7.0.0"
+
+postcss-reduce-transforms@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29"
+ integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==
+ dependencies:
+ cssnano-util-get-match "^4.0.0"
+ has "^1.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-replace-overflow-wrap@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz#61b360ffdaedca84c7c918d2b0f0d0ea559ab01c"
+ integrity sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-safe-parser@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.1.tgz#8756d9e4c36fdce2c72b091bbc8ca176ab1fcdea"
+ integrity sha512-xZsFA3uX8MO3yAda03QrG3/Eg1LN3EPfjjf07vke/46HERLZyHrTsQ9E1r1w1W//fWEhtYNndo2hQplN2cVpCQ==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-selector-matches@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz#71c8248f917ba2cc93037c9637ee09c64436fcff"
+ integrity sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==
+ dependencies:
+ balanced-match "^1.0.0"
+ postcss "^7.0.2"
+
+postcss-selector-not@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.0.tgz#c68ff7ba96527499e832724a2674d65603b645c0"
+ integrity sha512-W+bkBZRhqJaYN8XAnbbZPLWMvZD1wKTu0UxtFKdhtGjWYmxhkUneoeOhRJKdAE5V7ZTlnbHfCR+6bNwK9e1dTQ==
+ dependencies:
+ balanced-match "^1.0.0"
+ postcss "^7.0.2"
+
+postcss-selector-parser@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865"
+ integrity sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=
+ dependencies:
+ dot-prop "^4.1.1"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
+postcss-selector-parser@^5.0.0, postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c"
+ integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==
+ dependencies:
+ cssesc "^2.0.0"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
+postcss-selector-parser@^6.0.0:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
+ integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
+ dependencies:
+ cssesc "^3.0.0"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
+postcss-svgo@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258"
+ integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==
+ dependencies:
+ is-svg "^3.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+ svgo "^1.0.0"
+
+postcss-unique-selectors@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac"
+ integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==
+ dependencies:
+ alphanum-sort "^1.0.0"
+ postcss "^7.0.0"
+ uniqs "^2.0.0"
+
+postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.1:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
+ integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
+
+postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
+ integrity sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=
+
+postcss-values-parser@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f"
+ integrity sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==
+ dependencies:
+ flatten "^1.0.2"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
+postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.5, postcss@^7.0.6:
+ version "7.0.14"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.14.tgz#4527ed6b1ca0d82c53ce5ec1a2041c2346bbd6e5"
+ integrity sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==
+ dependencies:
+ chalk "^2.4.2"
+ source-map "^0.6.1"
+ supports-color "^6.1.0"
+
+private@^0.1.6:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+ integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
+
+process-nextick-args@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
+ integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
+
+process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+ integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
+
+promise-inflight@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
+ integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+
+proxy-addr@~2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec"
+ integrity sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=
+ dependencies:
+ forwarded "~0.1.2"
+ ipaddr.js "1.5.2"
+
+prr@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+ integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+ integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
+
+psl@^1.1.24:
+ version "1.1.31"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
+ integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
+
+public-encrypt@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
+ integrity sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=
+ dependencies:
+ bn.js "^4.1.0"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ parse-asn1 "^5.0.0"
+ randombytes "^2.0.1"
+
+pump@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
+ integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+pump@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+ integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+pumpify@^1.3.3:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb"
+ integrity sha512-2kmNR9ry+Pf45opRVirpNuIFotsxUGLaYqxIwuR77AYrYRMuFCz9eryHBS52L360O+NcR383CL4QYlMKPq4zYA==
+ dependencies:
+ duplexify "^3.5.3"
+ inherits "^2.0.3"
+ pump "^2.0.0"
+
+punycode@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+ integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
+
+punycode@^1.2.4, punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+ integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+
+punycode@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+ integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+q@^1.1.2:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
+ integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
+
+qs@6.5.1:
+ version "6.5.1"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+ integrity sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==
+
+qs@~6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+ integrity sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=
+
+qs@~6.5.2:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+ integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+
+querystring-es3@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+ integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
+
+querystring@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+ integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
+
+querystringify@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef"
+ integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg==
+
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"
+ integrity sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==
+ dependencies:
+ safe-buffer "^5.1.0"
+
+randomfill@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.3.tgz#b96b7df587f01dd91726c418f30553b1418e3d62"
+ integrity sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ==
+ dependencies:
+ randombytes "^2.0.5"
+ safe-buffer "^5.1.0"
+
+range-parser@^1.0.3, range-parser@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
+ integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=
+
+raw-body@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"
+ integrity sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=
+ dependencies:
+ bytes "3.0.0"
+ http-errors "1.6.2"
+ iconv-lite "0.4.19"
+ unpipe "1.0.0"
+
+rc@^1.1.7:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd"
+ integrity sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0=
+ dependencies:
+ deep-extend "~0.4.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+rc@^1.2.7:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+ integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+ dependencies:
+ deep-extend "^0.6.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+read-cache@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
+ integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=
+ dependencies:
+ pify "^2.3.0"
+
+read-pkg-up@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+ integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=
+ dependencies:
+ find-up "^1.0.0"
+ read-pkg "^1.0.0"
+
+read-pkg@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+ integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=
+ dependencies:
+ load-json-file "^1.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^1.0.0"
+
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3:
+ version "2.3.4"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071"
+ integrity sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.0.3"
+ util-deprecate "~1.0.1"
+
+readable-stream@^3.0.6:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d"
+ integrity sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+readdirp@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
+ integrity sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=
+ dependencies:
+ graceful-fs "^4.1.2"
+ minimatch "^3.0.2"
+ readable-stream "^2.0.2"
+ set-immediate-shim "^1.0.1"
+
+readdirp@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
+ integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==
+ dependencies:
+ graceful-fs "^4.1.11"
+ micromatch "^3.1.10"
+ readable-stream "^2.0.2"
+
+redent@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
+ integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=
+ dependencies:
+ indent-string "^2.1.0"
+ strip-indent "^1.0.1"
+
+regenerate-unicode-properties@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.1.tgz#58a4a74e736380a7ab3c5f7e03f303a941b31289"
+ integrity sha512-HTjMafphaH5d5QDHuwW8Me6Hbc/GhXg8luNqTkPVwZ/oCZhnoifjWhGYsu2BzepMELTlbnoVcXvV0f+2uDDvoQ==
+ dependencies:
+ regenerate "^1.4.0"
+
+regenerate@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
+ integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
+
+regenerator-runtime@^0.12.0:
+ version "0.12.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
+ integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
+
+regenerator-transform@^0.13.4:
+ version "0.13.4"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb"
+ integrity sha512-T0QMBjK3J0MtxjPmdIMXm72Wvj2Abb0Bd4HADdfijwMdoIsyQZ6fWC7kDFhk2YinBBEMZDL7Y7wh0J1sGx3S4A==
+ dependencies:
+ private "^0.1.6"
+
+regex-not@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.0.tgz#42f83e39771622df826b02af176525d6a5f157f9"
+ integrity sha1-Qvg+OXcWIt+CawKvF2Ul1qXxV/k=
+ dependencies:
+ extend-shallow "^2.0.1"
+
+regex-not@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+ integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+ dependencies:
+ extend-shallow "^3.0.2"
+ safe-regex "^1.1.0"
+
+regexp-tree@^0.1.0:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.5.tgz#7cd71fca17198d04b4176efd79713f2998009397"
+ integrity sha512-nUmxvfJyAODw+0B13hj8CFVAxhe7fDEAgJgaotBu3nnR+IgGgZq59YedJP5VYTlkEfqjuK6TuRpnymKdatLZfQ==
+
+regexpu-core@^4.1.3, regexpu-core@^4.2.0:
+ version "4.5.3"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.3.tgz#72f572e03bb8b9f4f4d895a0ccc57e707f4af2e4"
+ integrity sha512-LON8666bTAlViVEPXMv65ZqiaR3rMNLz36PIaQ7D+er5snu93k0peR7FSvO0QteYbZ3GOkvfHKbGr/B1xDu9FA==
+ dependencies:
+ regenerate "^1.4.0"
+ regenerate-unicode-properties "^8.0.1"
+ regjsgen "^0.5.0"
+ regjsparser "^0.6.0"
+ unicode-match-property-ecmascript "^1.0.4"
+ unicode-match-property-value-ecmascript "^1.1.0"
+
+regjsgen@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd"
+ integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==
+
+regjsparser@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.0.tgz#f1e6ae8b7da2bae96c99399b868cd6c933a2ba9c"
+ integrity sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==
+ 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"
+ integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
+
+repeat-element@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
+ integrity sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=
+
+repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+ integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=
+ dependencies:
+ is-finite "^1.0.0"
+
+request@2.81.0:
+ version "2.81.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
+ integrity sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=
+ dependencies:
+ aws-sign2 "~0.6.0"
+ aws4 "^1.2.1"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.0"
+ forever-agent "~0.6.1"
+ form-data "~2.1.1"
+ har-validator "~4.2.1"
+ hawk "~3.1.3"
+ http-signature "~1.1.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.7"
+ oauth-sign "~0.8.1"
+ performance-now "^0.2.0"
+ qs "~6.4.0"
+ safe-buffer "^5.0.1"
+ stringstream "~0.0.4"
+ tough-cookie "~2.3.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.0.0"
+
+request@^2.87.0, request@^2.88.0:
+ version "2.88.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+ integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.0"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.4.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-from-string@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+ integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+ integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
+
+requires-port@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+ integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+
+resolve-cwd@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
+ integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=
+ dependencies:
+ resolve-from "^3.0.0"
+
+resolve-dir@^1.0.0, resolve-dir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
+ integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=
+ dependencies:
+ expand-tilde "^2.0.0"
+ global-modules "^1.0.0"
+
+resolve-from@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+ integrity sha1-six699nWiBvItuZTM17rywoYh0g=
+
+resolve-url@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+ integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+
+resolve@^1.1.7:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
+ integrity sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==
+ dependencies:
+ path-parse "^1.0.5"
+
+resolve@^1.3.2, resolve@^1.8.1:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba"
+ integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==
+ dependencies:
+ path-parse "^1.0.6"
+
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+ integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
+rgb-regex@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
+ integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE=
+
+rgba-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
+ integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
+
+rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
+ integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==
+ dependencies:
+ glob "^7.0.5"
+
+rimraf@^2.6.2:
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
+ integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
+ dependencies:
+ glob "^7.1.3"
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
+ integrity sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=
+ dependencies:
+ hash-base "^2.0.0"
+ inherits "^2.0.1"
+
+run-queue@^1.0.0, run-queue@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
+ integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=
+ dependencies:
+ aproba "^1.1.1"
+
+safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+ integrity sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==
+
+safe-buffer@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+ integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+ dependencies:
+ ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sass-graph@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
+ integrity sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=
+ dependencies:
+ glob "^7.0.0"
+ lodash "^4.0.0"
+ scss-tokenizer "^0.2.3"
+ yargs "^7.0.0"
+
+sass-loader@^7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.1.0.tgz#16fd5138cb8b424bf8a759528a1972d72aad069d"
+ integrity sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==
+ dependencies:
+ clone-deep "^2.0.1"
+ loader-utils "^1.0.1"
+ lodash.tail "^4.1.1"
+ neo-async "^2.5.0"
+ pify "^3.0.0"
+ semver "^5.5.0"
+
+sax@^1.2.4, sax@~1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+ integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+schema-utils@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
+ integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==
+ dependencies:
+ ajv "^6.1.0"
+ ajv-errors "^1.0.0"
+ ajv-keywords "^3.1.0"
+
+scss-tokenizer@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
+ integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE=
+ dependencies:
+ js-base64 "^2.1.8"
+ source-map "^0.4.2"
+
+select-hose@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
+ integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
+
+selfsigned@^1.9.1:
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.2.tgz#b4449580d99929b65b10a48389301a6592088758"
+ integrity sha1-tESVgNmZKbZbEKSDiTAaZZIIh1g=
+ dependencies:
+ node-forge "0.7.1"
+
+"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"
+ integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==
+
+semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
+ version "5.6.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
+ integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
+
+semver@~5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+ integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8=
+
+send@0.16.1:
+ version "0.16.1"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3"
+ integrity sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==
+ dependencies:
+ debug "2.6.9"
+ depd "~1.1.1"
+ destroy "~1.0.4"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ fresh "0.5.2"
+ http-errors "~1.6.2"
+ mime "1.4.1"
+ ms "2.0.0"
+ on-finished "~2.3.0"
+ range-parser "~1.2.0"
+ statuses "~1.3.1"
+
+serialize-javascript@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.4.0.tgz#7c958514db6ac2443a8abc062dc9f7886a7f6005"
+ integrity sha1-fJWFFNtqwkQ6irwGLcn3iGp/YAU=
+
+serve-index@^1.7.2:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"
+ integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=
+ dependencies:
+ accepts "~1.3.4"
+ batch "0.6.1"
+ debug "2.6.9"
+ escape-html "~1.0.3"
+ http-errors "~1.6.2"
+ mime-types "~2.1.17"
+ parseurl "~1.3.2"
+
+serve-static@1.13.1:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719"
+ integrity sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==
+ dependencies:
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ parseurl "~1.3.2"
+ send "0.16.1"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+ integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+set-getter@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376"
+ integrity sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=
+ dependencies:
+ to-object-path "^0.3.0"
+
+set-immediate-shim@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
+ integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=
+
+set-value@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
+ integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE=
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.1"
+ to-object-path "^0.3.0"
+
+set-value@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274"
+ integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
+setimmediate@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+ integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+setprototypeof@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
+ integrity sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=
+
+setprototypeof@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
+ integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+ version "2.4.10"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.10.tgz#b1fde5cd7d11a5626638a07c604ab909cfa31f9b"
+ integrity sha512-vnwmrFDlOExK4Nm16J2KMWHLrp14lBrjxMxBJpu++EnsuBmpiYaM/MEs46Vxxm/4FvdP5yTwuCTO9it5FSjrqA==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+shallow-clone@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571"
+ integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==
+ dependencies:
+ is-extendable "^0.1.1"
+ kind-of "^5.0.0"
+ mixin-object "^2.0.1"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+ 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"
+ integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
+signal-exit@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+ integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
+
+simple-swizzle@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+ integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
+ dependencies:
+ is-arrayish "^0.3.1"
+
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+ dependencies:
+ define-property "^1.0.0"
+ isobject "^3.0.0"
+ snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+ integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+ dependencies:
+ kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.1.tgz#e12b5487faded3e3dea0ac91e9400bf75b401370"
+ integrity sha1-4StUh/re0+PeoKyR6UAL91tAE3A=
+ dependencies:
+ base "^0.11.1"
+ debug "^2.2.0"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ map-cache "^0.2.2"
+ source-map "^0.5.6"
+ source-map-resolve "^0.5.0"
+ use "^2.0.0"
+
+sntp@1.x.x:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
+ integrity sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=
+ dependencies:
+ hoek "2.x.x"
+
+sockjs-client@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177"
+ integrity sha512-R9jxEzhnnrdxLCNln0xg5uGHqMnkhPSTzUZH2eXcR03S/On9Yvoq2wyUZILRUhZCNVu2PmwWVoyuiPz8th8zbg==
+ dependencies:
+ debug "^3.2.5"
+ eventsource "^1.0.7"
+ faye-websocket "~0.11.1"
+ inherits "^2.0.3"
+ json3 "^3.3.2"
+ url-parse "^1.4.3"
+
+sockjs@0.3.19:
+ version "0.3.19"
+ resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d"
+ integrity sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==
+ dependencies:
+ faye-websocket "^0.10.0"
+ uuid "^3.0.1"
+
+source-list-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
+ integrity sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==
+
+source-map-resolve@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a"
+ integrity sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==
+ dependencies:
+ atob "^2.0.0"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.0"
+
+source-map-support@~0.5.9:
+ version "0.5.10"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c"
+ integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map-url@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+ integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
+
+source-map@^0.4.2:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
+ integrity sha1-66T12pwNyZneaAMti092FzZSA2s=
+ dependencies:
+ amdefine ">=0.0.4"
+
+source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+ integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+spdx-correct@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
+ integrity sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=
+ 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"
+ integrity sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=
+
+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"
+ integrity sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=
+
+spdy-transport@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
+ integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==
+ dependencies:
+ debug "^4.1.0"
+ detect-node "^2.0.4"
+ hpack.js "^2.1.6"
+ obuf "^1.1.2"
+ readable-stream "^3.0.6"
+ wbuf "^1.7.3"
+
+spdy@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.0.tgz#81f222b5a743a329aa12cea6a390e60e9b613c52"
+ integrity sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q==
+ dependencies:
+ debug "^4.1.0"
+ handle-thing "^2.0.0"
+ http-deceiver "^1.2.7"
+ select-hose "^2.0.0"
+ spdy-transport "^3.0.0"
+
+split-string@^3.0.1, split-string@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+ integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+ dependencies:
+ extend-shallow "^3.0.0"
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+ integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+
+sshpk@^1.7.0:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
+ integrity sha1-US322mKHFEMW3EwY/hzx2UBzm+M=
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ dashdash "^1.12.0"
+ getpass "^0.1.1"
+ optionalDependencies:
+ bcrypt-pbkdf "^1.0.0"
+ ecc-jsbn "~0.1.1"
+ jsbn "~0.1.0"
+ tweetnacl "~0.14.0"
+
+ssri@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
+ integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
+ dependencies:
+ figgy-pudding "^3.5.1"
+
+stable@^0.1.8:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
+ integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+ dependencies:
+ define-property "^0.2.5"
+ object-copy "^0.1.0"
+
+"statuses@>= 1.3.1 < 2":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
+ integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==
+
+statuses@~1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
+ integrity sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=
+
+stdout-stream@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b"
+ integrity sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=
+ dependencies:
+ readable-stream "^2.0.1"
+
+stream-browserify@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
+ integrity sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "^2.0.2"
+
+stream-each@^1.1.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd"
+ integrity sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==
+ dependencies:
+ end-of-stream "^1.1.0"
+ stream-shift "^1.0.0"
+
+stream-http@^2.7.2:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10"
+ integrity sha512-sZOFxI/5xw058XIRHl4dU3dZ+TTOIGJR78Dvo0oEAejIt4ou27k+3ne1zYmCV+v7UucbxIFQuOgnkTVHh8YPnw==
+ dependencies:
+ builtin-status-codes "^3.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.3.3"
+ to-arraybuffer "^1.0.0"
+ xtend "^4.0.0"
+
+stream-shift@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
+ integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
+
+string-width@^1.0.1, string-width@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+string-width@^2.0.0, string-width@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string_decoder@^1.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"
+ integrity sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+string_decoder@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
+ integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+stringstream@~0.0.4:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
+ integrity sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
+ 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"
+ integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-bom@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
+ integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=
+ dependencies:
+ is-utf8 "^0.2.0"
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+ integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
+
+strip-indent@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
+ integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=
+ dependencies:
+ get-stdin "^4.0.1"
+
+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"
+ integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
+
+style-loader@^0.23.1:
+ version "0.23.1"
+ resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925"
+ integrity sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg==
+ dependencies:
+ loader-utils "^1.1.0"
+ schema-utils "^1.0.0"
+
+stylehacks@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5"
+ integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==
+ dependencies:
+ browserslist "^4.0.0"
+ postcss "^7.0.0"
+ postcss-selector-parser "^3.0.0"
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+ integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
+
+supports-color@^5.3.0, supports-color@^5.5.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+supports-color@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
+ integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
+ dependencies:
+ has-flag "^3.0.0"
+
+svgo@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.2.0.tgz#305a8fc0f4f9710828c65039bb93d5793225ffc3"
+ integrity sha512-xBfxJxfk4UeVN8asec9jNxHiv3UAMv/ujwBWGYvQhhMb2u3YTGKkiybPcLFDLq7GLLWE9wa73e0/m8L5nTzQbw==
+ dependencies:
+ chalk "^2.4.1"
+ coa "^2.0.2"
+ css-select "^2.0.0"
+ css-select-base-adapter "^0.1.1"
+ css-tree "1.0.0-alpha.28"
+ css-url-regex "^1.1.0"
+ csso "^3.5.1"
+ js-yaml "^3.12.0"
+ mkdirp "~0.5.1"
+ object.values "^1.1.0"
+ sax "~1.2.4"
+ stable "^0.1.8"
+ unquote "~1.1.1"
+ util.promisify "~1.0.0"
+
+tapable@^1.0.0, tapable@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.1.tgz#4d297923c5a72a42360de2ab52dadfaaec00018e"
+ integrity sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==
+
+tar-pack@^3.4.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f"
+ integrity sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==
+ dependencies:
+ debug "^2.2.0"
+ fstream "^1.0.10"
+ fstream-ignore "^1.0.5"
+ once "^1.3.3"
+ readable-stream "^2.1.4"
+ rimraf "^2.5.1"
+ tar "^2.2.1"
+ uid-number "^0.0.6"
+
+tar@^2.0.0, tar@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
+ integrity sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=
+ dependencies:
+ block-stream "*"
+ fstream "^1.0.2"
+ inherits "2"
+
+tar@^4:
+ version "4.4.8"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d"
+ integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==
+ dependencies:
+ chownr "^1.1.1"
+ fs-minipass "^1.2.5"
+ minipass "^2.3.4"
+ minizlib "^1.1.1"
+ mkdirp "^0.5.0"
+ safe-buffer "^5.1.2"
+ yallist "^3.0.2"
+
+terser-webpack-plugin@^1.1.0, terser-webpack-plugin@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz#3f98bc902fac3e5d0de730869f50668561262ec8"
+ integrity sha512-GOK7q85oAb/5kE12fMuLdn2btOS9OBZn4VsecpHDywoUC/jLhSAKOiYo0ezx7ss2EXPMzyEWFoE0s1WLE+4+oA==
+ dependencies:
+ cacache "^11.0.2"
+ find-cache-dir "^2.0.0"
+ schema-utils "^1.0.0"
+ serialize-javascript "^1.4.0"
+ source-map "^0.6.1"
+ terser "^3.16.1"
+ webpack-sources "^1.1.0"
+ worker-farm "^1.5.2"
+
+terser@^3.16.1:
+ version "3.16.1"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-3.16.1.tgz#5b0dd4fa1ffd0b0b43c2493b2c364fd179160493"
+ integrity sha512-JDJjgleBROeek2iBcSNzOHLKsB/MdDf+E/BOAJ0Tk9r7p9/fVobfv7LMJ/g/k3v9SXdmjZnIlFd5nfn/Rt0Xow==
+ dependencies:
+ commander "~2.17.1"
+ source-map "~0.6.1"
+ source-map-support "~0.5.9"
+
+through2@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
+ integrity sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=
+ dependencies:
+ readable-stream "^2.1.5"
+ xtend "~4.0.1"
+
+thunky@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.2.tgz#a862e018e3fb1ea2ec3fce5d55605cf57f247371"
+ integrity sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E=
+
+timers-browserify@^2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.6.tgz#241e76927d9ca05f4d959819022f5b3664b64bae"
+ integrity sha512-HQ3nbYRAowdVd0ckGFvmJPPCOH/CHleFN/Y0YQCX1DVaB7t+KFvisuyN09fuP8Jtp1CpfSh8O8bMkHbdbPe6Pw==
+ dependencies:
+ setimmediate "^1.0.4"
+
+timsort@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
+ integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+
+to-arraybuffer@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+ integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
+
+to-fast-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+ integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+
+to-object-path@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+ integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+ dependencies:
+ kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+ integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
+to-regex@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.1.tgz#15358bee4a2c83bd76377ba1dc049d0f18837aae"
+ integrity sha1-FTWL7kosg712N3uh3ASdDxiDeq4=
+ dependencies:
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ regex-not "^1.0.0"
+
+to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
+tough-cookie@~2.3.0:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
+ integrity sha1-C2GKVWW23qkL80JdBNVe3EdadWE=
+ dependencies:
+ punycode "^1.4.1"
+
+tough-cookie@~2.4.3:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
+ integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
+ dependencies:
+ psl "^1.1.24"
+ punycode "^1.4.1"
+
+trim-newlines@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
+ integrity sha1-WIeWa7WCpFA6QetST301ARgVphM=
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+ integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
+
+"true-case-path@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62"
+ integrity sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=
+ dependencies:
+ glob "^6.0.4"
+
+ts-pnp@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.0.1.tgz#fde74a6371676a167abaeda1ffc0fdb423520098"
+ integrity sha512-Zzg9XH0anaqhNSlDRibNC8Kp+B9KNM0uRIpLpGkGyrgRIttA7zZBhotTSEoEyuDrz3QW2LGtu2dxuk34HzIGnQ==
+
+tslib@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
+ integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
+
+tty-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+ integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+ integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+type-is@~1.6.15:
+ version "1.6.15"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
+ integrity sha1-yrEPtJCeRByChC6v4a1kbIGARBA=
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.15"
+
+typedarray@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+ integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+
+uid-number@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+ integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=
+
+underscore.string@2.3.x:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-2.3.3.tgz#71c08bf6b428b1133f37e78fa3a21c82f7329b0d"
+ integrity sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=
+
+unicode-canonical-property-names-ecmascript@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
+ integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==
+
+unicode-match-property-ecmascript@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"
+ integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==
+ dependencies:
+ unicode-canonical-property-names-ecmascript "^1.0.4"
+ unicode-property-aliases-ecmascript "^1.0.4"
+
+unicode-match-property-value-ecmascript@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277"
+ integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==
+
+unicode-property-aliases-ecmascript@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57"
+ integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==
+
+union-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
+ integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^0.4.3"
+
+uniq@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
+ integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
+
+uniqs@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
+ integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI=
+
+unique-filename@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
+ integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
+ dependencies:
+ unique-slug "^2.0.0"
+
+unique-slug@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab"
+ integrity sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=
+ dependencies:
+ imurmurhash "^0.1.4"
+
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+ integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
+
+unquote@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544"
+ integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=
+
+unset-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+ integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+ dependencies:
+ has-value "^0.3.1"
+ isobject "^3.0.0"
+
+upath@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.0.tgz#b4706b9461ca8473adf89133d235689ca17f3656"
+ integrity sha1-tHBrlGHKhHOt+JEz0jVonKF/NlY=
+ dependencies:
+ lodash "3.x"
+ underscore.string "2.3.x"
+
+upath@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.1.tgz#497f7c1090b0818f310bbfb06783586a68d28014"
+ integrity sha512-D0yetkpIOKiZQquxjM2Syvy48Y1DbZ0SWxgsZiwd9GCWRpc75vN8ytzem14WDSg+oiX6+Qt31FpiS/ExODCrLg==
+
+uri-js@^4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+ integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+ dependencies:
+ punycode "^2.1.0"
+
+urix@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+ integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+
+url-parse@^1.4.3:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.4.tgz#cac1556e95faa0303691fec5cf9d5a1bc34648f8"
+ integrity sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg==
+ dependencies:
+ querystringify "^2.0.0"
+ requires-port "^1.0.0"
+
+url@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+ integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
+ dependencies:
+ punycode "1.3.2"
+ querystring "0.2.0"
+
+use@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8"
+ integrity sha1-riig1y+TvyJCKhii43mZMRLeyOg=
+ dependencies:
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ lazy-cache "^2.0.2"
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util.promisify@^1.0.0, util.promisify@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
+ integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
+ dependencies:
+ define-properties "^1.1.2"
+ object.getownpropertydescriptors "^2.0.3"
+
+util@0.10.3, util@^0.10.3:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+ integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk=
+ dependencies:
+ inherits "2.0.1"
+
+utils-merge@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+ integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
+
+uuid@^3.0.0, uuid@^3.0.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
+ integrity sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==
+
+uuid@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+ integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
+
+v8-compile-cache@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c"
+ integrity sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw==
+
+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"
+ integrity sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=
+ dependencies:
+ spdx-correct "~1.0.0"
+ spdx-expression-parse "~1.0.0"
+
+vary@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+ integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
+
+vendors@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22"
+ integrity sha1-N61zyO5Bf7PVgOeFMSMH0nSEfyI=
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+vm-browserify@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
+ integrity sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=
+ dependencies:
+ indexof "0.0.1"
+
+watchpack@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
+ integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==
+ dependencies:
+ chokidar "^2.0.2"
+ graceful-fs "^4.1.2"
+ neo-async "^2.5.0"
+
+wbuf@^1.1.0:
+ version "1.7.2"
+ resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe"
+ integrity sha1-1pe5nx9ZUS3ydRvkJ2nBWAtYAf4=
+ dependencies:
+ minimalistic-assert "^1.0.0"
+
+wbuf@^1.7.3:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
+ integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==
+ dependencies:
+ minimalistic-assert "^1.0.0"
+
+webpack-assets-manifest@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/webpack-assets-manifest/-/webpack-assets-manifest-3.1.1.tgz#39bbc3bf2ee57fcd8ba07cda51c9ba4a3c6ae1de"
+ integrity sha512-JV9V2QKc5wEWQptdIjvXDUL1ucbPLH2f27toAY3SNdGZp+xSaStAgpoMcvMZmqtFrBc9a5pTS1058vxyMPOzRQ==
+ dependencies:
+ chalk "^2.0"
+ lodash.get "^4.0"
+ lodash.has "^4.0"
+ mkdirp "^0.5"
+ schema-utils "^1.0.0"
+ tapable "^1.0.0"
+ webpack-sources "^1.0.0"
+
+webpack-cli@^3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.2.3.tgz#13653549adfd8ccd920ad7be1ef868bacc22e346"
+ integrity sha512-Ik3SjV6uJtWIAN5jp5ZuBMWEAaP5E4V78XJ2nI+paFPh8v4HPSwo/myN0r29Xc/6ZKnd2IdrAlpSgNOu2CDQ6Q==
+ dependencies:
+ chalk "^2.4.1"
+ cross-spawn "^6.0.5"
+ enhanced-resolve "^4.1.0"
+ findup-sync "^2.0.0"
+ global-modules "^1.0.0"
+ import-local "^2.0.0"
+ interpret "^1.1.0"
+ loader-utils "^1.1.0"
+ supports-color "^5.5.0"
+ v8-compile-cache "^2.0.2"
+ yargs "^12.0.4"
+
+webpack-dev-middleware@^3.5.1:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.6.1.tgz#91f2531218a633a99189f7de36045a331a4b9cd4"
+ integrity sha512-XQmemun8QJexMEvNFbD2BIg4eSKrmSIMrTfnl2nql2Sc6OGAYFyb8rwuYrCjl/IiEYYuyTEiimMscu7EXji/Dw==
+ dependencies:
+ memory-fs "^0.4.1"
+ mime "^2.3.1"
+ range-parser "^1.0.3"
+ webpack-log "^2.0.0"
+
+webpack-dev-server@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.2.1.tgz#1b45ce3ecfc55b6ebe5e36dab2777c02bc508c4e"
+ integrity sha512-sjuE4mnmx6JOh9kvSbPYw3u/6uxCLHNWfhWaIPwcXWsvWOPN+nc5baq4i9jui3oOBRXGonK9+OI0jVkaz6/rCw==
+ dependencies:
+ ansi-html "0.0.7"
+ bonjour "^3.5.0"
+ chokidar "^2.0.0"
+ compression "^1.5.2"
+ connect-history-api-fallback "^1.3.0"
+ debug "^4.1.1"
+ del "^3.0.0"
+ express "^4.16.2"
+ html-entities "^1.2.0"
+ http-proxy-middleware "^0.19.1"
+ import-local "^2.0.0"
+ internal-ip "^4.2.0"
+ ip "^1.1.5"
+ killable "^1.0.0"
+ loglevel "^1.4.1"
+ opn "^5.1.0"
+ portfinder "^1.0.9"
+ schema-utils "^1.0.0"
+ selfsigned "^1.9.1"
+ semver "^5.6.0"
+ serve-index "^1.7.2"
+ sockjs "0.3.19"
+ sockjs-client "1.3.0"
+ spdy "^4.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^6.1.0"
+ url "^0.11.0"
+ webpack-dev-middleware "^3.5.1"
+ webpack-log "^2.0.0"
+ yargs "12.0.2"
+
+webpack-log@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f"
+ integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==
+ dependencies:
+ ansi-colors "^3.0.0"
+ uuid "^3.3.2"
+
+webpack-sources@^1.0.0, webpack-sources@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85"
+ integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.6.1"
+
+webpack-sources@^1.0.1, webpack-sources@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
+ integrity sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.6.1"
+
+webpack@^4.29.6:
+ version "4.29.6"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.6.tgz#66bf0ec8beee4d469f8b598d3988ff9d8d90e955"
+ integrity sha512-MwBwpiE1BQpMDkbnUUaW6K8RFZjljJHArC6tWQJoFm0oQtfoSebtg4Y7/QHnJ/SddtjYLHaKGX64CFjG5rehJw==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-module-context" "1.8.5"
+ "@webassemblyjs/wasm-edit" "1.8.5"
+ "@webassemblyjs/wasm-parser" "1.8.5"
+ acorn "^6.0.5"
+ acorn-dynamic-import "^4.0.0"
+ ajv "^6.1.0"
+ ajv-keywords "^3.1.0"
+ chrome-trace-event "^1.0.0"
+ enhanced-resolve "^4.1.0"
+ eslint-scope "^4.0.0"
+ json-parse-better-errors "^1.0.2"
+ loader-runner "^2.3.0"
+ loader-utils "^1.1.0"
+ memory-fs "~0.4.1"
+ micromatch "^3.1.8"
+ mkdirp "~0.5.0"
+ neo-async "^2.5.0"
+ node-libs-browser "^2.0.0"
+ schema-utils "^1.0.0"
+ tapable "^1.1.0"
+ terser-webpack-plugin "^1.1.0"
+ watchpack "^1.5.0"
+ webpack-sources "^1.3.0"
+
+websocket-driver@>=0.5.1:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb"
+ integrity sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=
+ dependencies:
+ http-parser-js ">=0.4.0"
+ websocket-extensions ">=0.1.1"
+
+websocket-extensions@>=0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
+ integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
+
+which-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
+ integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+ integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+which@1, which@^1.2.9:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+ integrity sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==
+ dependencies:
+ isexe "^2.0.0"
+
+which@^1.2.14:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
+ integrity sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==
+ dependencies:
+ string-width "^1.0.2"
+
+worker-farm@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae"
+ integrity sha512-XxiQ9kZN5n6mmnW+mFJ+wXjNNI/Nx4DIdaAKLX1Bn6LYBWlN/zaBhu34DQYPZ1AJobQuu67S2OfDdNSVULvXkQ==
+ dependencies:
+ errno "^0.1.4"
+ xtend "^4.0.1"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+xregexp@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020"
+ integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==
+
+xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+ integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
+
+y18n@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+ integrity sha1-bRX7qITAhnnA136I53WegR4H+kE=
+
+"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+ integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+ integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
+
+yallist@^3.0.0, yallist@^3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"
+ integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==
+
+yargs-parser@^10.1.0:
+ version "10.1.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8"
+ integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==
+ dependencies:
+ camelcase "^4.1.0"
+
+yargs-parser@^11.1.1:
+ version "11.1.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
+ integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
+yargs-parser@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
+ integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=
+ dependencies:
+ camelcase "^3.0.0"
+
+yargs@12.0.2:
+ version "12.0.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc"
+ integrity sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==
+ dependencies:
+ cliui "^4.0.0"
+ decamelize "^2.0.0"
+ find-up "^3.0.0"
+ get-caller-file "^1.0.1"
+ os-locale "^3.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1 || ^4.0.0"
+ yargs-parser "^10.1.0"
+
+yargs@^12.0.4:
+ version "12.0.5"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
+ integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
+ dependencies:
+ cliui "^4.0.0"
+ decamelize "^1.2.0"
+ find-up "^3.0.0"
+ get-caller-file "^1.0.1"
+ os-locale "^3.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1 || ^4.0.0"
+ yargs-parser "^11.1.1"
+
+yargs@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
+ integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=
+ dependencies:
+ camelcase "^3.0.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^1.4.0"
+ read-pkg-up "^1.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^1.0.2"
+ which-module "^1.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^5.0.0"
diff --git a/actionmailbox/test/fixtures/files/avatar1.jpeg b/actionmailbox/test/fixtures/files/avatar1.jpeg
new file mode 100644
index 0000000000..31111c3bc9
--- /dev/null
+++ b/actionmailbox/test/fixtures/files/avatar1.jpeg
Binary files differ
diff --git a/actionmailbox/test/fixtures/files/avatar2.jpeg b/actionmailbox/test/fixtures/files/avatar2.jpeg
new file mode 100644
index 0000000000..e844e7f3c6
--- /dev/null
+++ b/actionmailbox/test/fixtures/files/avatar2.jpeg
Binary files differ
diff --git a/actionmailbox/test/fixtures/files/welcome.eml b/actionmailbox/test/fixtures/files/welcome.eml
new file mode 100644
index 0000000000..5d6b3c1ea5
--- /dev/null
+++ b/actionmailbox/test/fixtures/files/welcome.eml
@@ -0,0 +1,631 @@
+From: Jason Fried <jason@37signals.com>
+Mime-Version: 1.0 (Apple Message framework v1244.3)
+Content-Type: multipart/alternative; boundary="Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74"
+Subject: Discussion: Let's debate these attachments
+Date: Tue, 13 Sep 2011 15:19:37 -0400
+In-Reply-To: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
+To: "Replies" <replies@example.com>
+References: <4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>
+Message-Id: <0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com>
+X-Mailer: Apple Mail (2.1244.3)
+
+--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain;
+ charset=utf-8
+
+Let's talk about these images:
+
+
+--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74
+Content-Type: multipart/related;
+ type="text/html";
+ boundary="Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1"
+
+
+--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1
+Content-Transfer-Encoding: base64
+Content-Disposition: inline;
+ filename=avatar1.jpeg
+Content-Type: image/jpg;
+ name="avatar1.jpeg"
+Content-Id: <7AAEB353-2341-4D46-A054-5CA5CB2363B7>
+
+/9j/4AAQSkZJRgABAQAAAQABAAD/4gxYSUNDX1BST0ZJTEUAAQEAAAxITGlubwIQAABtbnRyUkdC
+IFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAA
+AADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFj
+cHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAA
+ABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAAD
+TAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJD
+AAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5
+OCBIZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEA
+AAAAAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAA
+AAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAA
+AA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBo
+dHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAt
+IHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAt
+IHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcg
+Q29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENv
+bmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAA
+ABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAA
+AAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAK
+AA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUA
+mgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEy
+ATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMC
+DAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMh
+Ay0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4E
+jASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3
+BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDII
+RghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqY
+Cq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUAN
+Wg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBh
+EH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT
+5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReu
+F9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9oc
+AhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCY
+IMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZcl
+xyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2
+K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIx
+SjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDec
+N9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+
+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXe
+RiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN
+3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYP
+VlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1f
+D19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/
+aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfBy
+S3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyB
+fOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuH
+n4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLj
+k02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6f
+HZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1
+q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm4
+0blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZG
+xsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnU
+y9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj
+4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozz
+GfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t////2wBDAAICAgIC
+AQICAgICAgIDAwYEAwMDAwcFBQQGCAcICAgHCAgJCg0LCQkMCggICw8LDA0ODg4OCQsQEQ8OEQ0O
+Dg7/2wBDAQICAgMDAwYEBAYOCQgJDg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4O
+Dg4ODg4ODg4ODg4ODg7/wAARCADwAPADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAEC
+AwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0Kx
+wRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1
+dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ
+2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QA
+tREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYk
+NOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaH
+iImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq
+8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD9v1Wob5cWEh5q4v3qhvlzp0gz2oA+XvEwiTXbtWwTuJ59
+6/Mn4tCGP9p+OabLR5UEKeB81fo345uPK8Y3lvnkj86/M341XaW3xuSfjYeWz3IPFAHv+r6mINN0
+LdLt3na+Bnj6nmvtn4ISiT4eaeN2VVSAfXrX583Eiah4L8PrCgeVmGZT2yucV90fAZnTwLbiQ/vQ
+SKAPrjTseWMVsL0rhdU8UaF4R8E3/iLxJqUGkaLYw+Zd3UxwqD8OSScDA55r4n8Yftla1r0l/bfC
+rwxcQaWG2w6zrVs0YkyR8wQ4IVgTtJGTQB+iEl1awRk3FxFEoGSXcKB78kV5n4m+MHwu0W5TStY8
+c+GbS/mDlbd75C20dWO0kjFfiD8Y/ib8V9e1Kzmu/Ef9owLcx3U9pZ6o8TSIzlGgAyAQvXH+1XkX
+iP4ca0uq3ev6fJHo2nXOnTCeVSX8iKVlQfvDncVYqrjrhgegoA+xfi9J+zR4ul8RfEZ/iO/2G5u2
+imtYLDfdCYHaDEpwQrY9ORXhej/DT4A61q2oahonxIkdbexa7vYdV0do4lj3KkgV93Ubs4+pr4ck
+8F+LLmaz068WKwuWl2hJnIFwFTd8vcDaAQehwa5PWfE15HomnaQt1NpMls6xsshDCRxv2yAqM4wS
+Dj1oA/c/9mz4O2Pwt+OGpeJ/D9/4e17wvqaobHU9OvUm2wkE4bPKnPav1XsJ1n0xWVw+ByQfbP8A
+Wv4/tB+N/jHwHbKPDGuXaRXOyS5tIZCsSspPzIeBj2PNfU3w2/4KK+NfC3jjT73UL3UbnThax2dw
+s0rSb23sTcFehbaQuP8AYoA/psHRfxplwhe1IHPB4r81Pgf+394d8ZR6g3ia7tJ7CC4X/TreJ4sR
+FQNxRhuyGznAxzX6SaNq+l6/osGpaVeQX1lOgaOWI8MD0oA+SfjX4S8Rz3yX9pcB9PLbJY8cqCOu
+fxr4J8ZeAr6y1YbJ237+cnOa/YfxtZRT+E7oMoI8tsHNfnX8Q7i2ttfjMwjVdvJPrmgDrf2UPhdb
+vfap4q1PM96lwILTP8AwCx/Wv0UtoPs8CoDkAcV8q/sx6jb3nw4uli+8l64fjHOBX1sBx1BoAjC8
+Uu3PHpUoXj/69L0oAwNZ02C50e4WVN4dcN7jHSvgPxR4b03S/FGrQrbeUBOzqM4wCOBjvX6H3p/0
+F8gkbT0FfnH8d9e/sLx1fGSC5hjkAw7IQrHDd6ALPw9ttNj+K2jzMka4uNqZ+9nFfonZL/oEfPav
+x7+Gev8AiDxP8YtLXw9ps+pXVvcrLNGH2oiZxuJ+nbrX6+6OZjo0AnULJs+bFAGnTgMU6igA7V81
+fHG8srDRLWa5kiTbNxu6/dNfSh5U151488D6R4t8OT22p2a3MRXj1U460AX1B80U26QnTpR3xVlV
+4x39ajuQTZyYGaAPjT4nosXjGeXG1sYQj1r8u/jq+34rQzNgAHDhugz6V+qXxYg/4qhwOAw4NfmB
+8e7Zk8dWqlR8/V2XIHpQB6b4Uxd/DPSIRMWLMHDKPbpX1T4U+JPg74VfCsav441eDRY2ceTE0bGS
+VicAIO5JwK+LLDxloPgfwHo+i6xLNJ4mmKyWljaL5hmQ92C9Fr6j+DX7OF/8afHlp8TPiLHfw+Hh
+KH0zSrp9yKi9BtI+XnmgDfu/D/jT9pXWLHUNVguLLwjHcpcaVpkTsINqnlrg/wAbnghegr2vw9+z
+TpHhnTpILxm1ppJzcKbvDorduP7o6AV9g6Zoum6Lo8NjplnDa2sSbEjjQKBirMkAkYbwuPpmgD8a
+/jx+zvf6ppUy3dxp+kWVjKj6febREFk3Z2lgOFI6+4r5q0eP4p+HPi3caBDp9t4u0a5sJ7q50CZU
+P2YpGFMgJ/5ZnCsMZzX9A2t+GNK1rSJrK/topoJVZGBQelfF/j/9mG1l+IGn+LtKudSi1exiaG2k
+trgoAmDjev8AEP4SO4JoA/Jn4m22kReCfDWtabpuqaNf6jDHca4uopi12qCQ0DDn5gSMDtmvhXUd
+Klt/FkkxZ4nW28+1Mluf3SqcBVz13Zzk1+wvxq/Z08QWvgm0t9Age0tjeiSexsZz5NsrqRIsCvzs
+blmX+E4xxXx/rHwB8ceIYtXWSwvnksdtnYwYO+FCOPlxkDHPNAH58apJfzeIDbugaNQ3mb1wVz06
+VNo+hXd9enT0i8+Vx9yEZI9zX3p4J/Y18Za7dPealGYAB5ayyRk9OpAxzn+lfaHw6/Yw8K+GjDdX
+0dxM+P3m9fnYkc8HpQB+afhT4KeO9Z8E2S6Ok1rLMrRho8KyRHHJwfUV9X/D74+fEv8AZ/8AEtho
+vju/8V6Jp9nai2XU4ZfOjuGByMoflBxxn0r9JtC+HGl6Do8dtaW8duqR+UmE/h+vrWb41+E/hXxx
+4PudI1zSYL61mG12ZRvPGMhuxFAHtXgj9pbwN8bPg8jeFNds5tYe1HmxHLFHK4G8fwkn8K/O34ze
+IvEGh/GG80HW5Qt7AqsHAIRgScYzx1r558cfDH4n/ssfE+08ffC+8vb7w+rGG5tjllkhbOYpFB7A
+5DV2Wl/ETSP2lfhdL9tk0jS/ijo48i3+1XbRrLGTkELn5iP6UAfr5+zJ8OLvwr8MotVvdQuL251S
+NLqRWOFjyoIAr61UYQD0FeQ/BN5z8APDEV3JHJdQ6bDFMydCyoAT+lexYB96AGUYNPwKWgCNkDQl
+W5Br5T/ah8I6Zf8A7Out3clsslzbAXETAYYEH1/GvrCvD/j9bNdfs2+KkQAuLByM98YOKAPiL9iq
+K3HxT8cW5jVnUQvGx6gZI4NfqTGirEFUYGK/KX9ja6Fv+0p4rtndf3umo+M9w4/xr9XEIMS4I6UA
+OoozSblHVh+dAC1VuhnT3HXipvMT+8KqXdwiWL8joe9AGAoPPFOkX/RXzTgCDTn/ANQwoA+R/izF
+/wAVKGPKlDgetfmh+0E+nLrFn54dL5lH2TYMtI5ONuO4r9Jfj/qFrotmdTuXRI44mLEngcGvz9+H
+XhO6+Inx2/4Tnxf5ptYbh10TTwMpGRwHP6cUAdp+zF+zJP4z+IFv4g8a28xWGOKYL5eCADuC8/qK
+/aDStMs9J0W206whjt7aGMJHGiYAArifhp4dj0XwHAxiQXUyhpXUY5x0+mK9LVcUAMKcVWlyGGOl
+Xz05qpcDEZNAFB3A61k3Gx3J6n0x1q5cPgEd6xJJMTHNAFC/0rT7tClzbQSIPVBmvMLn4e6Rb67c
+XdlZwq07hpiRncR06V6dNOPM64+tQF8HcckY7c0AcFb+FNNhgYC3i8wdWCbRUdzpdlGuFjHHcCuq
+1CYpFlQ2Se9c1PORuXkkigDmL62jWJAqfxelYEtuU4UZ56V092ZPJL/dweprJkQtuc79w6YoA4vV
+dJtNS0u6s721guEkRhtdQRyMd6/Ff9qf4KS/DL4uDxZoEVxaaPeEOPs+Va1kGfm46jJz+dfuVNGs
+8GSxLA8jvXzp+0B4Eg8afA/VrN4/KuooWaGYLlhwTQBf/YR/aLl8QaBp3grxRqNtc3gsYza3sT5F
+zhcEsD0bjkV+pIuo/LBDhhjrmv4+PAfjrVvhN+1FpHm3t1pWmremGa6tXKzW7HjfjoRX9J3wU+L9
+j8Qfhdavp2rx6rqdhGq3gA2uRtGHI/2uDn3oA+uDexA4J5+tMN/ED3x9a88S7upow2GTIyQDmmlr
+lpMGQ+2TQB3ralEGPP615D8ZdTiPwM8RrlfnsnABPtXSfZrhxuJevOfifo0138LdVjILZtmwPXjN
+AH5xfs++Jf8AhF/2v4HYOUvYWgfnjqCP5V+v9tr0b6bGyvu+TjFfiz8PbQXX7WXh2EKoR7g/pX7H
+abpoGiwnoAlAGu+vjPy5P0qBtclz0asXUL3T9NtmeRkXb97ccYrjJPiT4Ztrry5NUsVc/wAJmWgD
+0Y6pdO3y7h+FVLy8v205znsccVFouvaXqtukkE0UqseGVwRXVfY4pdPZlwwPSgBPtCD+IVFJeIIH
+5HtXnD6+4J5HHvULavcSRcNgepoA8J+P+iSeMrvTPD6TbYJnMt4yx5PlqckZ9T0qD4VeA9Ni8TaW
+tvFi1tmJIcY3MeRx9Bmuj8W6xHpxS5uZYxc3Eqwwr3bJ5/CvSvh5aK0cV2xiUMRsjVcHPTJoA+g7
+RFjsokUAKB0FW6rW/wDqUHtVmgBrMAue9U5zuXOasP8AKMkZ9qpyAtGTnA9KAMm7GMk9MVzUzYbk
+nJPFdNcgDJOTx6Vz158iltvXrx0oAzWAOfm/IVI6qUXLP+Aqtna2BuJJx0rXS3DRZPpjA9aAOS1A
+b3cIWYD1rl5jiZvvbSOSD0rvbmyzIQCVbGc/zrlru2xFKwQhc/dFAHPSAtjIOzoCeap3EZcGJsq2
+3IYKeaufP5qD5goPAxSF90rHf83TINAHNtbqB8uVnJwc1zfi23YeDbxRF5p8k7sLmu/8tTMemeu4
+jNYniC3WbRXH3d0TKcHAORxQB/O/+0X4TGnfHDV7kwAWtwxLxhcEdwR719n/APBN/wCM1hpHxE1H
+wVr4cXs8I+zXTHLSRDpG/rjqPbFeVftRaOV8a3lvL+7uoyT5eM7k/vZrwf8AZ28Rx+A/2vdC1yec
+WtlHGxedVDHGRxg98n8qAP6MvFHxg8MeHb2W2uNVtRMibvKjILYxkfoRXlQ/aT0l74Rw+Y6g5zuA
+r5TtfgP8VPjF+2r4k1SDV4dF+G8kUElvejLyT70VnCqOnJIHtivraz/Yl8FWunfLrPiFrwIMyNKv
+X1xigD0nwd8dvC+tahDaz3y2t052rHK+M/TtXr/iXUNMvPAN5+8WRXtmIIYYIINfnR8S/wBnbxf4
+BsH1jw/e3Wv6VCSWiCDz4sd8jtXI+Hfin4rm8ItoralLJFHF5WyWP94oHGDnnigDiPCur2ui/tfe
+H724Kx20OqOoI7gsRX6x6z8QtE0P4cyatdXESQRR5GJBuNfj9eaHv8epeyNIiRyiQtjGD611PjLx
+l4n8QX2keCdDmu7/AFC72xwxLJuGT/EfQD3oA6b4k/H3xZ418bvo3h23vnDyYgsbI73YerEVgwfB
+74/arp66uPDDxqfuwz3KiQ/m1foN8Av2evD3w58K2t9c2seoeKriPdfahIoLFz1VM9AK+pU022SM
+KI0X6DkUAfiRo/jz4k/DLxoIdRg1nQLuFvmtrk5hkHoCTg56cGv0n+BvxusfiT4ZuYZ1S01i1Rft
+NvnoOPm/Wuw+Lfwp8PeOfAF/a32n273Rib7PPj54mAOCD9a/LPwh4lvvhH8eZrmTcFj821vR03gE
+YPv0oA+wdT+JOmWCmeXVokY9FZhWt4O+KmieINdOnw6lZXE687N3NfCei/sifHfWvD0V14m8e26X
+fa2SIkD8a4bWvhr8WPgd4ph8QXEyX9tZzh/tUbZ3A8EP/k0Aff3j3UDd/GvT9Osx5zgRFEYZX5jy
+R9BX1Z4LiA0a0d1EcxGWGMdsV8QfDjVLnXviPc6zf4897G2KW5XhGZclgTX3/wCErMy6fFKuPLRQ
+oHtjP86APQbc4jUe1WjytQDCqPYU+WaOGAvIdqjqaAGzEbTyOlUTINmM8is+41yyDOvmqFH8R6Hm
+sefXrMT7FuIM9wW6f0oA1rmQFWAZRxXJ6nchoGUOcj0ps3iHTPtBj+22zT4zsVwTzWNdXkM29VYE
+hucUAWbb57sFi5Xp1rrbMp5Wzg/hXHxPHGZGEinH88ZqzpmpCe9aLzASvJxQBq6tIsKk8Z71wt7d
+K1wI/wC8Ogre1qVpZv3WXyOOeK4p3mMiO6qnXnPH60AMmh+csMHH+1VWe1PlZBNOa4T5klnVJeoD
+EAH8azn1yzRGhN5ayFW+crKp2/rQBYVGR8E/LjFEkMc0Dps8xAhDJ7mnFormz3wzblOfmXnBHak8
+mS3tFcgNuOD3P40Afkb+11p0ln8QEfyE+0NG2GI5MeT8v86+FdOt7P8A4S/Ss23mWksiM5Xg7DuW
+T/vng/hX7J/tYfDNvEXw+/tvTYQ8tsv71UHzNX463dmdE1WeHMqtFd9BztznP/6qAP6Mf2MbhtW/
+Ye8KXd5GXvbMS2byt/y2WORljf3+QLX2AqgghcY+lfAX/BPnxRbah+wRplq0redaatcwN5h5ILBx
++jCv0Et9phDD5uOwxQBmXmkQ31q0UsaFXUhsjOR6V+f/AMd/gMnhjVpfHvhmN47czF9SskXK8nJd
+cD3zX6Odq5/xLpMGteDNT064UFLi2ePpnqp7UAfij491a2ttGa4twhDruyD1Fe+/sYfDo65far8T
+NZt/MneU22m704SMdWGe5r5R+KOkXeia5rGhXqTI9lcPCMjhlDcEfhiv13/Z68NQeG/2b/C1jArA
+LZISSO5GSaAPdoohDbqiAcdKmzmkZgqkk8Cub1bxLY6VCXupo4VHdjigC7rlxFBocrSHACE5r8TP
+ifa/8JZ+0JqGnaQHmlv9UkjhCdTzzX3P8bvj5YWHhe/0zRLuOS6ljKtcI3CDHOD0zivGv2VfhjqP
+iv4nT/EvxDaulhb7otLjljIMjnhpORyOeDQB+gMccSLsVVAHQcV5r8SPC2m+IPAGo2d5AJUmi+Yd
+jg969IxtbJzXHeONUt9M8FXs88ipEsRLMegHqaAPjn4b6dLZ/F7UQd8sjgMGEoIjijyAAv04r9KP
+DlukPhi3YIwZ1DYPB6V+cHgvxRpD+O7S+02CNreOUW81ymPl3PkCTngZ716n8VP+ChX7MPwN8S3P
+hHxh4t1S+8UafDG13p2jaa9w8bOoIXcSq5wc9aAPuaV/LhLBCzcgDsa8u8R+IL5JTH87IeCqDBHN
+fOPgv9qH4sfG/wCHY8S/BT9m/wAQR+GLpd2m674+1uDR7W9TODJHHH5spXjqVGe1a2sP+1rLpLTz
+j9mnwyQMiSS+1O8KDv1gUUAcl8TPH/jDw8zvY+Gtf1lQ/wA6WKDESdiMn5j6ivknxP8AHT4k6pLP
+YaZ4A8QafbSykW9xJK6SSN0OVwdvT8a3PiR8Xvjf4X1aTTda+L/wLudRmQywWml+E7uYEDqCxcYP
+1r5ktv2j/jPca1DDc3XwpmBnKLnTrm3Zz6/KzYoA9Eh+IPxIu9YjkvfDWoadqUISIXKxMzEbuWJ4
+6fSvpnRfix4h0uG0tdVmlluDKUQgE+cOCD+Wa8Y8M/FD4rX2mC8b4c+EPGzRDDJoPiTZdfMM8xXM
+anHXvXRaR+0B8Kz4ks9A+JPhTxd8INWL7EfxXpgjtAxz9y6jLR4PqSBxQB92eHfET6ppqzApIzxA
+8HjJrQh1uPR9f3zMiIRhgetWPB3g6K68GWWp6K1td6dLGr29xayCVJlPRlZcgqRz1rn/AIheGb59
+MutsUlvc+WRHIVOM/hQB5F8Wf2l/C3geymhkik1O6jO5bZDjdnjqK/Orx9+2T8QNR1NhoerT6RZw
+zOu6KNeehA+YHpz+dYnxzbR9C8feX418U2sd+rsfs8MvmSYHT5Rz36V5nZ3GhT6TDcaT8KvG2r2d
+wQYrq9gjtYpj3I8xt2D64oA6Wx/bB+KGoavDa39zc6lbBSZHiiAd/TOAK7VfiF4x1uddRsbTxLbS
+mPDfZ43ZexYkY54xXEWPxO1zwzrzR6Z8EfCNo1uVMi6nqgLIp6E7UP6E19D+B/2lviRf+Ko9Mt/B
+nwb0uW5IEEOo63NbRMMfMRIIiOlAHT+BPF/xOQrNFd3UIkkU2ofiM9jvBPfHpX234E8bXXiOxey1
+zR7rRNVjj+ZiQ6SEfxKQeh9K8X0bWPjfq+lpqy/Ab4WaxpUsfE2g+OUcyYJyQssKjP41har8edR+
+HYWbxz8APjd4aslc4vNM0uDVYkA65+zyFto65K9KAPqnXtNg1TwvPaXG0FlwrEZGcelfz/8Axn0t
+9G/aB8T6e0oSVbyRQDHtzzhSB9K/XXw9+2T+zZ4xttkHxU0bSL1JhG9nrkT2UyOMcMrjj0r84f2r
+H0iT9rqDWdOvbG/0TV7Tzbee0ZZI5M9JEYcHoR7HigDa/Z0+PHin4YfD3S7fT72M2E2tTxypOpEb
+nZGcA9mr9d/g3+1Jpvi3xrp3h3WLM6beXqYt3WTekjDGRwetfN/7CHwe8LePP2ANXufE+g2Wrwze
+Mbs2b3MKkgJHGmVI6DKnv1zX2r4U/Zw+HnhTxZHq+kaDDb3cZzE+4nyz3Kg9KAPpKORZIwynIIzm
+ormZILOWWQgIqk5J6cVSghktbVETJOcc9Kp+IrG41DwxdW0TbXeMqCvagD80P2jdD0298WanrkKW
+y+bOAz7hg4719Ofs5fE7TPFngCHTUljXUdOjSK4iB6DHB+lfD/7QX9taJruoaBqAkXyJfMR8fK6c
+4P1r339iv4e3dj4Nv/G9/Jum1kr5Ean/AFcanA/WgD7/ALzzG09vL5Jr4B+NHgL41eIfiIp0OCHU
+tFf5IlFwYxCfVh3r9DQuItpwaryWkUsgZ1DEHJJHWgD4D8AfsmtLfWeqfEW9Or3Snc2nRDFshz39
+a+6tD0Kz0bSYLSygitreFdkUUabQorZSKONNqIq+mBVjjbz1oA+XvFfxF0nw7EzXN4iEKTtB5OK+
+BfjF8Y/G/wAUdI13wX8IfC+veJ9R2hb+ewt2dbVSQAXIGAD/ACr6O+MHwBuviFOHh1rUdJlAIL2s
+hGc19E/s+fCjw/8ABX9nqx8O2Amnury4kuNR1CfBmuJGOAWYegGAO1AH5p+D/BPj/wAMyQ2nikRa
+JDPpyz61ehCymBEIkG0DJcdABzmvCP2tf2O7Dx18B/Ev7Tnh+bxfoWvJDHe3vhvXIVEcunxhEyvA
+aJ/LBkwxJGce9fu14x0XRPL0uQ6fay313qEUcbsD8gzuYj/vmrvxK8Gad46/Z38a+D9St47iy1rQ
+rqyljK8kSwlev4/WgDA8B3+g6P8ABnwh4e0wC107TdCs7e0j9I0gQL9eMc9818c/tQ/GDVbLTV0n
+wxBc3l3NP5dvaxNtkuJMckgZJQf0r6I+CenQeLf2BPg7qkgY6u3gzTrbUW6EXUFskNwrZ/iWWN1P
+uprc0v4baZ4d1afU2tYLnU3yPtdzCsjop6qpPQfSgD8qvjP8IbL4cfsC3HxK8V+E9R8Y/EnxD5Vs
+ki3726aCZlyJSF4cL0wcZJHNflh8Ix4k8U/H5tNsYtdvvJMkxiRAjEA4AmJBABweByQeor+pTxjo
+un+I/BOpeHdat7PVNGu4TFNa3AwpHb6Edj2r5MtPgr8N/h3Ndy6FZXuixTNulSDVZZBIPT5jmgD5
+C1Pw/p3w2+KulaUuqXdrBeW6SQNE7+ZYyPjfGxA5jB7E19C+G7G713Vrzw547tNM1bwRJYsNSS7U
+SW7W7Kd8nIOAF+YntRd+AND8R+N47tNEdLBAwmuJ3O+XJ7nOTXR/Gn4d+JdP/YWtbzwpd22n6jHf
+2VlpOlSs63GtXEsywWlkrKwxvmdC+cr5YcngUAfGf7IH7JHjz4taz8X9Z0j4+/Ev4W/AbRvF19ov
+hy38O6i6XGpmCU/OvmZVIkRkXhTuJI42nPo/x1/YZ+IHhb4banrPw/8A2qvjLqGpRRM4tfEV+Xiu
+DtJ274ymzOMZINfqv+zt8J7H4Tfsd+Dfh5a3BvU0OzaK4vCpH2+9aRpLy6YEk5kneVgMnAIAJxmu
+u8X6dZXPh+5tbmKOWCRSrKy/ez26GgD+YT9mzwx/anhjU9cv0XxH8So9ceC9+3sZ57dUO0Ehs/KT
+u59q+1Y4NQ8R/Fa18Ixam8d+I9mpXgcFIU7xRA8AkZGe2OnFeJf8Kq8W/Cj/AILBvovhTWrHT7TV
+rifUIxODtvbVsG4hAx80mzayDsd1fTllpPh7SfGP2m20bzNQhuGaaOclnQMxzznnrnNAHwF+07p4
+8O/FLxDp9l/bemT6TqAt7Kz8t3gS2aNT5rOGyzMT9PpX1Z+yP4A8PfHD4a6x4Z1HRr+WXT9Jjnm1
+LVblW+z3TMw/dKACkZXB2kk5zzX1BrXwM8EfFSW31G8SW11Z4ET7RDc+UxxgAOCGzwK9o+HnwCm8
+A+Fp9E8LS2WmWFzk3twDumuTggFnAHAAHGKAPjT4aa/47+Dvx1l8Im4v49Fiumt4lnB+y3MYPRc8
+KT/eFfpA3jjTpvBUOoGb7HOQP3DOQ68cjj/JryM/Ae61W626tqF1qQChYy5ztGc4Bz2z1r1Pwl8J
+I9Itxb6jqEt3GhwiyYYgdsk0AfhV+3l8OIPHn7efhq58A+HILK+1vQpJdUaO08uOV4JGDXDgDrtI
+BOOcCvcvhH8GvDPxQ/4JBaN4Og1nTrP49/DbxkbeWOdtsws7+72qjK5BeB1lVlYZwy4r9FW8B+Hr
+z/gpBq2rf2dFLHoHw+t7BUl5Ec95dzyv14OY44/w+ted/HvQ/C/hn4t/C/xbqejaV/Yt3fLo+pq8
+KgbWYywPlRkMkq5U9iaAPrL9jLwU/wAPv+Cavw10i8Ahubq1l1K5DJ5e1riV5QCD0IVlFfU8TB4l
+ZSjIehVsiviXWNNubvSDd+ONa1PXtB0uAtZ6X5ax20MajCqIYwokOAOZNx54wMCu4+B/j8ap4hGi
+xWk+naTNAWs7WVt5jIG4Ef3QRn5e1AH1VwR2NI5+RvoaUfdFUdRuUtdJmmdtoVGP5DNAH5k/tXQr
+qnxIv44UDLFbAOQM8n1r139izxXHqv7PEekOcXWk3L20qkYOM5XNcN9mj+IXxJ8bM4+0Ib1xGSM7
+VAwB+lcx8FHl+E37buq+Db92gsNfgWa0APymRf64oA/UCiobV1ms1ZWyCM5qzx7UAJt5p1GR60ZH
+rQB5oFBYE5OPXnNdLIobQ7FAdqMyjA+tYIGOxroLMCa1tQ/PlzDP06igCj4jgjl8UeF45BuAvmeP
+nphD/jXYj/V+uBXBa1cl/Heil8lYZ2x6DIxXcr93k9s8UAfLOnaT8Zfgv418VW3hrwpa/Fv4Vajq
+k+p6NpthqEFlrOhSXEjz3UJ89liuIGmeSRNriRd+3aQAak1f9prwZpemLN408EfGTwLCx2tPq/gm
+7ECt3HmorKfqODX1HkZPSqUzSeWyoxXjggkYoA+Gte/aL+BWoWgvI/iXaWMDtjbcWU8b4z3UpkV5
+PqXxz/ZyFw0svxH0zU5hu2KqTysfYKEr9D9R077W379VmbGCWUHP6Uy28OWULCUWkEZA/gjC/wAh
+QB+f/h/46fDjVr54fCnhT4p/EK4i+ePTvDng27k8zjJUyOioPxPevpTwP4V8feOPHWj/ABC+K/hq
+x8D6RoTyy+CPA63S3U1pLLGYvt+oOvyNciJpEjjQlYllkBJYgr9BxSNEwjMj+X0xuPNacWLm8RcH
+YvJPqaAL9lapa6XFbxLtRVwAa8+8a7k06QxqTg9MV6TI6RozOcCvP/E9wJbCYLjBFAH53fHb4Y3X
+i7WdJ8TeE7m30H4i6O63PhvW5Uylrdx8eXIO8UsZaNvQEHtivID8WvC8lzbWHxc8M658GPiKF23M
+txpslxpV2y8GSC6iVl8ts5AbBGelffWoWMFxJcQTACOTBBB5B9RWVFa+TcfY50MJPPnFidwH6ZoA
++cfC/wAVvg7dygJ8SPB2+JwhY6ksbDA7bsV9A6f8Tvh9DYxsPil4OWA9PM1eEDHv81WJ/B2kaoJJ
+L3SNC1IN90XenRSH8SVyafB8LfALMDL4D8Fyucbn/seHI/8AHaAHTftCfATQrQR6l8Zfhxbyj+/r
+MZP5AmsHVP2sfguNKkg8LazqHxH1mVCtvpfhXSZr2e5bB+VcKFGf7zEAcEmvSLLwd4S01Qtj4U8M
+WpXo0WkwKfzC5roBI0dm0EREUWMBIwFAH0FAHiPwo0fxRHpnijxr4901tD8VeMNXGoy6ObgXD6Ta
+JCkNraO44LpGm5tvAZyMnGa8Z/bVvIbb9ky1iZGMp1eAxvHj5DuJLc9OlfYshYq2CDkc1+d37eGq
+xxfBXTtOLMBLqiRsVHAUAnn60AfdOif8Tn4U2+p+YJ0utLjdeRtY+WDn8a4r4KpJP8Z9LCblEM0m
+Qp4AAP515d+yt4qutb+DGlaZdXD3It7IQhT/AAqFwB+VfSnwQ0dLG+u9SlUAxvOIiepBkIH6UAfV
+W84POB2rxv4yeJx4f+EuqT+ZtkaMxx4P8TcYr0s3a+QSzEfjXx9+0JrP9oTaPoolyjTGSVVPp0zQ
+ByfwJsp5tV1ed1LISvmHH8Z61yf7UGj3mg634W+IOmq63uj3auTGMEr3BPpXuHwKt47XwnOzgB55
+y+K9I+Inhmy8VeBL/TbiFJ4ZIiNuAeaAOw+Gviu28VfCrRtXgkDR3Nsj4J6ZHI/OvQ/MFfn1+zn4
+rk8F6trnwy1u5ZJrC8drIynG6JjkAfSvtM65B5akODlc/e4oA7EzKD1FMNwgNcI/iK3BO6WPHfD9
+KzpvFtlGfnuY1696ANlQW2kdDWtprkNNGccgEfhWJZyCWwjcNkHpitmy+XVYm/2se3IoA5LxvdjT
+tSgmLFVDeYTnHCjcf5V6Jp9yt1o8E6n/AFkSkV5x8XdP+0fCm/1CGMNewrtjBOOvek+FviWLXfBA
+XcBLEem7PH/6xQB6pTTtbgioy3ynHWkDYGc80AQmKMyHIxj0rPupdsfyk4qzNMVjcnGa5y9vMAAD
+B5oAhmuQsuWLYBya7PTgselRytw0nNeVNPLcXgjHzMeta/jKXUJvhrJb6bczWN3JaNHHPFwY2IwG
+HoQeaAOm8QaqkFiMH5jxgHp6V5zrOqwiwhikLnzCdxHavnX9nLRv2lbfwr4z8M/G7V7bxLo1hOh8
+Ka5cbFvrhDu3JKVADAALg4B46nNdPrN1qi6w9lcKsUsTfL5zbRj19xQB18dul9NIYgvykAkZ/X0r
+Onl2aqLCeGMSIMlycg185eGNK/aGt/2ztf8AE/i7xRFB8K7a3aHRdEsI4xbzqwAEr4yxb6n8K+gd
+RlF9NBLHuVo4/vEfeP8AkUAdHbqrwrIg6HoOlasKDzBgsuRzzXOWk5eNNhZcD5gPWuhiYpGrj5z3
+BNAF/agTaS2fWqUx2nJOFPGR1zQb2KRiELZBw2V6VBIwklGG3KOgI70ANLBQS21UXhjux9TX5P8A
+7cXiO0vfEXh3w6/mSs9z9okRTw2GAHP0Jr9RNcv/ALF4fubjjcqMB7nFfhd8e/FNv4r/AGx3Essl
+5b2t9FCkZ5AU7QcY/GgD9Fv2U4WHh/xZPaxmLyVZbJTIPkXbx296+m7/AOJvh34domjXd5bRXgiW
+SZTJlskZ5A9+awPgj4J0Twj8MrS30aORjqMQOZCGZQcEknHoa+R/jJ4P8S+KP2i/F2pR3E0NibwQ
+xiMZIVVAGP1oA+2bX46aHeaC0ttcmX5SQea8fv8AUJPFfjVdRuI3eLqpXpjtXiOheDda0vw3bx3F
+2zKflGVwSp9R619BeGNOMFgo+YYTbgd6AJ9M8cJ4PYwSLweVABB/nXuXw88bw+MtDklAKujlXUnu
+a8C8QeCP7cu43MbqB1bPNegfC/w/J4W8RzRR/NbTDOD60AeD/tEaTfeCvjVpfjTTA1ss8YhmdO5F
+VbP43a9eaPAIJnZlXDlVzX1P8e/CSeJvgnfCOISXMEZmiJ5ww/pXw78ItPi1fU47W6jVQsjRyAdd
+woA6+b4oeK2DYeVgx6KORWRceOvFl4JCDcljxjJr6Tg+H+lrhvs6cDnK9ad/wg2moxbyUAPt0oA9
+y+G3iODxL8MdF1a1ZZLa7tEuImBzlWXNekxkpMjDqrAivz//AGK/F1wfhXqHgPWZWGs+E9Rl02dT
+/cVyEI9sAV+gEeSgYtxQAeNbBdW+FWrRkneLRpUwcbioJx+lfKPwQ10WPxIvLFbiGO0m5WEtyAcn
++dfTuvambHwfdrI3yPC6IxGQuQea+DPBkk/h79oi0t7qS3kzuRWCcuoCuG+g3GgD9IlcNHu7VBI+
+AeaxdD1L7Tp5Z+jn5SfTtitWcjqDxQBk3cxAZielcndtJI4+fac4HvXQXAaW48teSetT6do6Taj9
+ouMCGL7qnuaAH6LobQwiecAu3PPpXVNaWz2QjmjSRAehqGW7VQVyqjGMVk3eqxxxgLJlu/vQBmeJ
+ruPT/D83kLsiij3bQeuBXxd4l8d2mqeNZo9sCXUWQBKeCB24r3vx9rc03gu/SOOV3MT7lA5AFfCW
+g28moa1qs9zAIriK93QkOcquOQSetAHsGk+Lnkmis53jks5hveSInauD9017NpTWE1sjRliNgbbt
+xg88c18369p9jpi6bcyLkTtiMJIQu4jjOPerXh/xRqekSzTXRnnRo95CNvLkHG0enWgD6aitxHdN
+Mny55welaMDliWOGUjGK8dt/iEspjR4HgZQBJuHKk9M12uk66l/cI0MgRGcocj7rYGDzQB10jhYy
+Q7AdCoFQmQmLaoGCOAPX1oM6l5AQpOOF/rWNc6tb2FrJcSsmVUgJ6nFAHjXxz8XL4c+H+qwRTJDc
+R2jujHkbsHrX4W6Pcz6h+0D9v1BozcSXSSJK0m1Qd4AJz25Nffn7S/xVs1tfEVib5BeMoEdu5wGA
+5P5Zr82fB13Pe/F6zinRpruW4iW0VCCOWAXPbGaAP6aPh9FDY/C6xYTGeOG2AEqsCDhQDgjsSK42
+40OKe7mufKV5biVpHyOpJqD4YyX0XwVsLC/uYLi/EYS4+zsWSIqACBjg/wCNeg21t8hkYEKoxz7U
+AeJeLLWOzkihVBkEZA7VveGZIWC26bCwAPJyayPEkjT+MPJTEmSQSQTiul8I6LLFqr3Mm1lzgcUA
+d/Dbj7OuEUH1x1q0irHdo4UKc54qZuE2jjFNwSBu7UAd3IsWq+E5IpFBV4yGB7ivzbFtJ8Pf2vNR
+0xgYbCe6MkBx1zX6K6BdKY/IbpjGDXy/+0x4JIgs/F1jC32qylzI6ngr3oA9osZvtWkW8y/xLkgf
+Snyjgj73qK86+F3iFdc8A2ZLbpljwxByOK9MnA8rjg45oA+Fy8/we/4KzQOSYvDfji32njCrdRk8
+H3Oa/UXTLkXOlRvjhlzwa/PX9t3wrej4RWfjrRoHfWPDGpRajblRyFU5cce2a+t/gr41tPG/wX8P
+eILOZJIL6xSYAHJBI5B9waAO88Ypu8C3eACwU4z06GvhdZ7OHxJC8kdvHcacSkcu7LsG4fd+lffH
+iGAXPg+8hHDGM8+nBr84fEyz23jzUYtPhDPGyuzY+8GfBzQB9l+GfFcK6NGzXILsqeVG2OV2jkV6
+/balFcWO1yiOQAvzdTX5/wDh3xVFe6jLYsXihkt1jgBPzxOh5Of7uK+ltE8R+dYCNJDJNakLPnsw
+H9aAPahDh5JASMDr6GrWo3/9nWixrt2hNzMe3HJrM06+GoaArk4fHIrjvHurvFo6x20ck88oUKB6
+5xigDattQm1GFpVJdNxwcEB/oaR3s4pB9puVY5+4vJX2+teQ6f4c+Md9qdoLOXw9b+GXB+1wSXTp
+dKexTClcevOa9CufA3jRrURabrOg6MeMO8T3EmcDOTgdeaAK+r31mbCSEaZcSrKfndmCkj6V59Jb
++FNKL3EumD7RKxMpYIM+nTis/wAU/Df41zazHNB4u8G6nZRkma2ms5YHPPADKT/KvNfGnhn4z3cN
+vp9l4X8LPCp3yzR60wIOOmCm40AbvifxFotxLHbLo7XtuMDEJH7r0OK81nvPD5tZbYSjT33kKr/u
+y34j0/rXlV38LvjRfeK7q/uvFsPhywcKBYaYokIx1ZmfBq2fhNr8unSwan8QNTvA0ZCbbdGYMeOO
+M0Ad+1tdRadcPDi8jbDYS4Jyv+93ru/BkE1rpdws3meaAssblicgHGK8e8G/BnX/AA14Unkl8e+I
+NQnL/uYbjAjiXPQjFe/ac/m6fFAAGukdYiVXaCCRk0Ad54p1ZNC8H3GpMIUl8jKZPGcV8E+Lvi3d
+Jpup6tJeCODHlRqkhI8w54x+Br6F/aN1y7svhbJa2ilpSmxUH8bHI/TGa/LzXry7bwtdWOozNBZW
+YeZWjGfMcDnr6ZoA+fvHPiLVfGXiy+v9TmWVoGkCK6YEkZruv2cfA8HjL4sq1/Oum2VraNcI6AF3
+bdhVGa8t1xoF1O0061DS7o/MXcPu5Hc19ffsq6ZE3imGeeGNLgwsjxbcAgdCPwoA/XrwVYpYfD7T
+LK3thBHFEFwrZz6k+pPWu+v/APRfDjSDG4qc5rnPCke7RLIKP3eOlWvG18tp4VfB7dB9KAPKrErd
+eJrlmJyrZznPfpXrekWogsQVHykZryTwjBJI/nMpyzZJPWvdbWHbp8SgfMR+lAFZlO4EHGac+DFx
+96pMAscimBeelAFywlMGqJzgN39K1vGehWfiX4b3unzIksc0JU7RXOKxznHI613mkSi60jY3U9c0
+AfBvweu5PDXj/VvCt3K8Ztrlkj3emTivrJk327OGBGBj3r5W+K2nS+C/2pbLXBGwstQfbIU4APAr
+6U0O/j1DwxbXCKwXyxgE8mgDpfid4Zg8SfDfVtNnQSR3Nq8ThhkEMMV8V/sS+KJdA1Xxf8IdVd4r
+3w3qMi20Uh+Y2zsSp96/RbUoBcWUkbKXyMH6V+X3jK1l+D//AAVU8GeM4kNtoviqJtM1F+ieaAfL
+J9ycUAfqpcos2jzgdWjOPyr84viPfQ6D8VtWhNu11cXtwLdsEI0S/MwYDoRkV+iOk3X2zw/HMGD5
+HGPpX5i/tXRzaL8evD9yqv8A2ddBmu+dpkCg4QHqCT6UASaVdWcmvPLDbra2N4gSBwdxDA4c+xr3
+LQdWKeMriDzpHiuYVdiOnHAb68V8Tx+JNUvtVsNTsrYQW1w/krHPJt278geWPUGvqDRIJbSW0S1k
+23SRmOWRyZBhTkqSeh68YoA+xPBmpi4t5rfuhZSexA71uPp6ahrMUMm8LG2c+o9K8k+Huos/iWFY
+GMttIm4+gPfivfLWPbfh8+tAHR2sMNvbBEUKo6cVBO8Y5LHHSmtOFBBOfrWDqV0PIfnbQBzHinxT
+p2kWDtdyTfdJ3BfSvnfXvi3oUa/ahcTrHK4QOy/catj4ntez6NPFDPHCJU+QnqOa+NvEOmz/ANlx
+6fJfzqEuPNmcKQHHYZoA+hh4v0HU5PObzpYnyBKHxkg9h+NdLBLpk1ruto4Wdjxg5Ixivm/wt4fh
+1CSFXkn8xQfLKyZGD65719AaJottp/lli0kkYG07uOev8qAOyhjQ2o/d8bTuB5oiiX+1o59mY/L5
+Zudp+lRK7iUgSgemBx+NWlDfaFLs+4rhNnAxQB8sftEazGusWcbySSQrEwKK2CWPRv6V+YfjySWG
+Yx+aQiR5njL/AHC3O0+ua+9v2ntRT+1biAxOzW6eYHj6luyfng1+YviJdV1DUHnurhpzfDz7nDgi
+NVzt/KgDkbaaDU/FEUlxujkW62xgNzIu3v7V99fsytZXPjVbR5Zlmh3Kh2fJJ7cfXFfnxClt/aUl
+ufOEKSrslXh2YnkfSv0a/ZC04z/Gv7OZAbGCAyMBgruYDCj6UAfrpoFr9m020jCgGJACo6HgZrjP
+iBOJoorZCWZ5MBRXo1viNlYHgj9MV5jqpS88bIOGQMePSgC54asDawwxSZJUDJr0xF2RAljkdvSu
+a0eDcWwOAcDPtXUffkfKHOKAKLYy2PWndQMdutSeWAOBn2NI20oduAe+KAISACQOAeprd0O4MN0s
+TO23HTNYgXMb5GafBKsF5G43HHWgDhf2hPCA134Q3V9bRhr2zAmhYL8wxycflXnPwV8TNqnguG2n
+fM0Y2sS3PBxjFfWt/bRav4JmhZVKvGVOR2Ir8+vDjS+Af2kNY8PzqyRSzGW2LHqCelAH6SEFgcV8
+EftreErq+/ZzvfEOnRsdU0G6h1O2KDDAxyKWwRz0zX33GteY/E3w9Br3w01XT54/NhngeNwehzxQ
+Bgfs/eOLXx3+zp4b161mE32uwRmcHkOFwwP418kftz6Y39k+GdYgVfOsNSDDPAIIzj6VS/Yl1u68
+H+OPiP8ABzUXKz6DqrT2Ubn/AJd5ckY9gRXoP7Z1otx8GrK52sdl6h+T64oA/NDwf4iiuvHOiWd/
+dCRv7UX552YeWGYnCc4HNforZGax1f8AtC1aE6XbttmgY7hIzcBwevU81+feg+EtQ8VfErw/aaHo
+V9rlzBdrNc2ttFvZQrY3SEcR/U1+5Or/AAg0O+8DxQ6bbJYzTQx70HbjJ6dT2oA8U+Ft/bx6rZwi
+Q+fkh0ycqSx9e3NfWNugDbuvH9a+WbH4ceKvC/xMfUGC3OjtKJC4OGjKjA+ua+mtMvI7iyjlJA7M
+O+aANOaMueCRWNeWEsqsvJBrpUZWRivrVaZwEOaAPLdY8F2F/EZL6JnEa7cZ9a+b/iH8PNEh065N
+v9raAYMgSZsDHP8A9avsye6jaFy2CFGCT2rwD4kavbwWrxR4lkZxny03GgD5d0fQpNOliubZJljL
+gMj/AMJPTp2xXrdm8n2eMNPbICcEEHJPtXCazr0UUgtonEUk0m3KgqQ3b6mtjRmuDJFJczuwiOJN
+2NzH2xQB6bbDYrb9wRVznGQa0lkDEfO3loQSwHQVjfaolsdyA4bAwWxj61C+oQxRsjMAmM/I2c+3
+1oA+Ff2ndatNK1y8mm8uNnm+Qq3zOW+X9OtfnLqek6tqF2iaetxcTXbm3SCIbyEH9T1r9M/jD8C9
+b+Lv7Ruj3iT21r4RjieTUYizedIwxsAA4r6p+FP7P3gvwZLBeR6RZCcbSH27juA9TQB+O+s/syeN
+/Bvwo0bxprVheQW9/M0sVqkDSuAo4LYGQD6V7r+xhL/Z/wAZLuzuo3gl80MPtEbRkg/w/N3r6w/b
+2+JfjD4d+FvhpB4U8RXHhu0u7qZL8wTKrOileNpHIr4F8EftvfFrTPGeq2Gm3ui+INOswsrw6vYx
+zNKc4wGxuUe+RQB+5Wr3X9naA15kLtTOPbFeXeGp11HX7i68wMHJC5PSvMvDf7Q/hb4n/seat42u
+3svBN3oUiw+I7S9uh5Npv4WZWPWNjx04OR2qT4X+KfC3iK2kXwr4q0bXZly5SyvVkYepCg5xQB9X
+aVGiwFxgnNbBAy3OOK5bSzNb6fHmQSnp93pWtDdgHZN8o9fU0ASgHBNRhFEJZvu9zVtWUythuoqF
+bm0kumtkuLSSZOWjDgt+QoAqsdm3ByT37UhVgDtKlvSrLR7pNzfIPSm8hj/Cv8NAHVaDdebZ+RJ0
+Awa+Nf2oNBfw/r+h+OLJTHIk3lzY/iBHQ19WaVcNBqAz909Ky/jJ4Uh8X/AnXNO2Bp2tW8okdHA4
+/WgD1xO9ZfiKXTbPwxdXWq39pplise6a4up0jjjHqzNivz4/bP8A26R+z9q9r8PPhxpVh4o+KN2m
+6f7XL/o2lK33GlA5LEdE7V+Efxk/aa+LnxU8Xxad8QPFmr+OS9xldONx5GnxyMeEEKYVgP8AbzQB
++sPxP+NPws+GX7dmifEbwb4x0Pxg01tJZa5pXh66S6nlXIKE7SVHPrxXG/Ev9rHxf8fvGfh/4R+A
+PB9no2oa3qMcFp9snF3dn5gXkZEysYQfNz6V+a2lL/whngWSbybG31K6XNy1rGIyncJkdcV9f/8A
+BMeyh8W/8FPPEHiS9jDtoHg+4uLIHpFJLJHDn67WNAH6K/E+20j9nv8AZLuvD/hy6FrfR2IfWdeE
+Q8+6cj523dgWzxX6UeFbpdQ+Ffhq/SQyLcaVbyBv7waNTn61+O3/AAUhu7iw/ZambzWhtLm8SC5I
+HUHmv1i+D0zXX7J3w1ncfM/hmyPPb9yv/wBagDq763WSJwy7lPauFlhk02+M0Ct5ROWA616XMmSR
+7Vz9xaB1IYAj36UAUbTVIZYMq6pxnHenXeoRrACDnIznOK4fWbC9sbuSa0L4IyQvSvNtZ8X3sFg6
+ywtvXjaxwGoA7TXPFFrb3HlzMQRGSFEn3iOcV4f441q3vLCK4sZYhIcb1Zs5JHSvO/EfiW6k1Z7s
+28ylFyux84Pt+Ga87k8S3F5fBLuXZZQtuQLF3PYmgDtY4yZDd3M1u7AkeQ65Ue496ty6sselxiAM
+v7z95KVwVXuBXDNr+nC1inDl3CEhc8nnjisr+2tR1TUUt7YM8W87jjqcDigD1r/hIrh4ooUAK9v7
+23sasRanPPNJHCTPgde34VwukaJqsl0skjh18vGxh69q9k0DQswmNrIKFQYB9fWgDpfDGlCGJp5s
+jIBI7Zr1iyEvmKCsSQBflb3NcvZW7R2cMTDyyi9u/tXV2bEWwfGMHpQB+YX/AAU/8H3Wt/DD4W6x
+BftbLb3N1azhT8hUhXBIr8gvh/oaWPjnVz9oaYG3QOSdoZs9a/eP9v8A0aPxD+wlqSrcfZrvTrtL
+yJgOW4KsoP0Nfhh8JUc3OoXs9uDc2swt5A5yGwaAPur4C29pqg8d+CNSMR0fxN4M1Gx1FZB5kJxA
+0sRbjgq6Aj61+Tnw68f+Lvhr8SdO8ReEtavdK1eykBBgnKI+1uUYdwcV+lFj4kg8EfCrxbqYlRLm
+fTbiC3SEYyXUgkn6HH4V+UEEYS4ZZGIDMSxznJzQB/SP4A/4KE/DPVfgR4d1TxXpus2viGa12X6W
+UO6MzqBu2+2c0/X/APgot8J4tah0vwz4Z8T+JNVliB+zIqIVOcbee/rX4h/BrS7nxrp+raDY6tY2
+eqWIF9apeviGTJ2spPbjmsvW/FOn+Cp9T0nwrfLqHiu53RaprafdhGSPKtvT0Ld6AP04/aQ/4KMa
+xYeGo/Bfw605dD8RTRg6tqYdZVsgRzEuOsg6H0Ir44+HP7WHxH8KfEi18SjUrjU9QglEs26VsXce
+fmDZPWviv7TKnmlndwc+ZHIc5J/iPqau6XevbzQE/vEWTK/U9aAP67vAvj218f8Awa8K+NdMGLHW
+bBLtR/cLDJU16AsjeWn8dfIv7G82m3n/AATX+GT6Xem/ijs5EnJ/5YyCRtyfhX1lZfPbbd/3aAJ0
+fbcBuhU7jXokSpfeG5YmAcMnOfpXnz4EeW78HFdl4buM2rwvywPP07UAfyzfHrXr3xH+3d4v8Ta2
+7SXc3im7juNy8hElaKNefRVFfLuiaeH/AGsxBPHGoiunlRW4XoSDX1l8fLS21r45+NtZ0dQ+n3mv
+3dxZSKeiNMzKSRXzBr1tdad4z0jxorK8BYQagy9YHPGT7GgD1T4jqf8AhDHuol3yfOrBex7mvuH/
+AIJBukn7QPxjuCokaDQrWFJG7K07HH0+Svh/XbpdY8IRfZwPKIw0q/MrDA55r6n/AOCX3i618Dft
+++L/AAdqUyQDxXonlWRyADNC29V57ld2KAP0u/by+HV18QP2CfG1jpVnLc6xYW6ahZFOSWiYMRjv
+xmv0D+DOsafr37JPw31fS2RrG48OWnlbTwAIlUj8wa8x120ttY8P3CXiK1u9u0c6OMkgggivIf2L
+vEl54RtfF/7PXieaUan4WvnuPD0sp+W70yZi8ZUnrsyVIHSgD7ycbhis6eMZJrUPSs646Hgn6UAY
+09usyuGAYY6E15H4v8KQX9nMPKBOMjBr2GRhlsgjisHVIhJZvgA/LQB8W614LaO9mjKOseNoIPIA
+7+/WuCuPAdw2IbeZpYi2XA44/wAa+pvENnt1AOMAbeSa4janm/u0VcNyaAPG4fhpDHcCT7wABWRm
+yy+oNdXBodjbTwrHbRKoH3k+UMR613JVSXLjCE4wPWgWyO4XylZO/qtAFG202FEaOVQUIGwBs4zX
+Z6RAIoEXDu2OAwxVCGFjGoUg7Tg/LW/apsjVmZcZ5KtmgDai+VSAqJKRkMTwKd9ujtUd5SUXaOpI
+rFu9S+yxSMyL5XZ8818b/tCftFWPgTw/cQWd2kmqyLtgj80ZGcjO3+VAHgH7dnxmcs/gC0d5EaNZ
+LlY29WIVcevtXw/4Z0X+yvC6WYEUdzK/n3DgfxZztP4cVBql3qnifxbc+LfFDyXl9M7GCOT7yqef
+MI/QCpbjXodP0J3uZPKIjyVbG4fWgDlPit4vjtvAE9ujqpkiaCMFcDOMn/Cvie3VmkWOEMXkPyv2
+X3NezfEPXE127t7O2aN7VGLu7OBgkV5za28EaNEkn7z+GQj7w/pQBt2WqXOheGr7SdFjSG4vlVdS
+1DPzumeY0/uj1rn4U3bdxL5JABU/KetWktSS6EED72d2SfpS7UKOY3Ykp8qHhlIODmgCoqSbvMy7
+v/GMDFRu5S4RQNzHk7R0rdVYvMCLtQKP3jA5zWRfaVd395tsSXK9Qo60Afsr/wAEtfjZAw8T/AzX
+Ls/a55W1bQGcj958oE0S/QLux7V+y0DtFcbQm2P1Nfy8/sk+FviP4d/b1+Eviyx0PUXsLbxFDFez
+oQUjt5PkkDeg2sc59K/qLuUQyyrGxaMv8hHT86ANLKlBtxir+iXBg1rBcAP2FcebiaJtpJbFP0q8
+mk8VWW5WRGJGAOaAP//Z
+
+--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1
+Content-Transfer-Encoding: base64
+Content-Disposition: inline;
+ filename=avatar2.jpg
+Content-Type: image/jpg;
+ x-unix-mode=0700;
+ name="avatar2.jpg"
+Content-Id: <4594E827-6E69-4329-8691-6BC35E3E73A0>
+
+/9j/4AAQSkZJRgABAgAAZABkAAD/7AARRHVja3kAAQAEAAAAZAAA/+4ADkFkb2JlAGTAAAAAAf/b
+AIQAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgIC
+AwMDAwMDAwMDAwEBAQEBAQECAQECAgIBAgIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMD
+AwMDAwMDAwMDAwMDAwMDAwMD/8AAEQgAwwDDAwERAAIRAQMRAf/EAJ8AAQABBAMBAQEAAAAAAAAA
+AAAKBAUJCwYHCAMBAgEBAAAAAAAAAAAAAAAAAAAAABAAAAUDAgMDBgcIDQkDDQAAAQIEBQYAAwcR
+CCESCTEVCkFRIhMUFvBhcYGhJRfRMiMkNEQ1RZGxweFCUpIzZGV1JhhyslRVNkZWZhliooXSQ3SE
+tJWltbYnN0c4EQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCZBQKBQKBQKBQKBQKBQKBQ
+KBQKBQKBQKBQKBQKBQKBQKBQKBQKBQWx5emeOta98f3VuZGVrS3lrk7O61M3NrejTkNdvqlq5Xcs
+pkqezbKJjHOYpSgGojQYANyviVenfgaW3oTEHKb7gHhArXoXlxxc0JvdZsUodbY27cikSppSvZb6
+gOUl1AVTYEAE3rNNOYMEm4TxW26yTqpg2bdMRYvx3GHATJonJZOgdJXPmVPy8ntt21edrcRurrg+
+kBbqG/btiOmhu2g8CSnxE3VNmDYytxM6oowtaAL65dFoZF2pY8GAnKJnQxGw1u+U33wgBShrQdPM
+vW+6qLGtfVybeFkxZdflHtN609Xmt8RN1wAAoEY0DsgWImVPoH80mt27Yjx5deNB3rFvEh9WCOub
+WsX5yi8sQN9m2nUMsgxLjT2N0t2xKInXLGqMtjv7ScpdBuW1JDDqIjxoM1u1HxW7M+SWMRbd7g5F
+D2NcYqF9yhixwXuqdpumJpad1sJcvWrrqAL2nryJFNy6Qgie3bOIBbEJRe27ebtc3dxoZXtyzXCc
+otdq4FlZYZXC4kfG2+Jeb1DtGXiw3SJru6dgX0tvXThrQenKBQKBQKBQKBQKBQKBQKBQKBQYSeph
+1ytsXT0FTA0xbmac93UN28mxxE3JIRujl09u57JencgAb5Ga3cuFD8XtW7yoxe0pO2gg0b9+rZvP
+6i6qylyO5+7GLmFVfVt2OscJHZsiSQ94BtlUPd4VKpS9LCWB5fWKrpihx5Sl10oMSIqR1HUR1146
+9uvl19HtoK781Q/2n+5QUH5z8PPQV3/tnw/71BXJvxT6fi8/Z2UHJ4tFJZkSTs8Lg0ed5XK5AttN
+zKwMSG+4ujktvmALSZGkTW7l67cMPmCg9o5B2cdQzY9djuRJlhzPWCbqv1LixS1K1SNi5DEL623c
+7ybAKWwchR15bhg7eIUEifpY+JVkTE+x/BHUDXGd4/fBIzR/PlhKPfTSpADWbBMhI7YFBajOYCBc
+Xk1ukH0jEENaCbFFpVG5vHmiWRF7bZHG35Cncmd6aFVpa3uCFVaLesKE6i0YxTFPbOHAdDFHgIAI
+CFBf6BQKBQKBQKBQKBQKBQKCMv11OtNY2gs7ptZ24OFh23JylmEsjfkVwiqzixmcieqLqWyJ+aVr
+rVz8DZH0rIDzCADpoGPboreHMkW9Qj5u96liTIzRAZUrtOcAx+rdlDJN8sqV4FWLZpLHS7656aIo
+BTltprRQsqlxhMYp7dq2UboTacK9NvY9t+xPewpjHbfjVqx8sSqEjo2OjEnka55tqiGIoO7vT8Dg
+7OF64U2nMe8IgHZppQQQfEJ+H6S7L7DtvK2nWHl329v8l5cjY6BECxVhlwergijcm5SjtgZRAlq8
+RslNdIUyC7ct2zmOU5TAESX8z+HnoKJL2j8v7g0H5w/JPj+P4a0BUq7ePw+nz0E9jwj/AE2Udxsm
+fUVyrFrV4664rx7t6tvCIpwt2kl4PfOeN1tQQQAwqLZG5IoL2CRTyj5aCdEub0DolvIXNCjcUSm1
+csKEa5NZVpb9m6USXbN5OoJctXbVwg6GKYBAQ4DQRr+rr4dDbZvVgshyTtfhUK2/bqmlOpdWVfGW
+61GoBkdXbIa8ZjmDA02SNTcvcTl5bTmmTkulum1vhcLxKEYDpCdRrOnTO3XOWwPeXdeI5i1NNluP
+npvl9+6f7IJYF8bCBzQq7pzFtRZUpOT1glEbHs90t4no+kAT+kaxI4pEy9AqTrUK2xaVI1iS9bUJ
+lSa+QtyyoT37RjW71m7bMBimKIgIDqFBU0CgUCgUCgUCgUCgUGKzq79Q9i6eO1aQTpCpRKswTYiu
+KYejd2+T2pZIr6fS++GSAB791BHbN4t+5oXQTCUuvGgjLeHG6cl/qPbu8m75N2CZVkPHWGpOkfzp
+5OQ65Bk/OUgv3XZsSuYKRPacGKHI7IrlScdSGunR2TlNaOctBsj7Fiyms2k6azaTp7FolmxYsWyW
+rNmzaKBLVq1atgUlu1bIUAKUAAAANAoPrQWeQR5hljI6xmUMzXIo6+oFLW9MT2gTObQ7Nqy0awrQ
+OLestXkqxIpsnEp7dwpimAdBCghudSfwl2NcuSCUZd2Fzdpw9InY6h6XYKmdpXexyudLXOptJYZI
+khVLjEbKm9qS2lUWVKW2JgALlq2GgBHxhPhiOpE/7as/Zwk0eYsbTnFze9OuOtvjqrTOmSM0I4+7
+LrMqWoAQqLzbErZGtEYGg1s4g/Dy+QxRMGFHG+2LPGYXlHHscYPyVM3pfbC8ibmaGP70pv2DGKUL
+5CIUN7WwJhD0/vfjoJaPTU8JZknIl6N5R6g0hT46xw4NKd3SYThjlfPlRdfUjbuWEcwcrjeLNEk/
+sxhNct2rqtYU/oHt2h1Ggnu4exFjvAmL4LhrE0ZQQ7HOOI22xSIxxtJyJm1na7BbFggnHW4pVXhA
+bl+9cE12/eOa4cROYREOyaBQa+XxgGxhDCMr4i34wtuOmTZbtWsW5XOmKIWPfOKNhbkQfLnIGltS
+7RhMZKc3DmFvKP3wjqF48Md1K5PMweNjebphfdL7SzLJHgd1kLlcV3lSBtU2SOsGa3NUIivsJm+9
+7Witc4mLas3ilDQtBMqoFAoFAoFAoFAoFB8VCiwksXlSq/ZTJk9s96+oUXCWbFizbKJ7l29duGLb
+t27ZQETGMIAABxoNX/1p98jjvl3mzt7aXE9/E2LVazHmLURD2DprjWzq7tpzkIDZExDqH9eUTibm
+H8Fbth5KDYR+HYwy14e6SO1m4lb7KN6ykySDLUpv27RCXXF1l0lde71ag5fSunLGkKG2UREdCkAA
+4aUGbugCADwENQ8w8aBQfO7cC1auXRLcOFsh7gktWzXbpgIUTCW3bIBj3DmANAKACIjwCgpCAS/e
+IIDp6kx7ptPKY2gAA/JQfidpa0hiHStremNaKYtsydGnsmtlOYTnKQbdsokKc5hEQDtEdaC4UCgU
+Cgj5+J3xEqyp0kM0OSBDaXK8US7HGTDgYgGvWG5FI7MZdFCc2giQydJJxunEP/NkNrQau7EGQ5Ji
+nJkVn8QdlrFJog+tr+yu7ddMnWIVzcqtqLF+xdIICUwDb0EOwQHQeA0G2w2gbi4dus254tzhCXhK
+8t8vjLed1OmuWznb5OjTWk0jaVZLf8wqQuhLgCQQAeUSm00EKD0rQKBQKBQKBQKBQYlut1ubXbWO
+nTnCZMTiLZLJkhR4tiiq2cbaiw6zk9xsNfTXwOT2W/aRet5Lo6gS4YvDUQoNW46PrQivpk61ztGW
+rroXVgCcTnIa+fUx1BilMW2c4mEePy0G622Iw+O4/wBku0OFRK0isxuM7aMHtLOVuuWryE6NNjaN
+lKoTX7BSWb9tUYRu+sKABcE/MAcaD1dQKBQBDUBAfLw830hxoLQf1SC6UeNmxasHualtmPZspLHK
+a9bEpREbYgI6lEA8/wAlBdSmKcpTFEDFMAGKIdggYAEB+cBoP4vXSWLZrlwwFKXzjpqI9gB8YjQW
+2y4GG4BLhP54/wCD5jgQxfSIF21y6CTnT+tKUQ5uY5y3ADiUAMF3oFB4Z6m8KbMh9O/exEni0N9v
+cdsmY1N4gEC4cDssId3xNdtkEBAbthU2kOT/ALRQoNL4nD2NYIeYdO3X0QEQ7aDYIeFmyndlW0TM
++MxMa8kxpldsdm9SJtS+zz6NlOdKUv8AB9Qoi57g+f11BKBoFAoFAoFAoFAoIpfi152WMbI8KR0i
+w1hVKc33r9pKUwgC20xxFyC6BygIc9uxdd7RhDjoIgPkoNcua4e5cG4cTHOY/MYREREwiP7NBvBe
+l0uXOXTd2IrHJEsbV9zaXgQipA4Wrllakup8ax1ONhTZugW5bu2wtAAgIBpQe76AIgACIjoAcREe
+AAAdoiNB8Qv2xONvX0y66lABHQOYQKOoAIemAagHaNB9dQ05tQ5dNddQ008+vZpQWNUrTXb3qzcp
+xMnulKU9/wBnT3Ut/kC8e5d04h+D4AHk184UHztPyG1rbOJilAR5TAUTfMIAGutBa3dxBbb5bHMU
+SCPIUeHMP8YQ837lB/aVVoOqvUBDyhpw8w0HJUd8bpClObU/IFwoiUSH9UcTerLeIIiJVFsoAFzy
+c3Zp2AFbQeVN9hil2R7wzHHlKG13PomNygflKGKpXqPIPA+geTy0GlBWffD8o/51BNM8JS9ONtZv
+Djw3rFtoVo8av1lvIP4a2tTukyRmv3g17RtLhL2UE0WgUCgUCgUCgUCgh/8AjCGlMp2p7Wng6stt
+W2ZolSROiHTnVWnOKoTqb5fLokFvIA+T8JQQcNnWIVGfd2G2/CiYnObKObcaQq9+CG+W2ifpc1IX
+G+eyACNy2nQXbhzBpxKUaDelxliQReOMEaa09lI2x9mbGVAmT2rdiwnRtiOyjT2bNm0Utu1bt2rI
+ABSgAAHZQVaxWFg4FtiQ14SGONsboENyWiX7xdAMUwaXj2BII9oBqIdlBVCYR7f2KC3UFOqt3QKJ
+AuH5f4uo6fHwAaC2+y+16ir7POH08KCt7rS+cP2A+7QPZE3mH6fuUH77P/Qfh/JoKyg/U14SXA5j
+Dym4DqIjp5vpoOJ5ax/H8uYryTiiUnvkjWUIHLsdSAyQRBWVmmzEvi7mKbTiCgErocCeY+lBpZd2
+m3SZbUdy2cds02Xd9SHBeUJjClr4Hpd7g0iItEv11/3gYKCVR4R9vtlf95DlYDntCz4vT+u/j21S
+6Sr0oecdCGPQTXaBQKBQKBQKBQKCLb4rKIR6QbNMHurtaLfcmrOV9sarImEDntP8JfAXmt6CHpWr
+jYnNrx0oInPRCxoz3erHsAI1K7qhzJuIgkgMRSNs9oGZg1dH0pScgBqJPRAR1EAoNxGN843Qt8AD
+yiAcR+5QR1Ooz4jzZhsNnMrwcwMUv3K5+h7TpJofj9zZWSC49lP8GLZJys73rgRyQX7YahbbrT3d
+IA6HKU4CABHpk3jHd6QKwVx3aztNjbWIAX2GSzLKTtr8Yuhfcoga/EAUFc2+Mf3frA0DaXtNeOPa
+15IyeI/94pgoMqnTj8UhjfdHlCC4J3UYSLt0nWTJaywqE5CiEyPK8SvElebxGyMM0q942Zhfcf3X
+125gtc4GDm5dRAOYaCWpQUiVL8PN+/QQ3OoH1jOo9uN3DZS2X9IzA2ZFkh27zyXQTN+VIdjxld3P
+vVnezNjQDPKZ3rAYBH9CmMIupROIecAAKDBpmRq8QgjUShFuN3vTrESOLNQOMp+0bffhSJtLQDqB
+XMAeAxfNRLHwAA/RDpp+1QYiWzN+6FVOu8nffbJ2SQoBB0993Leq8urZ3v8A1QeBzKZn0+IWvT4q
+DKTsK8R1vk2kZPi7TmfODvu/2/23Vojk0hE1d7kqyCMYKYmrphSXL7sfkJn7Ti1kdtQMJQAQ0HUA
+m+QPxCfSAnDWmWWN5kVjKhU294Wm/IePcowJ3brQhprdK9QaxbtmAB4gBjfL5KCAP4gPJ22rNfU5
+y7mnajlOMZpxrleB4hkT7MIZcF5izbPysbxE3NquDykL6wxWJrMbQA9Iwhx0AaDNN4S1M8e3b0FC
+VOnCKWLWJk15UY/40L5dPMBR2LVvTUycECS+Y4j96blAO0aCZ5QKBQKBQKBQKBQYM/EO4xJPem5P
+pARImvrcZS2GS21fvCBbyZKpd7UeUilEe25cUO6fmAOIlKPmoINnTK3UxjZJvuwFuum2OpTkyE4Z
+c5iL3F4OBe9yu+QoS9RaIGZuYpyiIP8A5wEKCQ7uS8X5uElTXKGDbltAjWGUrzGnlCyTbNUvenOU
+NBjFO1HdmdmjAM7H3+xHuCYCmOblEA0HQKDExs26DnUV3vxlFml3RxnB2Pp45XZmhylufeXwMhT8
+XYAuuuQmqJtZTzx8HW+U/enATAIiHlEAzhYu8LvDMZKyyM3UIycabg26vr1C9t+MAaGrQdRKP2mm
+KYAHTyCA0F5y34bN/kCZY7RLf02SjITO26RVFnzaviyVxN0DlE3MaXQUzyLAAiGnDUdRDyaiAYM8
+oYFyj0otyeL5Vvw6dm3XPuHXuSs7jB5diPvnHsAljtFHwrmBsdZExmDQVhyOQR1BolRTdnENNaDK
+Blfxg+7G/OnxZhHadt4iGNwM19yseXnibSzIAl9EXQXV3gL2xx0TDqIaAQvKABxHUdA63HxW/Unl
+qGQSpJi3Z5F4/ieLhkWVxVwieVQNkZnGaw6BGibPec34t23dD31B0AS+lp5Q5QoOhJh1A+rX1386
+W9rGHHuNbfIRJYy7yLJGOsKvr3j7EzTEBFgJKcsbh8i3CGkE+j4mAQ7qETcwiHDt5gza4A6B3Tn2
+0MXvbuDWOG7KQRdqNJJRlLca7PjRiaHtEVZjGdgHHZjCxmjhdeAu4mNoABrwoPW2y/ed0ud0ORXv
+CWzxZhsJXAGszj7jt2BwxQ1S2Ksxu6TyvEoOeoz2PFHiJR0EAEB7BARD31kba/t9yvGFsUyvh3Dm
+QIo8/Vy1km2N2R2Hhpr8dBCt6znSwwl0+ZPineNgfHLXNNs8kyc1QbKe1rI7w+OsAiUodWIHNnJE
+5e1iL8aNT0veoB/qF8HTsAAAI++e5/D8jZJnT1ivCLbtZxVkV2iLrjzCceenx4izXFoqyvbaY55i
+7CMhyCY0gHi7jxHWgl5eEmWorsa3jWb7mnB7vLsNqSs6e4AEO3Eb5oN1yJa11OFpUrJaEwcC84AP
+bQTIKBQKBQKBQKBQKCPZ4jPcPCsebPl2AHO69qJtmpU33GFmYjmtOQoYxIGle4qVRSiIHTequksc
+nZ68dfIFBBh2vwfI0/3Asm3LHolheeMzzvEeN8WSuTOs0x/KsRZta8nsznE3geYBf8fyMH7s8oDx
+DQaDPb0pYvuT6j3VaSX+pI+SjLci6ceL3ZtW4yy0I92s+WMTTU8DaIhLCRnWNv0jYJ4dzdXh2MU/
+fpifWg6agITplStUrVe1q13tqseK5d8PIOlBgx8QPA92k32Crmnaf7+LUiKeM7ln6LYl78+0KW4o
+Bk/VHuu9C/yCOd//AKXaKDzx4dSD7x4ZtVyGl3HtmS4phN5nTQ2bWoTlYH4ssi7Ya4c+RXpqZnYw
+zqOY7K/HAjQDi5kIBSunDXURDNNvK2kId4exbcxiFZF41JLE2xbMEETROds4OLXm6KAcuO8gM98x
+PUsV2APxRN3mBuflDXTloI6fhK4Dt9zHC90kTybtWw/KMxYancPkqLN8ziLLPpd3TkFmMIY7A85Z
+3oWD3AFhAPqsSB6Qh5tQkwdUeMQzGfTr3gzKL7b8Y5YdYdg+YSRBjBbDYKaLu7mRsOhNKXW3dh9w
+twIFZdjyU33hh5DCAlHQShHa8LDi9hadjOfshNK7+8OXNxgRt9XdndETx8ycGj/43QSBdxuCEm4P
+A+Y9uKxc5oUm4HF0wxMK9t7Gd2lgatP66ZuHf7L81BHe6ZHh+M17EN48WzfuXzHA3oMSRl7DFcVx
+uZ5M7OztKmUYAMseBcw/2eIwvLqAtPn8gaUEqBL3okV+1pVzWALWvu1chcmgHZp7p83yUGHjr5R9
+ieOlFveUqW9sSJkbZjaaMKAoCLU1O8Vm7KDQDOUA9IwmenTh5aCHV1KWmxHdg/QbhC1LzP6TZ5uO
+kysugfWTRkLcFbcojb119LU5TG4/xvMFBx7p17jcydOp4xbvMxzOMQzmBZOl0txHljDzbKl16bRd
+AjXWHYbmTIkoaSPbNdlFhLaVMbmhJdsGOTQ5DFEQoNlHivKLbleIMM2jxrfccla2x0QqSmE4GbHp
+suX+cdQDQwAfT5qDtSgUCgUCgUCgUETPxDcXfse7jNj+5ppXNa3uWe43bEKGSfWzS0SyJ5RhcoaO
+9v8AlygjZ573hbp5tv1w7lzdchbmXeDti3EQ6P5Sm7bEGSAyt5+z3JzG5RVryC0NJWMDSKBmJ3WG
+gCPcGgajprQSfMJRMuyfxRW7HEKoXEsP6guK5dluDLiCUbLs65B0ycJ7RiiJDWTZbLKWwpg4CXQa
+CT5QUaVVp5/xIfN+/wAaAr/G/wAr/HVfk4UFvkGdf8PuG8pziQssZVQnHkXyTlqUrnF4eOEUijG9
+Sd2NoZjHUQBlHTycezsoMLPhNNvnuVsGnO5h2bzJJDu/zJLZShAAKBWzH+OxNBIeQphEdBLctuY6
+/J5xoJQMij7FL2B7ikhRNrvHpK3Okek7IvKHdjq1OjQLW7tPEDehctjoYO3lEeIDxoIa3RwiA7AN
+5HUU6W+QFzgilLPlQu4/AhrjV6tpyHiY7G9ldOQvKbnOeDPZXICiJf0F26gBRCSeKoEqpC7Jfypl
+dGdyQh5wag00HXgOtBV5NnJslCyilhp2l1ZnQoA8md7YgRpddSnEvJbIb0x4jrrx7NOyg4y1pXT8
+7XfDs17aDAx4g7Ib/LdueEenzicBes8dQDO0Qx4yxZsIU7qGPYm+A4yt3MU4lAxDSQxSj26APYPZ
+QYZPFRY5YsK7gOnxgloAyOI4J6fzXG0S5uDTkaGWde6PfHZwKJmQP2aBlLpCRjaJ4f1futywxlR7
+t8zZOwRkhWdxKbvfE+KHV91iWJWsOTlKHu+Yrq66mExtRDQALxCWZ0q/alfT72rq1n5WtxhD/wD5
+Hp5aDIZQKBQKBQKBQKCL14ophVq9r+K5Ck/U07efYfi/uS9a0EZPrfZFxTlffxK8m4VXtL28TTBW
+3SS5hfGsNGj/ABAGxkzDKXaI+XmJa0B48mtBN73lbC5b1I9suwrfFgmWtUY3z7ZIdCMhY/kYWrdt
+jmjw3JmxzmmJ5RbIIFtxpLPWZRbACh9+U/EObloPXm1LdXjvdjDn52iqxpZMrwt1LG8+YQcTC15D
+xLlYSlGXNRmgSF79jwHEQaHYBHgADrx0APTir8U8vk83k+bWg/fzVc6+w/VKP8vfHL6paWj/AMX+
+UaCNv1I92D71DsksXRz6c8rLkCZZalLa2b3dw8F0v4v287fGF4tnmMQGSWjA1Pr8b1gFdTaemIGa
+LfMZzMABLBwfhuD7ccJYtwTjdEDPAMSwaKY4iiINeDNFGfuu2A83pCY5SgJhH+FrpQdsJvJ8P41B
+gz6wnTbypukJivdxsukjfj/f/tMdTyHELm6EZgi2Vo0S6VydcR5FtvJu47idQBvQM4hyl5R1HlNq
+UPCeFOuBt5azDjPf/GZ506dzzO5dwZAx9mqGzUMUXJMICYrziXIIGfikjwELqIOhjAHaBjF0MIZF
+Eu/rZGri77LEu9LaWtjzK1+8b4ubcwMjt3Q0/tUHhHcZ1uNpcIi/dO3CVf4tdx8zax+x3B+JWd6l
+nvdLHbQIi0O/uv31Qc66WHS63GuO4OTdVPqj3W163sTNqux/C+IG7u0IBtYx4RoBsZSILbWpOity
+K3HjgQtv9TEMfm5nQR5Qjz+LmB0V7+MXNuhraJm2kwFGhMJSgCh/c5llq6a3buCHMPs9sSmOUo6f
+hCCID6IgHpjrR9SC5vF6cnSowLAUDiimu8xtxvnrIMXazEud2M+PALBWuLFOXQOSRZfHlAB/ia8d
+QoJSu0zF/wBiO2nCGJ/y33LgTPG/bv7JZBoPR9AoFAoFAoFAoMOvXMw39rGwXIyRIhBarhbozzVA
+P9k/pfj8TBQa62eJVUTS+1yFja1qTJzV/cicOXfbT3O7RN7/AL3C0eaR6/pag2anh3Mypsx9LXEF
+62495OGN5ZkrH0qvCcTnM7BML2QiWzBza2/ZYvPENkA/iWwHy0HPN43R7xtuDzIi3XbfcqSvZjvX
+ZGwW8mfMTs4OrZLWs+pjNWWcdg+MdvIFsSgUoauZD6E1ETcKDyFHtiXXwg6ebMEX6i+05+b5MYDM
+02mOCZrbl0UtgICcrLFzEfrdoxihwEXMwAPHQQDSg4M2+H73R7glKr/qNdXTc1uBijwcRf8AF2F2
+pmwjFXkumoC7uTWF23eAohwL3SXXyjQZttm2w/arsDxiGLNrmH43jOK8omkrwQDuUolbm29jrMZW
+5mF7kdz0dNTjoAhwAvHUPWXtSpWq/onZ5h+AUFd+SUFD7Uk9q9l0/wDUfJ+z2UHBMo4bxLm2PjE8
+xYrgmWY7x+pMiw5klzUHER5iklBDgIjrx4caDwE49EjpMOrkDou2CbdRXAGpu7Yd3UA8R491tTtb
+IbT4gD5KD1pgfZ/tX2rpXNLt027YcweR8H2F5W4pxvH4k6ufLobldXdqt235+AptBATGMOvHXhQe
+kknsn5pr8+v33x6+Wg14HiKMUZW3j9YiFbb9vURXZAzI9xHHUNjsUI+WmollRaht2bLpA9PGhAj0
+dZmY6ozsJjcvIQBAQ5h1DovpMbQVmY+pt7v+/Jct4n6frX9nEWnBQegiTs7Y+EYv3vEQcQAAjcgf
+w710oJ/qVKlSJUKRJr+JfRQf1QKBQKBQKBQKDic8i7XNobKom7IfbWmTtbxG1yH+1vmANaCGht+w
+Oyv8c6pHRrzBFYo4T52juYNyWyZ2dmcXV0bc/wAUgx3N3LjsSctz3kf2TldR5TlHl10HXSg9U+Df
+3NN7rBd3W0tZfIkfrMoie6WItvrhMdQzZBisfx3kO1bsj/NhFFsRiwnEOBu+Sa6CHEJwPs4fF9H/
+AJNBQ+zAl0EBAddfPpQflB1JnCKTycYkyhEcUTdBjPJcjgkvjeOcid0keCwGfO7DcTxKXXmo2oHL
+H3w/rBLrzCIB2jwoMT3To2z9YzblPTx3eDvOwTuxwCtI5OIuLnH5wXNrQ6uQga2ETl4gQl6NW9Ox
+zMJigI8ogI60GUjcUz5vkGEpy1bcJ1BcfZsXNJhx3NskRI8uikTdzCTleHWKtgiZ8KQAN6Iej6VB
+h56c2xfqt7eNwE2yrvi6kJd0uMZLFXhrQ4SaQmYM5JU5PRQtSlqLKGhlDH5I6Ajyg0l4ajxDhQZ4
+Ff5KOn5X5PP2cPh5qDivtSpIPzfP+6HkoKH2lV/pv+dQcqa1X417Jp9zt+Sg12uaN+cIxR1yOpzv
+QTPI2JRt4wtm3Fe3VtsN9u9ZmO5M7dCttcJLaMUD2jJ2YJY4vbsbTmuWkYDqACUaDM54c/bSrw5s
+tXZMkKD+9mdZR7ye3f1T+qOFBIWoFAoFAoFAoFAoFBg86r3Txyfm5+xzuw2nvjnC90uF3RncmNdG
+/ql1d+6v0R3R3pr/AHjoIJER3X7sOkf1DU+VouwXMZZvhzurWziAPaNtSwuYQ7IjgeVSeASdgZrV
+n2mPTNoUtdwTW1FtQiUprSlOa2otWrpA2nPTb6lO3/qc7a2XPOD3MELml9mZMq4rdVqe/MsRTsUv
+r1cZkVqyFoVbcrAh77S627RErqjD1hAt3SKE9gMgntQ+Yfo+5QWSg/j2oPMH0/coHeof6CP0UFEq
+fvZPzHTt+L46Cu70/Ffa/L5+Oumnm81BQqn5KrS+bT4aUHFFXYHyfuhQf1/ROPtn7vw+agwfdb3r
+UYw6V2KbUWgp2LIW9DIzABsZYzu3QWIYPZCwRMiyjkpKmuFvJo+hV3Td3N5jW770rtchNLBL922E
+SDoi9MJJ1EJBLs95hlrveh8QyevvPOtklh2yu7Op1zyZx7xt3rpbR1p043Dl5h0OYeI0GwTjEXYY
+QwsUTiSHuWPMrV3axoW39UNLT+3QX6gUCgUCgUCgUCgUCgggeLB2cEQS3H27KNtyowuyW7FJOqTN
+g2U4g3ktODStv3S8SlOkV3LIa66GTaeegjZ7BOoTuk6duXEud9rs7CJShU1Gjstj7y323+CZBjJr
+9tRdjs4i6k9pO7toqbRbtm7bOnXIrwesSqLFz06DYRdJvxQGIN/uScdbX874cfcH7msgX77PFXKF
+HUTPC08fG9pXvCuykUKDFmOPVitG3XTWEi+26oyFtiB3TnEhBCUd7UHmD6fuUFf7N8Xw/lUD2f8A
+oPw/k0D2b4vh/KoOPt8qhrs5rmhqlMZeXVIURXMra8szk6l0Lz6GaymA9vgHboFB9FTCl0D5P3v2
+qDrOYzGJ48iclnk8kjHDoTDWJ1k8tlkmc0bLHo1HGNFecXh9e3dwvWELY1NiBPcvX7945Ldq2QTG
+EACgiM7/ALxbO3fF9p3g2wyBr9w88spVaFNmGdJHaD4WYHG4nEidxZ48vSoshZHM3rAEt1PdsR1F
+c0LcsLVFsdBCBBmvNWV9y2WpvmjNczfMk5ZyW9LX2RyZ7VjddXd9XiFhtto7VoLdhGgbFF5PbSIk
+xLaZKjsBZtWyWyAUA2cnQy2vG2sdP/E7WuQmRSWesFibP6A46mC48lAxwMPZqAjpQZlaBQKBQKBQ
+KBQKBQKBQYsespt8btxHT6zzHFDf3gvYYkeYM63s1c4sUXIfP56DVAL2w8eenRgumC4REoOZFdEB
+AL6K5qe0YupQEwkKPKbT+EAhQZeOgffBN1hNhtwbg2+bMd+wBg14+1QqWJQt8A1/Cje5R8mg8aDb
+v8PyT4/j+GtBi46n217qN7oo1jCP7Cd4LbtLTNTs7DmVwFtfWyUytvLbIDQdolUbtnfLWlspw5CC
+TUTAbmERMUQwoj4YveRkFT78Zu6w2eXrLIOQuSB6a2iauzU1iGoho7yqbA/aj2cRAKDlBfDK7qZw
+l93s89YjcZkHH3YvYu6Xp2M6tenMBTe802ewA4APkoL6/eEc2wtLWiV4T3abn8Y5MZTd4sU4cRhL
+w1d6ejoJmhsZWLQPOACPD46DN507tr+47Zlt1JiHcJuklO7ycDOHmQo8nzkXj1jRFHUS9yxExpFe
+f3seQebXiOgmHQQ8ocU6tS49vpj7/hUGKU5toWf7IGNygGqjGcis6cTaamC7oHl1Gg04lB696emH
+Q3Bb4du+LbgWRbXeftl13u3ya2EaBuLec1Ks5/4iRMlPdMGnH1Q8aDb6MLWljzCxx9o/EkbM1s7a
+h1/qny+Sgv8AQW/89+HmoLhQKBQKBQKBQKC30FwoOjd0DD7w7c84NOv5bi/JH0sj15vjoNU5k3aJ
+Pp1tryZvAgdkr/HdvOV4nizNLClIAusEj8/tOqXGE+dA1KYI5K5AwrWk94Q5bS6xbAeBxEA7j6EC
+4P8Aq6bBFVsto2ueWe2e2oAwlKFxme7N4pigICFwhDmEvEQ5g7PJQbe50Sqkirj9zjx/yaCva3T8
+04/D4fJQeC99rV1K3VPClHT2k+CWVQhK7BkSLZt74aXB3KHMVoc2SVkhs0EvqgLx1KGuocR0HQMS
+/wBiHidcnKwapDuZ2lYXj638tXRx3+tw1D9Ti17fOHCgzzbOcXZ3wlt9hWPtx+cTbjctMguwSnKg
+s/uuDwVze/WDpbA4gAMRTCUR0Awh8gUHc6qUflyTT5fpHtGgxndXQ43elz1AFZC857e1DMxFZQNy
+cpr8Mc05T66Dwtjd10/haacNaDTzakJbu3ro8tmwUDHADlJcumMPLZT2NSXBMoU3TAUgAQ/LxOYO
+QpjAGbjoqYpkkT6jG0lmnDK5w97m7uyTi2nemS9bc7sOl8WI5wJ8aS3RG4ZskLW5kV2DDxPbtgOn
+Gg2i1AoLH+c/Dz0F8oFAoFAoFAoFAoFB1vlpL7Xi/IyVV+SLYHMGzs/1syfPQRaPDWQ6HzfMvVo2
+25AijdNcZTeNw5slcKkIFFolTQWZ5RjfdVzm1AbZ2F3AnDjqYNNO0AxIbn+n9IugH1WtsW5NZFJr
+kXZW3bho3PMXy5uS2zya3HbLtzu+G38pQ5XDL8KZLt0ycDjyyZNbtqbYAJ74gGyjwxmfGO43FOP8
+u4ombZPMbZIjDTIobNmIxitjw2udpPoa2Y1rmZnzQ4lM2jxKcvKIagIFDsNKl7pVf+m9v7vH56Dk
+FAoOByhzVpPxRIuHT975NKCxtbCrVqtfzT976KDGn11Z3FcX9I7fCofndG0kkuGHTH7SdUptIlb3
+Mp8rRxlrj6BKe8Fxe8uV9yC6CK2BjWEFu9d0ApDCAQEuhP0dXrqL5oTZJyzHVSbZ1geStTlld4vn
+IlsZWlgJiObVhGN63LalcpfbdwUkhUCNv3bRhzfg1CgxRDMVkVqSKvFErStCBqQtEWc8DRhiQtjM
+DQ0M7Q0bXcLgDOz/APLYUEy+gUFv9l/Gu3h9H7etBcKBQKBQKBQKBQKBQcGyh/sHOPa/+F3j5P0J
++xQRlvC/6O+9LqaStJqtSf3PbES7savrbKOTnPUB4Dw7loJYm6va9hLehhqbbdNwULb5njCftpkT
+83gTV3bHQSn7plUTdTEPdYJDHTH5gdS6GAwAHZqUQhPMkh3o+GO3RLIVMGnIm5vppZslZHNvfm+7
+daBZ3RfbuJrcpx/a0MwwTcKyWiAWUNBvQnRQAQ0ECmKEx3b3uuwzu6xgxZp2+ZTa8sQB5KXleo2G
+jlE3YSmHubIEU5SvsDkhfVjqDpoYoBqIAAgIh3MlfnT/AE7234fL8dB+9+qvZfy7h5/39aCg70+V
+YHw+fQKDxPv86tG07pr48XSDPE2aHDJXdxl8MwDDXdmc825CcfVczTyRAxDhB4+c5ih3s6CUoAI8
+dS8pgiewXDfUP8TZmtiz1uDcpZtm6c+LnYjlB2WPkdjtN0AtnLcaMUA6Ax28v5FPw74yA5k9WyAY
+e6gAAABCZriLDWJ9vOI4NgfCmO2qA4rxi1BGoVGGsnIDUbiI8roJji/SGQDq6urqJjCAeUR40ETb
+cSdFjbxMKF4fkhWhnmZsFSFkFwMBwdmtywvDYpcddQEQ5iXISICHkEBCgl7JPyX9j/NoP6oFAoFA
+oFAoFAoFAoLC/PzDE2BdIZC+NjK0srWLkuXOf6KaGnXWgih9XLrwRdpYXzA2zmVNcndlrW8Nk3yo
+2fW0TaGnT/dH/iCR8aDIn4VrbE54n2MS7cJNWRwZ5buzyh76My50OUHN1xRFCg3Y8drg8RAX57fH
+QwgIBqA/PQSQ350VpHT2v804fPrxoOKT2LYlz1A3rGWWIvGZ/CZM1d2vsVkbODs0PID/AABAQEPn
+4DQRoc0+HPyFhLIS3O/Sn3TzfbJOu8gcUUJty98aGY/okAzULmYX1inZhuCbQkpbbgAUOJhoPLjp
+uq8UVtPfl0eybtJgm6pp/wCKW3D3vZ3v5vrfDc0839V0HE3XrHeIHWJQSR/o8NqFWHYucdrO4qVB
+2eXlFk7dPLQcWM+eLE3kMKtqaWBq2rwiSD3e9OBmfF230QbHcpiCYoyiZZNy8QRIYQ+qmvXQR40H
+vPYd4XXDcKfkOd+oZlVw3l5sXCDk5RxxdHp8xaDoJbnrAlMqlRiTrLpyjyCQ7qDOGuoDbEONBKwY
+o+xRppQsEcZ29lZmRtam5nZG5rI2NLO0tfFqbGtqRDpbJaEB0AvAPNpwoPkqa0ventfsP0cOzhxo
+I9XXg6X8o3SwKK7mtuCF0/xS7cNO42KNh9b5cxP3370O+Omnj/tHH38O9Gmg6r6afVyxhm3HSHGW
+4+VNeMs8wv8Au2+e+31S0y52aP8A6fkf+t2igzgNbqldkqF2aVwrUi38hXNvyfTQXCgUCgUCgUCg
+UHW+UMtY5w5F104ybOIvC48i1/HpI8A0j5P3KCMtvc8SxA8eql0T2nwgZo7fmE4m31TE+9v7ID6/
+1oIqW6rqg7vt3ar/AO7OYZQtj3/A8b/unE//AHQ10HafRdw5gHdH1MdtOEt0DN74YsyCSZlVQkXV
+9bWaWSyKsBpNjxpeiEuWz3I0Pc3EoCAmARDUKDbVsLC0xlpRR6PIm5naGduaW5mZm5pBpamdqaQH
+upra2rUOQpNNOH3vAAANAAAuipKlV8OPk+X5x40FjVMLX5PJ9z7oUFclSpUg/ivy60Fd7T8fw/k0
+D8W+GlB+eyJvMP0/coPrQfL2RN5h+n7lB9aC2pfaUn9NS/F8n7VBhV6kPRe26b2SPOWIosvbc9yO
+neAZhhTIV2bZgctsxjfaRj0nLYnoiJBDXUr1zcAHiFBFVxb1JNxfSM3ZZg2eZhfGzM8dxPJix6Uo
+G92uW2sAEAOzy3HhJYBTkMAD+hxABDy0Er7aD1DttO8aLd7YyyM1+8I/l0GcnfumWcP6oCg94UCg
+UCgUCgsEl9r7qXd3d5e2/wDLHc3evze9X1DQQfeun72e8yHvD/qA+26//tb7Ffs67P8AdD3Z+oaC
+I7KO9u8lv+3f336z9y+zh837v0UHFU/evtHD3k1+L3N1+fm9HWg9pdPb3g/xx7SO7vte9u/xN459
+g+zn7M/tD9Z37w91vej+73vlp2d5fgvPxoNz83e1d12vaO9Pbe7Q5u9+5u9P5sf073X9Qa/5Hk1o
+L7QU9AoKigp6BQKC2pfa9f1n83cvm8nxUD8b9r/Wfb/UnN2UFyoFBqTOvl3p/wBW/ePr78a+/bRp
+3f7na69yk/S3L6PNprprw018ulB0lsj94vtjg/8A/Umnegf/AIk+y37Qu3/dHy0Gyl2Xe932ZJ/e
+r/Fxp3Y0d2/4tvsR95f/AAH7Gvwmn9rUHsOgUCg//9k=
+
+--Apple-Mail=_83444AF4-343C-4F75-AF8F-14E1E7434FC1--
+
+--Apple-Mail=_33A037C7-4BB3-4772-AE52-FCF2D7535F74--
diff --git a/actionmailbox/test/generators/mailbox_generator_test.rb b/actionmailbox/test/generators/mailbox_generator_test.rb
new file mode 100644
index 0000000000..9ffa73321b
--- /dev/null
+++ b/actionmailbox/test/generators/mailbox_generator_test.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "rails/generators/mailbox/mailbox_generator"
+
+class MailboxGeneratorTest < Rails::Generators::TestCase
+ destination File.expand_path("../../tmp", __dir__)
+ setup :prepare_destination
+ tests Rails::Generators::MailboxGenerator
+
+ arguments ["inbox"]
+
+ def test_mailbox_skeleton_is_created
+ run_generator
+
+ assert_file "app/mailboxes/inbox_mailbox.rb" do |mailbox|
+ assert_match(/class InboxMailbox < ApplicationMailbox/, mailbox)
+ assert_match(/def process/, mailbox)
+ assert_no_match(%r{# routing /something/i => :somewhere}, mailbox)
+ end
+
+ assert_file "app/mailboxes/application_mailbox.rb" do |mailbox|
+ assert_match(/class ApplicationMailbox < ActionMailbox::Base/, mailbox)
+ assert_match(%r{# routing /something/i => :somewhere}, mailbox)
+ assert_no_match(/def process/, mailbox)
+ end
+ end
+
+ def test_mailbox_skeleton_is_created_with_namespace
+ run_generator %w(inceptions/inbox -t=test_unit)
+
+ assert_file "app/mailboxes/inceptions/inbox_mailbox.rb" do |mailbox|
+ assert_match(/class Inceptions::InboxMailbox < ApplicationMailbox/, mailbox)
+ assert_match(/def process/, mailbox)
+ assert_no_match(%r{# routing /something/i => :somewhere}, mailbox)
+ end
+
+ assert_file "test/mailboxes/inceptions/inbox_mailbox_test.rb" do |mailbox|
+ assert_match(/class Inceptions::InboxMailboxTest < ActionMailbox::TestCase/, mailbox)
+ assert_match(/# test "receive mail" do/, mailbox)
+ assert_match(/# to: '"someone" <someone@example.com>',/, mailbox)
+ end
+
+ assert_file "app/mailboxes/application_mailbox.rb" do |mailbox|
+ assert_match(/class ApplicationMailbox < ActionMailbox::Base/, mailbox)
+ assert_match(%r{# routing /something/i => :somewhere}, mailbox)
+ assert_no_match(/def process/, mailbox)
+ end
+ end
+
+ def test_check_class_collision
+ Object.send :const_set, :InboxMailbox, Class.new
+ content = capture(:stderr) { run_generator }
+ assert_match(/The name 'InboxMailbox' is either already used in your application or reserved/, content)
+ ensure
+ Object.send :remove_const, :InboxMailbox
+ end
+
+ def test_invokes_default_test_framework
+ run_generator %w(inbox -t=test_unit)
+
+ assert_file "test/mailboxes/inbox_mailbox_test.rb" do |test|
+ assert_match(/class InboxMailboxTest < ActionMailbox::TestCase/, test)
+ assert_match(/# test "receive mail" do/, test)
+ assert_match(/# to: '"someone" <someone@example.com>',/, test)
+ end
+ end
+
+ def test_mailbox_on_revoke
+ run_generator
+ run_generator ["inbox"], behavior: :revoke
+
+ assert_no_file "app/mailboxes/inbox_mailbox.rb"
+ end
+
+ def test_mailbox_suffix_is_not_duplicated
+ run_generator %w(inbox_mailbox -t=test_unit)
+
+ assert_no_file "app/mailboxes/inbox_mailbox_mailbox.rb"
+ assert_file "app/mailboxes/inbox_mailbox.rb"
+
+ assert_no_file "test/mailboxes/inbox_mailbox_mailbox_test.rb"
+ assert_file "test/mailboxes/inbox_mailbox_test.rb"
+ end
+end
diff --git a/actionmailbox/test/jobs/incineration_job_test.rb b/actionmailbox/test/jobs/incineration_job_test.rb
new file mode 100644
index 0000000000..a3907898ab
--- /dev/null
+++ b/actionmailbox/test/jobs/incineration_job_test.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionMailbox::IncinerationJobTest < ActiveJob::TestCase
+ setup { @inbound_email = receive_inbound_email_from_fixture("welcome.eml") }
+
+ test "ignoring a missing inbound email" do
+ @inbound_email.destroy!
+
+ perform_enqueued_jobs do
+ assert_nothing_raised do
+ ActionMailbox::IncinerationJob.perform_later @inbound_email
+ end
+ end
+ end
+end
diff --git a/actionmailbox/test/test_helper.rb b/actionmailbox/test/test_helper.rb
new file mode 100644
index 0000000000..bf93df1029
--- /dev/null
+++ b/actionmailbox/test/test_helper.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+ENV["RAILS_ENV"] = "test"
+ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = "tbsy84uSV1Kt3ZJZELY2TmShPRs91E3yL4tzf96297vBCkDWgL"
+
+require_relative "../test/dummy/config/environment"
+ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
+require "rails/test_help"
+
+require "byebug"
+require "webmock/minitest"
+
+Minitest.backtrace_filter = Minitest::BacktraceFilter.new
+
+require "rails/test_unit/reporter"
+Rails::TestUnitReporter.executable = "bin/test"
+
+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
+
+require "action_mailbox/test_helper"
+
+class ActiveSupport::TestCase
+ include ActionMailbox::TestHelper, ActiveJob::TestHelper
+end
+
+class ActionDispatch::IntegrationTest
+ private
+ def credentials
+ ActionController::HttpAuthentication::Basic.encode_credentials "actionmailbox", ENV["RAILS_INBOUND_EMAIL_PASSWORD"]
+ end
+
+ def switch_password_to(new_password)
+ previous_password, ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = ENV["RAILS_INBOUND_EMAIL_PASSWORD"], new_password
+ yield
+ ensure
+ ENV["RAILS_INBOUND_EMAIL_PASSWORD"] = previous_password
+ end
+end
+
+if ARGV.include?("-v")
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
+ ActiveJob::Base.logger = Logger.new(STDOUT)
+end
+
+class BounceMailer < ActionMailer::Base
+ def bounce(to:)
+ mail from: "receiver@example.com", to: to, subject: "Your email was not delivered" do |format|
+ format.html { render plain: "Sorry!" }
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/inbound_email/incineration_test.rb b/actionmailbox/test/unit/inbound_email/incineration_test.rb
new file mode 100644
index 0000000000..54488349fd
--- /dev/null
+++ b/actionmailbox/test/unit/inbound_email/incineration_test.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+class ActionMailbox::InboundEmail::IncinerationTest < ActiveSupport::TestCase
+ test "incinerating 30 days after delivery" do
+ freeze_time
+
+ assert_enqueued_with job: ActionMailbox::IncinerationJob, at: 30.days.from_now do
+ create_inbound_email_from_fixture("welcome.eml").delivered!
+ end
+
+ travel 30.days
+
+ assert_difference -> { ActionMailbox::InboundEmail.count }, -1 do
+ perform_enqueued_jobs only: ActionMailbox::IncinerationJob
+ end
+ end
+
+ test "incinerating 30 days after bounce" do
+ freeze_time
+
+ assert_enqueued_with job: ActionMailbox::IncinerationJob, at: 30.days.from_now do
+ create_inbound_email_from_fixture("welcome.eml").bounced!
+ end
+
+ travel 30.days
+
+ assert_difference -> { ActionMailbox::InboundEmail.count }, -1 do
+ perform_enqueued_jobs only: ActionMailbox::IncinerationJob
+ end
+ end
+
+ test "incinerating 30 days after failure" do
+ freeze_time
+
+ assert_enqueued_with job: ActionMailbox::IncinerationJob, at: 30.days.from_now do
+ create_inbound_email_from_fixture("welcome.eml").failed!
+ end
+
+ travel 30.days
+
+ assert_difference -> { ActionMailbox::InboundEmail.count }, -1 do
+ perform_enqueued_jobs only: ActionMailbox::IncinerationJob
+ end
+ end
+
+ test "skipping incineration" do
+ original, ActionMailbox.incinerate = ActionMailbox.incinerate, false
+
+ assert_no_enqueued_jobs only: ActionMailbox::IncinerationJob do
+ create_inbound_email_from_fixture("welcome.eml").delivered!
+ end
+ ensure
+ ActionMailbox.incinerate = original
+ end
+end
diff --git a/actionmailbox/test/unit/inbound_email/message_id_test.rb b/actionmailbox/test/unit/inbound_email/message_id_test.rb
new file mode 100644
index 0000000000..af467a8d45
--- /dev/null
+++ b/actionmailbox/test/unit/inbound_email/message_id_test.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+class ActionMailbox::InboundEmail::MessageIdTest < ActiveSupport::TestCase
+ test "message id is extracted from raw email" do
+ inbound_email = create_inbound_email_from_fixture("welcome.eml")
+ assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id
+ end
+
+ test "message id is generated if its missing" do
+ inbound_email = create_inbound_email_from_source "Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!"
+ assert_not_nil inbound_email.message_id
+ end
+end
diff --git a/actionmailbox/test/unit/inbound_email_test.rb b/actionmailbox/test/unit/inbound_email_test.rb
new file mode 100644
index 0000000000..76f047eb61
--- /dev/null
+++ b/actionmailbox/test/unit/inbound_email_test.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require_relative "../test_helper"
+
+module ActionMailbox
+ class InboundEmailTest < ActiveSupport::TestCase
+ test "mail provides the parsed source" do
+ assert_equal "Discussion: Let's debate these attachments", create_inbound_email_from_fixture("welcome.eml").mail.subject
+ end
+
+ test "source returns the contents of the raw email" do
+ assert_equal file_fixture("welcome.eml").read, create_inbound_email_from_fixture("welcome.eml").source
+ end
+
+ test "email with message id is processed only once when received multiple times" do
+ mail = Mail.from_source(file_fixture("welcome.eml").read)
+ assert mail.message_id
+
+ inbound_email_1 = create_inbound_email_from_source(mail.to_s)
+ assert inbound_email_1
+
+ inbound_email_2 = create_inbound_email_from_source(mail.to_s)
+ assert_nil inbound_email_2
+ end
+
+ test "email with missing message id is processed only once when received multiple times" do
+ mail = Mail.from_source("Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!")
+ assert_nil mail.message_id
+
+ inbound_email_1 = create_inbound_email_from_source(mail.to_s)
+ assert inbound_email_1
+
+ inbound_email_2 = create_inbound_email_from_source(mail.to_s)
+ assert_nil inbound_email_2
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/mail_ext/address_equality_test.rb b/actionmailbox/test/unit/mail_ext/address_equality_test.rb
new file mode 100644
index 0000000000..e4426aeae9
--- /dev/null
+++ b/actionmailbox/test/unit/mail_ext/address_equality_test.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+module MailExt
+ class AddressEqualityTest < ActiveSupport::TestCase
+ test "two addresses with the same address are equal" do
+ assert_equal Mail::Address.new("david@basecamp.com"), Mail::Address.new("david@basecamp.com")
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/mail_ext/address_wrapping_test.rb b/actionmailbox/test/unit/mail_ext/address_wrapping_test.rb
new file mode 100644
index 0000000000..c4eb1328ef
--- /dev/null
+++ b/actionmailbox/test/unit/mail_ext/address_wrapping_test.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+module MailExt
+ class AddressWrappingTest < ActiveSupport::TestCase
+ test "wrap" do
+ needing_wrapping = Mail::Address.wrap("david@basecamp.com")
+ wrapping_not_needed = Mail::Address.wrap(Mail::Address.new("david@basecamp.com"))
+ assert_equal needing_wrapping.address, wrapping_not_needed.address
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/mail_ext/recipients_test.rb b/actionmailbox/test/unit/mail_ext/recipients_test.rb
new file mode 100644
index 0000000000..fdcad440e2
--- /dev/null
+++ b/actionmailbox/test/unit/mail_ext/recipients_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+module MailExt
+ class RecipientsTest < ActiveSupport::TestCase
+ setup do
+ @mail = Mail.new \
+ to: "david@basecamp.com",
+ cc: "jason@basecamp.com",
+ bcc: "andrea@basecamp.com",
+ x_original_to: "ryan@basecamp.com"
+ end
+
+ test "recipients include everyone from to, cc, bcc, and x-original-to" do
+ assert_equal %w[ david@basecamp.com jason@basecamp.com andrea@basecamp.com ryan@basecamp.com ], @mail.recipients
+ end
+
+ test "recipients addresses use address objects" do
+ assert_equal "basecamp.com", @mail.recipients_addresses.first.domain
+ end
+
+ test "to addresses use address objects" do
+ assert_equal "basecamp.com", @mail.to_addresses.first.domain
+ end
+
+ test "cc addresses use address objects" do
+ assert_equal "basecamp.com", @mail.cc_addresses.first.domain
+ end
+
+ test "bcc addresses use address objects" do
+ assert_equal "basecamp.com", @mail.bcc_addresses.first.domain
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/mailbox/bouncing_test.rb b/actionmailbox/test/unit/mailbox/bouncing_test.rb
new file mode 100644
index 0000000000..d4bd6ea6db
--- /dev/null
+++ b/actionmailbox/test/unit/mailbox/bouncing_test.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+class BouncingWithReplyMailbox < ActionMailbox::Base
+ def process
+ bounce_with BounceMailer.bounce(to: mail.from)
+ end
+end
+
+class ActionMailbox::Base::BouncingTest < ActiveSupport::TestCase
+ include ActionMailer::TestHelper
+
+ setup do
+ @inbound_email = create_inbound_email_from_mail \
+ from: "sender@example.com", to: "replies@example.com", subject: "Bounce me"
+ end
+
+ test "bouncing with a reply" do
+ perform_enqueued_jobs only: ActionMailer::MailDeliveryJob do
+ BouncingWithReplyMailbox.receive @inbound_email
+ end
+
+ assert @inbound_email.bounced?
+ assert_emails 1
+
+ mail = ActionMailer::Base.deliveries.last
+ assert_equal %w[ sender@example.com ], mail.to
+ assert_equal "Your email was not delivered", mail.subject
+ end
+end
diff --git a/actionmailbox/test/unit/mailbox/callbacks_test.rb b/actionmailbox/test/unit/mailbox/callbacks_test.rb
new file mode 100644
index 0000000000..8d98a3f3ac
--- /dev/null
+++ b/actionmailbox/test/unit/mailbox/callbacks_test.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+class CallbackMailbox < ActionMailbox::Base
+ before_processing { $before_processing = "Ran that!" }
+ after_processing { $after_processing = "Ran that too!" }
+ around_processing ->(r, block) { block.call; $around_processing = "Ran that as well!" }
+
+ def process
+ $processed = mail.subject
+ end
+end
+
+class BouncingCallbackMailbox < ActionMailbox::Base
+ before_processing { $before_processing = [ "Pre-bounce" ] }
+
+ before_processing do
+ bounce_with BounceMailer.bounce(to: mail.from)
+ $before_processing << "Bounce"
+ end
+
+ before_processing { $before_processing << "Post-bounce" }
+
+ after_processing { $after_processing = true }
+
+ def process
+ $processed = true
+ end
+end
+
+class DiscardingCallbackMailbox < ActionMailbox::Base
+ before_processing { $before_processing = [ "Pre-discard" ] }
+
+ before_processing do
+ delivered!
+ $before_processing << "Discard"
+ end
+
+ before_processing { $before_processing << "Post-discard" }
+
+ after_processing { $after_processing = true }
+
+ def process
+ $processed = true
+ end
+end
+
+class ActionMailbox::Base::CallbacksTest < ActiveSupport::TestCase
+ setup do
+ $before_processing = $after_processing = $around_processing = $processed = false
+ @inbound_email = create_inbound_email_from_fixture("welcome.eml")
+ end
+
+ test "all callback types" do
+ CallbackMailbox.receive @inbound_email
+ assert_equal "Ran that!", $before_processing
+ assert_equal "Ran that too!", $after_processing
+ assert_equal "Ran that as well!", $around_processing
+ end
+
+ test "bouncing in a callback terminates processing" do
+ BouncingCallbackMailbox.receive @inbound_email
+ assert @inbound_email.bounced?
+ assert_equal [ "Pre-bounce", "Bounce" ], $before_processing
+ assert_not $processed
+ assert_not $after_processing
+ end
+
+ test "marking the inbound email as delivered in a callback terminates processing" do
+ DiscardingCallbackMailbox.receive @inbound_email
+ assert @inbound_email.delivered?
+ assert_equal [ "Pre-discard", "Discard" ], $before_processing
+ assert_not $processed
+ assert_not $after_processing
+ end
+end
diff --git a/actionmailbox/test/unit/mailbox/routing_test.rb b/actionmailbox/test/unit/mailbox/routing_test.rb
new file mode 100644
index 0000000000..d4ba702ac5
--- /dev/null
+++ b/actionmailbox/test/unit/mailbox/routing_test.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+class ApplicationMailbox < ActionMailbox::Base
+ routing "replies@example.com" => :replies
+end
+
+class RepliesMailbox < ActionMailbox::Base
+ def process
+ $processed = mail.subject
+ end
+end
+
+class ActionMailbox::Base::RoutingTest < ActiveSupport::TestCase
+ setup do
+ $processed = false
+ end
+
+ test "string routing" do
+ ApplicationMailbox.route create_inbound_email_from_fixture("welcome.eml")
+ assert_equal "Discussion: Let's debate these attachments", $processed
+ end
+
+ test "delayed routing" do
+ perform_enqueued_jobs only: ActionMailbox::RoutingJob do
+ create_inbound_email_from_fixture "welcome.eml", status: :pending
+ assert_equal "Discussion: Let's debate these attachments", $processed
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/mailbox/state_test.rb b/actionmailbox/test/unit/mailbox/state_test.rb
new file mode 100644
index 0000000000..b3a58ad667
--- /dev/null
+++ b/actionmailbox/test/unit/mailbox/state_test.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require_relative "../../test_helper"
+
+class SuccessfulMailbox < ActionMailbox::Base
+ def process
+ $processed = mail.subject
+ end
+end
+
+class UnsuccessfulMailbox < ActionMailbox::Base
+ rescue_from(RuntimeError) { $processed = :failure }
+
+ def process
+ raise "No way!"
+ end
+end
+
+class BouncingMailbox < ActionMailbox::Base
+ def process
+ $processed = :bounced
+ bounced!
+ end
+end
+
+
+class ActionMailbox::Base::StateTest < ActiveSupport::TestCase
+ setup do
+ $processed = false
+ @inbound_email = create_inbound_email_from_mail \
+ to: "replies@example.com", subject: "I was processed"
+ end
+
+ test "successful mailbox processing leaves inbound email in delivered state" do
+ SuccessfulMailbox.receive @inbound_email
+ assert @inbound_email.delivered?
+ assert_equal "I was processed", $processed
+ end
+
+ test "unsuccessful mailbox processing leaves inbound email in failed state" do
+ UnsuccessfulMailbox.receive @inbound_email
+ assert @inbound_email.failed?
+ assert_equal :failure, $processed
+ end
+
+ test "bounced inbound emails are not delivered" do
+ BouncingMailbox.receive @inbound_email
+ assert @inbound_email.bounced?
+ assert_equal :bounced, $processed
+ end
+end
diff --git a/actionmailbox/test/unit/relayer_test.rb b/actionmailbox/test/unit/relayer_test.rb
new file mode 100644
index 0000000000..fb2b48ea16
--- /dev/null
+++ b/actionmailbox/test/unit/relayer_test.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require_relative "../test_helper"
+
+require "action_mailbox/relayer"
+
+module ActionMailbox
+ class RelayerTest < ActiveSupport::TestCase
+ URL = "https://example.com/rails/action_mailbox/relay/inbound_emails"
+ INGRESS_PASSWORD = "secret"
+
+ setup do
+ @relayer = ActionMailbox::Relayer.new(url: URL, password: INGRESS_PASSWORD)
+ end
+
+ test "successfully relaying an email" do
+ stub_request(:post, URL).to_return status: 204
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "2.0.0", result.status_code
+ assert_equal "Successfully relayed message to ingress", result.message
+ assert result.success?
+ assert_not result.failure?
+
+ assert_requested :post, URL, body: file_fixture("welcome.eml").read,
+ basic_auth: [ "actionmailbox", INGRESS_PASSWORD ],
+ headers: { "Content-Type" => "message/rfc822", "User-Agent" => /\AAction Mailbox relayer v\d+\./ }
+ end
+
+ test "unsuccessfully relaying with invalid credentials" do
+ stub_request(:post, URL).to_return status: 401
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.7.0", result.status_code
+ assert_equal "Invalid credentials for ingress", result.message
+ assert_not result.success?
+ assert result.failure?
+ end
+
+ test "unsuccessfully relaying due to an unspecified server error" do
+ stub_request(:post, URL).to_return status: 500
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.0.0", result.status_code
+ assert_equal "HTTP 500", result.message
+ assert_not result.success?
+ assert result.failure?
+ end
+
+ test "unsuccessfully relaying due to a gateway timeout" do
+ stub_request(:post, URL).to_return status: 504
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.0.0", result.status_code
+ assert_equal "HTTP 504", result.message
+ assert_not result.success?
+ assert result.failure?
+ end
+
+ test "unsuccessfully relaying due to ECONNRESET" do
+ stub_request(:post, URL).to_raise Errno::ECONNRESET.new
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.4.2", result.status_code
+ assert_equal "Network error relaying to ingress: Connection reset by peer", result.message
+ assert_not result.success?
+ assert result.failure?
+ end
+
+ test "unsuccessfully relaying due to connection failure" do
+ stub_request(:post, URL).to_raise SocketError.new("Failed to open TCP connection to example.com:443")
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.4.2", result.status_code
+ assert_equal "Network error relaying to ingress: Failed to open TCP connection to example.com:443", result.message
+ assert_not result.success?
+ assert result.failure?
+ end
+
+ test "unsuccessfully relaying due to client-side timeout" do
+ stub_request(:post, URL).to_timeout
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.4.2", result.status_code
+ assert_equal "Timed out relaying to ingress", result.message
+ assert_not result.success?
+ assert result.failure?
+ end
+
+ test "unsuccessfully relaying due to an unhandled exception" do
+ stub_request(:post, URL).to_raise StandardError.new("Something went wrong")
+
+ result = @relayer.relay(file_fixture("welcome.eml").read)
+ assert_equal "4.0.0", result.status_code
+ assert_equal "Error relaying to ingress: Something went wrong", result.message
+ assert_not result.success?
+ assert result.failure?
+ end
+ end
+end
diff --git a/actionmailbox/test/unit/router_test.rb b/actionmailbox/test/unit/router_test.rb
new file mode 100644
index 0000000000..c5dce60856
--- /dev/null
+++ b/actionmailbox/test/unit/router_test.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require_relative "../test_helper"
+
+class RootMailbox < ActionMailbox::Base
+ def process
+ $processed_by = self.class.to_s
+ $processed_mail = mail
+ end
+end
+
+class FirstMailbox < RootMailbox
+end
+
+class SecondMailbox < RootMailbox
+end
+
+module Nested
+ class FirstMailbox < RootMailbox
+ end
+end
+
+class FirstMailboxAddress
+ def match?(inbound_email)
+ inbound_email.mail.to.include?("replies-class@example.com")
+ end
+end
+
+module ActionMailbox
+ class RouterTest < ActiveSupport::TestCase
+ setup do
+ @router = ActionMailbox::Router.new
+ $processed_by = $processed_mail = nil
+ end
+
+ test "single string route" do
+ @router.add_routes("first@example.com" => :first)
+
+ inbound_email = create_inbound_email_from_mail(to: "first@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "FirstMailbox", $processed_by
+ assert_equal inbound_email.mail, $processed_mail
+ end
+
+ test "single string routing on cc" do
+ @router.add_routes("first@example.com" => :first)
+
+ inbound_email = create_inbound_email_from_mail(to: "someone@example.com", cc: "first@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "FirstMailbox", $processed_by
+ assert_equal inbound_email.mail, $processed_mail
+ end
+
+ test "single string routing case-insensitively" do
+ @router.add_routes("first@example.com" => :first)
+
+ inbound_email = create_inbound_email_from_mail(to: "FIRST@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "FirstMailbox", $processed_by
+ assert_equal inbound_email.mail, $processed_mail
+ end
+
+ test "multiple string routes" do
+ @router.add_routes("first@example.com" => :first, "second@example.com" => :second)
+
+ inbound_email = create_inbound_email_from_mail(to: "first@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "FirstMailbox", $processed_by
+ assert_equal inbound_email.mail, $processed_mail
+
+ inbound_email = create_inbound_email_from_mail(to: "second@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "SecondMailbox", $processed_by
+ assert_equal inbound_email.mail, $processed_mail
+ end
+
+ test "single regexp route" do
+ @router.add_routes(/replies-\w+@example.com/ => :first, "replies-nowhere@example.com" => :second)
+
+ inbound_email = create_inbound_email_from_mail(to: "replies-okay@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "FirstMailbox", $processed_by
+ end
+
+ test "single proc route" do
+ @router.add_route \
+ ->(inbound_email) { inbound_email.mail.to.include?("replies-proc@example.com") },
+ to: :second
+
+ @router.route create_inbound_email_from_mail(to: "replies-proc@example.com", subject: "This is a reply")
+ assert_equal "SecondMailbox", $processed_by
+ end
+
+ test "address class route" do
+ @router.add_route FirstMailboxAddress.new, to: :first
+ @router.route create_inbound_email_from_mail(to: "replies-class@example.com", subject: "This is a reply")
+ assert_equal "FirstMailbox", $processed_by
+ end
+
+ test "string route to nested mailbox" do
+ @router.add_route "first@example.com", to: "nested/first"
+
+ inbound_email = create_inbound_email_from_mail(to: "first@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "Nested::FirstMailbox", $processed_by
+ end
+
+ test "all as the only route" do
+ @router.add_route :all, to: :first
+ @router.route create_inbound_email_from_mail(to: "replies-class@example.com", subject: "This is a reply")
+ assert_equal "FirstMailbox", $processed_by
+ end
+
+ test "all as the second route" do
+ @router.add_route FirstMailboxAddress.new, to: :first
+ @router.add_route :all, to: :second
+
+ @router.route create_inbound_email_from_mail(to: "replies-class@example.com", subject: "This is a reply")
+ assert_equal "FirstMailbox", $processed_by
+
+ @router.route create_inbound_email_from_mail(to: "elsewhere@example.com", subject: "This is a reply")
+ assert_equal "SecondMailbox", $processed_by
+ end
+
+ test "missing route" do
+ assert_raises(ActionMailbox::Router::RoutingError) do
+ inbound_email = create_inbound_email_from_mail(to: "going-nowhere@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert inbound_email.bounced?
+ end
+ end
+
+ test "invalid address" do
+ assert_raises(ArgumentError) do
+ @router.add_route Array.new, to: :first
+ end
+ end
+ end
+end
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md
index 51d85b95f2..182c43551d 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,111 +1,81 @@
-## Rails 5.0.0.beta3 (February 24, 2016) ##
+## Rails 6.0.0.beta3 (March 11, 2019) ##
-* Add support for fragment caching in Action Mailer views.
-
- *Stan Lo*
-
-* Reset `ActionMailer::Base.deliveries` after every test in
- `ActionDispatch::IntegrationTest`.
-
- *Yves Senn*
+* No changes.
-## Rails 5.0.0.beta2 (February 01, 2016) ##
+## Rails 6.0.0.beta2 (February 25, 2019) ##
* No changes.
-## Rails 5.0.0.beta1 (December 18, 2015) ##
+## Rails 6.0.0.beta1 (January 18, 2019) ##
-* `config.force_ssl = true` will set
- `config.action_mailer.default_url_options = { protocol: 'https' }`.
+* Deprecate `ActionMailer::Base.receive` in favor of [Action Mailbox](https://github.com/rails/rails/tree/master/actionmailbox).
- *Andrew Kampjes*
-
-* Add `config.action_mailer.deliver_later_queue_name` configuration to set the
- mailer queue name.
+ *George Claghorn*
- *Chris McGrath*
+* Add `MailDeliveryJob` for delivering both regular and parameterized mail. Deprecate using `DeliveryJob` and `Parameterized::DeliveryJob`.
-* `assert_emails` in block form use the given number as expected value.
- This makes the error message much easier to understand.
+ *Gannon McGibbon*
- *Yuji Yaginuma*
+* Fix ActionMailer assertions not working when a Mail defines
+ a custom delivery job class
-* Add support for inline images in mailer previews by using an interceptor
- class to convert cid: urls in image src attributes to data urls.
+ *Edouard Chin*
- *Andrew White*
+* Mails with multipart `format` blocks with implicit render now also check for
+ a template name in options hash instead of only using the action name.
-* Mailer preview now uses `url_for` to fix links to emails for apps running on
- a subdirectory.
+ *Marcus Ilgner*
- *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 deprecated `*_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.5.0 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*, *Kasper Timm Hansen*
- *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 8573eb1225..ab7c27c209 100644
--- a/actionmailer/MIT-LICENSE
+++ b/actionmailer/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2016 David Heinemeier Hansson
+Copyright (c) 2004-2019 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 397ebe4201..0db4b2668b 100644
--- a/actionmailer/README.rdoc
+++ b/actionmailer/README.rdoc
@@ -13,6 +13,8 @@ Additionally, an Action Mailer class can be used to process incoming email,
such as allowing a blog to accept new posts from an email (which could even
have been sent from a phone).
+You can read more about Action Mailer in the {Action Mailer Basics}[https://edgeguides.rubyonrails.org/action_mailer_basics.html] guide.
+
== Sending emails
The framework works by initializing any instance variables you want to be
@@ -93,42 +95,6 @@ Example:
.....
end
-== Receiving emails
-
-To receive emails, you need to implement a public instance method called
-+receive+ that takes an email object as its single parameter. The Action Mailer
-framework has a corresponding class method, which is also called +receive+, that
-accepts a raw, unprocessed email as a string, which it then turns into the email
-object and calls the receive instance method.
-
-Example:
-
- class Mailman < ActionMailer::Base
- def receive(email)
- page = Page.find_by(address: email.to.first)
- page.emails.create(
- subject: email.subject, body: email.body
- )
-
- if email.has_attachments?
- email.attachments.each do |attachment|
- page.attachments.create({
- file: attachment, description: email.subject
- })
- end
- end
- end
- end
-
-This Mailman can be the target for Postfix or other MTAs. In Rails, you would use
-the runner in the trivial case like this:
-
- rails runner 'Mailman.receive(STDIN.read)'
-
-However, invoking Rails in the runner for each mail to be received is very
-resource intensive. A single instance of Rails should be run within a daemon, if
-it is going to process more than just a limited amount of email.
-
== Configuration
The Base class has the full list of configuration options. Here's an example:
@@ -148,7 +114,7 @@ The latest version of Action Mailer can be installed with RubyGems:
$ gem install actionmailer
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/actionmailer
@@ -157,16 +123,16 @@ 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
API documentation is at
-* http://api.rubyonrails.org
+* https://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile
index 54e6cff48d..6ac408e1cb 100644
--- a/actionmailer/Rakefile
+++ b/actionmailer/Rakefile
@@ -1,15 +1,16 @@
-require 'rake/testtask'
+# frozen_string_literal: true
+
+require "rake/testtask"
desc "Default Task"
task default: [ :test ]
task :package
-task "package:clean"
# 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)
@@ -18,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 fa6043fdd7..cadccb905b 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 and delivery framework (part of Rails)."
+ s.description = "Email on Rails. Compose, deliver, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments."
+
+ s.required_ruby_version = ">= 2.5.0"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "https://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://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 55c017e338..953c5fb508 100644
--- a/actionmailer/lib/action_mailer.rb
+++ b/actionmailer/lib/action_mailer.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2019 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,15 +45,24 @@ 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
+ autoload :MailDeliveryJob
+
+ def self.eager_load!
+ super
+
+ require "mail"
+ Mail.eager_autoload!
+ end
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/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index a223cf82a1..c1ac9c2ad1 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,7 +135,10 @@ module ActionMailer
#
# config.action_mailer.default_url_options = { host: "example.com" }
#
- # By default when <tt>config.force_ssl</tt> is true, URLs generated for hosts will use the HTTPS protocol.
+ # 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
#
@@ -144,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
@@ -163,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
@@ -207,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
@@ -274,21 +293,20 @@ 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
+ # 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
@@ -298,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:
#
@@ -310,7 +328,6 @@ module ActionMailer
# end
#
# private
- #
# def add_inline_attachment!
# attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg')
# end
@@ -389,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.
@@ -416,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!
@@ -442,8 +461,8 @@ module ActionMailer
helper ActionMailer::MailHelper
- class_attribute :default_params
- self.default_params = {
+ class_attribute :delivery_job, default: ::ActionMailer::DeliveryJob
+ class_attribute :default_params, default: {
mime_version: "1.0",
charset: "UTF-8",
content_type: "text/plain",
@@ -456,11 +475,21 @@ 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.
@@ -468,6 +497,13 @@ module ActionMailer
Mail.register_observer(observer_class_for(observer))
end
+ # 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.
@@ -475,6 +511,13 @@ module ActionMailer
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
+
def observer_class_for(value) # :nodoc:
case value
when String, Symbol
@@ -485,7 +528,7 @@ module ActionMailer
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
@@ -522,6 +565,11 @@ module ActionMailer
# end
# end
def receive(raw_mail)
+ ActiveSupport::Deprecation.warn(<<~MESSAGE.squish)
+ ActionMailer::Base.receive is deprecated and will be removed in Rails 6.1.
+ Use Action Mailbox to process inbound email.
+ MESSAGE
+
ActiveSupport::Notifications.instrument("receive.action_mailer") do |payload|
mail = Mail.new(raw_mail)
set_payload_for_mail(payload, mail)
@@ -542,21 +590,22 @@ module ActionMailer
end
end
- protected
+ private
- 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
+ def set_payload_for_mail(payload, mail)
+ payload[:mail] = mail.encoded
+ 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[: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
@@ -564,19 +613,13 @@ module ActionMailer
end
end
- private
-
- def respond_to_missing?(method, include_all = false) #:nodoc:
- action_methods.include?(method.to_s)
+ def respond_to_missing?(method, include_all = false)
+ action_methods.include?(method.to_s) || super
end
end
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
super()
@_mail_was_called = false
@@ -586,7 +629,8 @@ module ActionMailer
def process(method_name, *args) #:nodoc:
payload = {
mailer: self.class.name,
- action: method_name
+ action: method_name,
+ args: args
}
ActiveSupport::Notifications.instrument("process.action_mailer", payload) do
@@ -596,10 +640,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
@@ -828,138 +872,155 @@ module ActionMailer
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?
- false
- end
+ # Emails do not support relative path links.
+ def self.supports_path? # :doc:
+ false
+ end
- private
+ 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 apply_defaults(headers)
- default_values = self.class.default.map do |key, value|
- [
- key,
- value.is_a?(Proc) ? instance_eval(&value) : value
- ]
- end.to_h
-
- headers_with_defaults = headers.reverse_merge(default_values)
- headers_with_defaults[:subject] ||= default_i18n_subject
- headers_with_defaults
- end
+ def compute_default(value)
+ return value unless value.is_a?(Proc)
- 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
+ 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) }
+ def collect_responses(headers, &block)
+ if block_given?
+ collect_responses_from_block(headers, &block)
+ elsif headers[:body]
+ collect_responses_from_text(headers)
+ else
+ collect_responses_from_templates(headers)
+ end
+ end
+
+ def collect_responses_from_block(headers)
+ templates_name = headers[:template_name] || action_name
+ collector = ActionMailer::Collector.new(lookup_context) { render(templates_name) }
yield(collector)
collector.responses
- elsif headers[:body]
+ end
+
+ def collect_responses_from_text(headers)
[{
body: headers.delete(:body),
- content_type: self.class.default[:content_type] || "text/plain"
+ content_type: headers[:content_type] || "text/plain"
}]
- else
- collect_responses_from_templates(headers)
end
- end
- def collect_responses_from_templates(headers)
- templates_path = headers[:template_path] || self.class.mailer_name
- templates_name = headers[:template_name] || action_name
+ def collect_responses_from_templates(headers)
+ templates_path = headers[:template_path] || self.class.mailer_name
+ templates_name = headers[:template_name] || action_name
- each_template(Array(templates_path), templates_name).map do |template|
- self.formats = template.formats
- {
- body: render(template: template),
- content_type: template.type.to_s
- }
+ each_template(Array(templates_path), templates_name).map do |template|
+ format = template.format || self.formats.first
+ {
+ body: render(template: template, formats: [format]),
+ content_type: Mime[format].to_s
+ }
+ end
end
- 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)
+ 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(&:format).each(&block)
+ end
end
- end
- 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) }
+ 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 insert_part(container, response, charset)
- response[:charset] ||= charset
- part = Mail::Part.new(response)
- container.add_part(part)
- end
+ def insert_part(container, response, charset)
+ 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
+ # This and #instrument_name is for caching instrument
+ def instrument_payload(key)
+ {
+ mailer: mailer_name,
+ key: key
+ }
+ end
- def instrument_name
- "action_mailer"
- 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..f228006920 100644
--- a/actionmailer/lib/action_mailer/delivery_job.rb
+++ b/actionmailer/lib/action_mailer/delivery_job.rb
@@ -1,13 +1,44 @@
-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
+
+ before_perform do
+ ActiveSupport::Deprecation.warn <<~MSG.squish
+ Sending mail with DeliveryJob and Parameterized::DeliveryJob
+ is deprecated and will be removed in Rails 6.1.
+ Please use MailDeliveryJob instead.
+ MSG
+ end
+
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 cbe5fc3e64..4efcd966db 100644
--- a/actionmailer/lib/action_mailer/gem_version.rb
+++ b/actionmailer/lib/action_mailer/gem_version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionMailer
# Returns the version of the currently loaded Action Mailer as a <tt>Gem::Version</tt>.
def self.gem_version
@@ -5,7 +7,7 @@ module ActionMailer
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "beta3"
diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
index 419d6c7b93..2b97ac5b94 100644
--- a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
+++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
@@ -1,8 +1,10 @@
-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
+ # 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
@@ -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 2867bf90fb..26910f20f0 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,12 @@ module ActionMailer
# An email was delivered.
def deliver(event)
info do
- recipients = Array(event.payload[:to]).join(', ')
- "Sent mail to #{recipients} (#{event.duration.round(1)}ms)"
+ perform_deliveries = event.payload[:perform_deliveries]
+ if perform_deliveries
+ "Delivered mail #{event.payload[:message_id]} (#{event.duration.round(1)}ms)"
+ else
+ "Skipped delivery of mail #{event.payload[:message_id]} as `perform_deliveries` is false"
+ end
end
debug { event.payload[:mail] }
diff --git a/actionmailer/lib/action_mailer/mail_delivery_job.rb b/actionmailer/lib/action_mailer/mail_delivery_job.rb
new file mode 100644
index 0000000000..609c6a72d9
--- /dev/null
+++ b/actionmailer/lib/action_mailer/mail_delivery_job.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "active_job"
+
+module ActionMailer
+ # The <tt>ActionMailer::MailDeliveryJob</tt> class is used when you
+ # want to send emails outside of the request-response cycle. It supports
+ # sending either parameterized or normal mail.
+ #
+ # Exceptions are rescued and handled by the mailer class.
+ class MailDeliveryJob < 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:, params: nil) #:nodoc:
+ mailer_class = params ? mailer.constantize.with(params) : mailer.constantize
+ mailer_class.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/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 5fcb5a0c88..1e5cab6d47 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,29 +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 ||= begin
- mailer = @mailer.new
- mailer.process @mail_method, *@args
- mailer.message
- end
+ @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+
@@ -51,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
@@ -67,7 +82,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
@@ -77,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:
@@ -85,14 +110,43 @@ 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 = {})
+ 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
+ job = @mailer_class.delivery_job
+ args = arguments_for(job, delivery_method)
+ job.set(options).perform_later(*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 arguments_for(delivery_job, delivery_method)
+ if delivery_job <= MailDeliveryJob
+ [@mailer_class.name, @action.to_s, delivery_method.to_s, args: @args]
+ else
+ [@mailer_class.name, @action.to_s, delivery_method.to_s, *@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..0a97af8105
--- /dev/null
+++ b/actionmailer/lib/action_mailer/parameterized.rb
@@ -0,0 +1,171 @@
+# 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 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
+
+ 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
+ job = delivery_job_class
+ args = arguments_for(job, delivery_method)
+ job.set(options).perform_later(*args)
+ end
+ end
+
+ def delivery_job_class
+ if @mailer_class.delivery_job <= MailDeliveryJob
+ @mailer_class.delivery_job
+ else
+ Parameterized::DeliveryJob
+ end
+ end
+
+ def arguments_for(delivery_job, delivery_method)
+ if delivery_job <= MailDeliveryJob
+ [@mailer_class.name, @action.to_s, delivery_method.to_s, params: @params, args: @args]
+ else
+ [@mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args]
+ end
+ end
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb
index aab92fe8db..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,27 +31,51 @@ 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.
def all
@@ -61,8 +86,8 @@ 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
@@ -73,42 +98,42 @@ module ActionMailer
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.
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.
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 a727ed38e9..893a4a25b1 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"
@@ -18,7 +20,7 @@ module ActionMailer
if app.config.force_ssl
options.default_url_options ||= {}
- options.default_url_options[:protocol] ||= 'https'
+ options.default_url_options[:protocol] ||= "https"
end
options.assets_dir ||= paths["public"].first
@@ -28,7 +30,7 @@ module ActionMailer
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
@@ -44,10 +46,25 @@ module ActionMailer
register_preview_interceptors(options.delete(:preview_interceptors))
register_observers(options.delete(:observers))
- options.each { |k,v| send("#{k}=", v) }
+ if delivery_job = options.delete(:delivery_job)
+ self.delivery_job = delivery_job.constantize
+ end
+
+ options.each { |k, v| send("#{k}=", v) }
end
- ActiveSupport.on_load(:action_dispatch_integration_test) { include ActionMailer::TestCase::ClearTestDeliveries }
+ ActiveSupport.on_load(:action_dispatch_integration_test) do
+ include ActionMailer::TestHelper
+ include ActionMailer::TestCase::ClearTestDeliveries
+ end
+ end
+
+ initializer "action_mailer.set_autoload_paths" do |app|
+ options = app.config.action_mailer
+
+ if options.show_previews && options.preview_path
+ ActiveSupport::Dependencies.autoload_paths << options.preview_path
+ end
end
initializer "action_mailer.compile_config_methods" do
@@ -56,17 +73,19 @@ module ActionMailer
end
end
+ 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
+ get "/rails/mailers" => "rails/mailers#index", internal: true
+ get "/rails/mailers/*path" => "rails/mailers#preview", internal: true
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 d83719e57d..ee5a864847 100644
--- a/actionmailer/lib/action_mailer/test_case.rb
+++ b/actionmailer/lib/action_mailer/test_case.rb
@@ -1,11 +1,13 @@
-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
@@ -15,15 +17,17 @@ module ActionMailer
extend ActiveSupport::Concern
included do
- teardown :clear_test_deliviers
+ setup :clear_test_deliveries
+ teardown :clear_test_deliveries
end
private
- def clear_test_deliviers
- if ActionMailer::Base.delivery_method == :test
- ActionMailer::Base.deliveries.clear
+
+ def clear_test_deliveries
+ if ActionMailer::Base.delivery_method == :test
+ ActionMailer::Base.deliveries.clear
+ end
end
- end
end
module Behavior
@@ -39,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
@@ -54,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)
@@ -70,36 +75,36 @@ module ActionMailer
end
end
- protected
+ private
- def initialize_test_deliveries # :nodoc:
+ def initialize_test_deliveries
set_delivery_method :test
@old_perform_deliveries = ActionMailer::Base.perform_deliveries
ActionMailer::Base.perform_deliveries = true
+ ActionMailer::Base.deliveries.clear
end
- def restore_test_deliveries # :nodoc:
+ def restore_test_deliveries
restore_delivery_method
ActionMailer::Base.perform_deliveries = @old_perform_deliveries
end
- def set_delivery_method(method) # :nodoc:
+ def set_delivery_method(method)
@old_delivery_method = ActionMailer::Base.delivery_method
ActionMailer::Base.delivery_method = method
end
- def restore_delivery_method # :nodoc:
+ def restore_delivery_method
+ ActionMailer::Base.deliveries.clear
ActionMailer::Base.delivery_method = @old_delivery_method
end
- def set_expected_mail # :nodoc:
+ def set_expected_mail
@expected = Mail.new
@expected.content_type ["text", "plain", { "charset" => charset }]
- @expected.mime_version = '1.0'
+ @expected.mime_version = "1.0"
end
- private
-
def charset
"UTF-8"
end
@@ -109,11 +114,10 @@ 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
include Behavior
- include ClearTestDeliveries
end
end
diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb
index e423aac389..e222301dff 100644
--- a/actionmailer/lib/action_mailer/test_helper.rb
+++ b/actionmailer/lib/action_mailer/test_helper.rb
@@ -1,4 +1,6 @@
-require 'active_job'
+# frozen_string_literal: true
+
+require "active_job"
module ActionMailer
# Provides helper methods for testing Action Mailer, including #assert_emails
@@ -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: ->(job) { delivery_job_filter(job) }, &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
@@ -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,46 @@ module ActionMailer
# end
# end
def assert_enqueued_emails(number, &block)
- assert_enqueued_jobs number, only: ActionMailer::DeliveryJob, &block
+ assert_enqueued_jobs(number, only: ->(job) { delivery_job_filter(job) }, &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)
+ args = if args.is_a?(Hash)
+ [mailer.to_s, method.to_s, "deliver_now", params: args, args: []]
+ else
+ [mailer.to_s, method.to_s, "deliver_now", args: Array(args)]
+ end
+ assert_enqueued_with(job: mailer.delivery_job, args: args, queue: queue, &block)
end
# Asserts that no emails are enqueued for later delivery.
@@ -107,7 +148,15 @@ module ActionMailer
# end
# end
def assert_no_enqueued_emails(&block)
- assert_no_enqueued_jobs only: ActionMailer::DeliveryJob, &block
+ assert_enqueued_emails 0, &block
end
+
+ private
+
+ def delivery_job_filter(job)
+ job_class = job.is_a?(Hash) ? job.fetch(:job) : job.class
+
+ Base.descendants.map(&:delivery_job).include?(job_class)
+ 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 01bdfb0685..c37a74c762 100644
--- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
+++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
@@ -1,36 +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")
+ template "mailer.rb", File.join("app/mailers", class_path, "#{file_name}_mailer.rb")
in_root do
- if self.behavior == :invoke && !File.exist?(application_mailer_file_name)
- template 'application_mailer.rb', application_mailer_file_name
+ 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
- private
def application_mailer_file_name
@_application_mailer_file_name ||= if mountable_engine?
- "app/mailers/#{namespaced_path}/application_mailer.rb"
- else
- "app/mailers/application_mailer.rb"
- end
+ "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 f23e575fe5..0000000000
--- a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-<% 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/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 8d740ac863..448807c144 100644
--- a/actionmailer/test/abstract_unit.rb
+++ b/actionmailer/test/abstract_unit.rb
@@ -1,25 +1,27 @@
-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
module Rails
def self.root
- File.expand_path('../', File.dirname(__FILE__))
+ 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'
+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.
@@ -28,18 +30,22 @@ 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
-# 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
+ActionMailer::Base.delivery_job = ActionMailer::MailDeliveryJob
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
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 50f2c71737..c07fca5b5e 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"]
@@ -300,6 +307,16 @@ class BaseTest < ActiveSupport::TestCase
assert_equal("HTML Implicit Multipart", email.parts[1].body.encoded)
end
+ test "implicit multipart formats" do
+ email = BaseMailer.implicit_multipart_formats
+ assert_equal(2, email.parts.size)
+ assert_equal("multipart/alternative", email.mime_type)
+ assert_equal("text/plain", email.parts[0].mime_type)
+ assert_equal("Implicit Multipart [:text]", email.parts[0].body.encoded)
+ assert_equal("text/html", email.parts[1].mime_type)
+ assert_equal("Implicit Multipart [:html]", email.parts[1].body.encoded)
+ end
+
test "implicit multipart with sort order" do
order = ["text/html", "text/plain"]
with_default BaseMailer, parts_order: order do
@@ -315,22 +332,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 +382,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 +396,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 +436,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,7 +464,7 @@ class BaseTest < ActiveSupport::TestCase
assert_equal("Format with any!", email.parts[1].body.encoded)
end
- test 'explicit without specifying format with format.any' do
+ test "explicit without specifying format with format.any" do
error = assert_raises(ArgumentError) do
BaseMailer.explicit_without_specifying_format_with_any.parts
end
@@ -501,20 +516,20 @@ 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
assert_called(BaseMailer, :deliver_mail) do
mail = BaseMailer.welcome.deliver_now
- assert_equal 'The first email on new API!', mail.subject
+ assert_equal "The first email on new API!", mail.subject
end
end
@@ -534,35 +549,41 @@ class BaseTest < ActiveSupport::TestCase
# 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 "you can specify a different template for multipart render" do
+ mail = BaseMailer.implicit_different_template_with_block("explicit_multipart_templates").deliver
+ assert_equal("HTML Explicit Multipart Templates", mail.html_part.body.decoded)
+ assert_equal("TEXT Explicit Multipart Templates", 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
@@ -572,7 +593,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
@@ -586,18 +607,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
@@ -613,37 +634,52 @@ 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
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
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
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
@@ -652,6 +688,14 @@ class BaseTest < ActiveSupport::TestCase
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
@@ -665,37 +709,52 @@ 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
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
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
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
@@ -704,20 +763,37 @@ class BaseTest < ActiveSupport::TestCase
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.stub(:now, yesterday) do
- mail2 = ProcMailer.welcome['X-Proc-Method']
+ 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
@@ -732,12 +808,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
@@ -747,12 +823,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
@@ -762,19 +838,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
@@ -798,8 +874,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")
@@ -814,7 +890,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
@@ -830,7 +906,39 @@ class BaseTest < ActiveSupport::TestCase
assert_equal "special indeed!", mail["X-Special-Header"].to_s
end
- protected
+ test "notification for process" do
+ 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
+
+ test "notification for deliver" do
+ events = []
+ ActiveSupport::Notifications.subscribe("deliver.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 "deliver.action_mailer", events[0].name
+ assert_not_nil events[0].payload[:message_id]
+ ensure
+ ActiveSupport::Notifications.unsubscribe "deliver.action_mailer"
+ end
+
+ private
# Execute the block setting the given values and restoring old values after
# the block is executed.
@@ -855,8 +963,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)
@@ -895,7 +1001,7 @@ 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
stub_any_instance(BaseMailerPreview) do |instance|
@@ -905,9 +1011,14 @@ class BasePreviewInterceptorsTest < ActiveSupport::TestCase
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
stub_any_instance(BaseMailerPreview) do |instance|
@@ -917,9 +1028,14 @@ class BasePreviewInterceptorsTest < ActiveSupport::TestCase
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
stub_any_instance(BaseMailerPreview) do |instance|
@@ -929,9 +1045,14 @@ class BasePreviewInterceptorsTest < ActiveSupport::TestCase
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
stub_any_instance(BaseMailerPreview) do |instance|
@@ -943,5 +1064,28 @@ class BasePreviewInterceptorsTest < ActiveSupport::TestCase
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
index b4344eb167..b658c96ec7 100644
--- a/actionmailer/test/caching_test.rb
+++ b/actionmailer/test/caching_test.rb
@@ -1,11 +1,13 @@
-require 'fileutils'
-require 'abstract_unit'
-require 'mailers/base_mailer'
-require 'mailers/caching_mailer'
+# frozen_string_literal: true
-CACHE_DIR = 'test_cache'
+require "fileutils"
+require "abstract_unit"
+require "mailers/base_mailer"
+require "mailers/caching_mailer"
+
+CACHE_DIR = "test_cache"
# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed
-FILE_STORE_PATH = File.join(File.dirname(__FILE__), '/../temp/', CACHE_DIR)
+FILE_STORE_PATH = File.join(__dir__, "/../temp/", CACHE_DIR)
class FragmentCachingMailer < ActionMailer::Base
abstract!
@@ -21,93 +23,89 @@ class BaseCachingTest < ActiveSupport::TestCase
@mailer.perform_caching = true
@mailer.cache_store = @store
end
-
- def test_fragment_cache_key
- assert_equal 'views/what a key', @mailer.fragment_cache_key('what a key')
- end
end
class FragmentCachingTest < BaseCachingTest
def test_read_fragment_with_caching_enabled
- @store.write('views/name', 'value')
- assert_equal 'value', @mailer.read_fragment('name')
+ @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')
+ @store.write("views/name", "value")
+ assert_nil @mailer.read_fragment("name")
end
def test_fragment_exist_with_caching_enabled
- @store.write('views/name', 'value')
- assert @mailer.fragment_exist?('name')
- assert !@mailer.fragment_exist?('other_name')
+ @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 !@mailer.fragment_exist?('name')
- assert !@mailer.fragment_exist?('other_name')
+ @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')
+ 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')
+ assert_nil @store.read("views/name")
@mailer.perform_caching = false
- assert_equal 'value', @mailer.write_fragment('name', 'value')
- assert_nil @store.read('views/name')
+ 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')
+ @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 ;-)')
+ @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')
+ 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 = @mailer.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, @mailer.write_fragment('name', content)
+ assert_nil @store.read("views/name")
+ content = "value".html_safe
+ assert_equal content, @mailer.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 = @mailer.read_fragment('name')
+ html_safe = @mailer.read_fragment("name")
assert_equal content, html_safe
- assert html_safe.html_safe?
+ assert_predicate html_safe, :html_safe?
end
end
@@ -125,16 +123,17 @@ class FunctionalFragmentCachingTest < BaseCachingTest
expected_body = "\"Welcome\""
assert_match expected_body, email.body.encoded
- assert_match "\"Welcome\"",
- @store.read("views/caching/#{template_digest("caching_mailer/fragment_cache")}")
+ assert_match expected_body,
+ @store.read("views/caching_mailer/fragment_cache:#{template_digest("caching_mailer/fragment_cache", "html")}/caching")
end
def test_fragment_caching_in_partials
email = @mailer.fragment_cache_in_partials
- assert_match(/Old fragment caching in a partial/, email.body.encoded)
+ expected_body = "Old fragment caching in a partial"
+ assert_match(expected_body, email.body.encoded)
- assert_match("Old fragment caching in a partial",
- @store.read("views/caching/#{template_digest("caching_mailer/_partial")}"))
+ assert_match(expected_body,
+ @store.read("views/caching_mailer/_partial:#{template_digest("caching_mailer/_partial", "html")}/caching"))
end
def test_skip_fragment_cache_digesting
@@ -145,18 +144,61 @@ class FunctionalFragmentCachingTest < BaseCachingTest
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", "html")}", :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)
+ def template_digest(name, format)
+ ActionView::Digestor.digest(name: name, format: format, finder: @mailer.lookup_context)
end
end
class CacheHelperOutputBufferTest < BaseCachingTest
-
class MockController
def read_fragment(name, options)
- return false
+ false
end
def write_fragment(name, fragment, options)
@@ -172,9 +214,9 @@ class CacheHelperOutputBufferTest < BaseCachingTest
output_buffer = ActionView::OutputBuffer.new
controller = MockController.new
cache_helper = Class.new do
- def self.controller; end;
- def self.output_buffer; end;
- def self.output_buffer=; end;
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
end
cache_helper.extend(ActionView::Helpers::CacheHelper)
@@ -182,7 +224,7 @@ class CacheHelperOutputBufferTest < BaseCachingTest
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 }
+ cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil }
end
end
end
@@ -193,9 +235,9 @@ class CacheHelperOutputBufferTest < BaseCachingTest
output_buffer = ActiveSupport::SafeBuffer.new
controller = MockController.new
cache_helper = Class.new do
- def self.controller; end;
- def self.output_buffer; end;
- def self.output_buffer=; end;
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
end
cache_helper.extend(ActionView::Helpers::CacheHelper)
@@ -203,7 +245,7 @@ class CacheHelperOutputBufferTest < BaseCachingTest
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 }
+ cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil }
end
end
end
@@ -220,7 +262,7 @@ class ViewCacheDependencyTest < BaseCachingTest
end
def test_view_cache_dependencies_are_empty_by_default
- assert NoDependenciesMailer.new.view_cache_dependencies.empty?
+ assert_empty NoDependenciesMailer.new.view_cache_dependencies
end
def test_view_cache_dependencies_are_listed_in_declaration_order
diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb
index 2786fe0d07..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,
@@ -32,14 +34,14 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase
end
test "default file delivery settings (with Rails.root)" do
- settings = {location: "#{Rails.root}/tmp/mails"}
+ 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
@@ -127,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
@@ -145,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]
diff --git a/actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.html.erb b/actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.html.erb
new file mode 100644
index 0000000000..0179b070b8
--- /dev/null
+++ b/actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.html.erb
@@ -0,0 +1 @@
+Implicit Multipart <%= formats.inspect %> \ No newline at end of file
diff --git a/actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.text.erb b/actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.text.erb
new file mode 100644
index 0000000000..0179b070b8
--- /dev/null
+++ b/actionmailer/test/fixtures/base_mailer/implicit_multipart_formats.text.erb
@@ -0,0 +1 @@
+Implicit Multipart <%= formats.inspect %> \ No newline at end of file
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/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb
index 50c4b74eb8..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,7 +20,7 @@ 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
@@ -26,7 +28,7 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new
Routes.draw do
ActiveSupport::Deprecation.silence do
- get ':controller(/:action(/:id))'
+ get ":controller(/:action(/:id))"
end
end
@@ -56,22 +58,20 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest
def test_send_mail
stub_any_instance(Mail::SMTP, instance: Mail::SMTP.new({})) do |instance|
assert_called(instance, :deliver!) do
- with_translation 'de', email_subject: '[Anmeldung] Willkommen' do
- ActiveSupport::Deprecation.silence do
- get '/test/send_mail'
- end
+ 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/legacy_delivery_job_test.rb b/actionmailer/test/legacy_delivery_job_test.rb
new file mode 100644
index 0000000000..3a3872df47
--- /dev/null
+++ b/actionmailer/test/legacy_delivery_job_test.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_job"
+require "mailers/params_mailer"
+require "mailers/delayed_mailer"
+
+class LegacyDeliveryJobTest < ActiveSupport::TestCase
+ include ActiveJob::TestHelper
+
+ class LegacyDeliveryJob < ActionMailer::DeliveryJob
+ end
+
+ 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
+ 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 "should send parameterized mail correctly" do
+ mail = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com").invitation
+ args = [
+ "ParamsMailer",
+ "invitation",
+ "deliver_now",
+ { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" },
+ ]
+
+ with_delivery_job(LegacyDeliveryJob) do
+ assert_deprecated do
+ assert_performed_with(job: ActionMailer::Parameterized::DeliveryJob, args: args) do
+ mail.deliver_later
+ end
+ end
+ end
+ end
+
+ test "should send mail correctly" do
+ mail = DelayedMailer.test_message(1, 2, 3)
+ args = [
+ "DelayedMailer",
+ "test_message",
+ "deliver_now",
+ 1,
+ 2,
+ 3,
+ ]
+
+ with_delivery_job(LegacyDeliveryJob) do
+ assert_deprecated do
+ assert_performed_with(job: LegacyDeliveryJob, args: args) do
+ mail.deliver_later
+ end
+ end
+ end
+ end
+
+ private
+
+ def with_delivery_job(job)
+ old_params_delivery_job = ParamsMailer.delivery_job
+ old_regular_delivery_job = DelayedMailer.delivery_job
+ ParamsMailer.delivery_job = job
+ DelayedMailer.delivery_job = job
+ yield
+ ensure
+ ParamsMailer.delivery_job = old_params_delivery_job
+ DelayedMailer.delivery_job = old_regular_delivery_job
+ end
+end
diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb
index 3871b16840..f09f1997c0 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
@@ -22,11 +24,11 @@ class AMLogSubscriberTest < ActionMailer::TestCase
end
def test_deliver_is_notified
- BaseMailer.welcome.deliver_now
+ BaseMailer.welcome(message_id: "123@abc").deliver_now
wait
assert_equal(1, @logger.logged(:info).size)
- assert_match(/Sent mail to system@test.lindsaar.net/, @logger.logged(:info).first)
+ assert_match(/Delivered mail 123@abc/, @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,9 +37,25 @@ class AMLogSubscriberTest < ActionMailer::TestCase
BaseMailer.deliveries.clear
end
+ def test_deliver_message_when_perform_deliveries_is_false
+ BaseMailer.welcome_without_deliveries(message_id: "123@abc").deliver_now
+ wait
+
+ assert_equal(1, @logger.logged(:info).size)
+ assert_match("Skipped delivery of mail 123@abc 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")
- TestMailer.receive(fixture)
+ fixture = File.read(File.expand_path("fixtures/raw_email", __dir__))
+ assert_deprecated do
+ TestMailer.receive(fixture)
+ end
wait
assert_equal(1, @logger.logged(:info).size)
assert_match(/Received mail/, @logger.logged(:info).first)
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 8c2225ce60..6bd58304e2 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(hash = {})
+ mail({ template_name: "welcome" }.merge!(hash))
+ mail.perform_deliveries = false
+ end
+
def html_only(hash = {})
mail(hash)
end
@@ -28,30 +35,34 @@ 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
+
+ def implicit_multipart_formats(hash = {})
mail(hash)
end
@@ -60,10 +71,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,7 +87,7 @@ 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
@@ -88,8 +99,8 @@ class BaseMailer < ActionMailer::Base
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
@@ -100,18 +111,25 @@ 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 implicit_different_template_with_block(template_name = "")
+ mail(template_name: template_name) do |format|
+ format.text
+ format.html
+ end
+ end
+
+ 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 }
@@ -131,6 +149,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
index 345d267a36..02f0c6c103 100644
--- a/actionmailer/test/mailers/caching_mailer.rb
+++ b/actionmailer/test/mailers/caching_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CachingMailer < ActionMailer::Base
self.mailer_name = "caching_mailer"
@@ -12,4 +14,12 @@ class CachingMailer < ActionMailer::Base
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 b834cdd08c..46260f6414 100644
--- a/actionmailer/test/message_delivery_test.rb
+++ b/actionmailer/test/message_delivery_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'active_job'
-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
@@ -12,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::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [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::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now!", args: [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::MailDeliveryJob, at: Time.current + 10.minutes, args: ["DelayedMailer", "test_message", "deliver_now", args: [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::MailDeliveryJob, at: later_time, args: ["DelayedMailer", "test_message", "deliver_now", args: [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::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [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", args: [1, 2, 3]]) do
+ @mail.deliver_later
+ end
+
+ DelayedMailer.delivery_job = old_delivery_job
+ end
+
+ class DummyJob < ActionMailer::MailDeliveryJob; end
+
+ test "can override the queue when enqueuing mail" do
+ assert_performed_with(job: ActionMailer::MailDeliveryJob, args: ["DelayedMailer", "test_message", "deliver_now", args: [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..7dd6156744
--- /dev/null
+++ b/actionmailer/test/parameterized_test.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_job"
+require "mailers/params_mailer"
+
+class ParameterizedTest < ActiveSupport::TestCase
+ include ActiveJob::TestHelper
+
+ class DummyDeliveryJob < ActionMailer::MailDeliveryJob
+ end
+
+ 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
+ args = [
+ "ParamsMailer",
+ "invitation",
+ "deliver_now",
+ params: { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" },
+ args: [],
+ ]
+ assert_performed_with(job: ActionMailer::MailDeliveryJob, args: args) 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
+ args = [
+ "ParamsMailer",
+ "invitation",
+ "deliver_now",
+ params: { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" },
+ args: [],
+ ]
+
+ with_delivery_job DummyDeliveryJob do
+ assert_performed_with(job: DummyDeliveryJob, args: args) do
+ @mail.deliver_later
+ end
+ end
+ end
+
+ private
+
+ def with_delivery_job(job)
+ old_delivery_job = ParamsMailer.delivery_job
+ ParamsMailer.delivery_job = job
+ yield
+ ensure
+ ParamsMailer.delivery_job = old_delivery_job
+ 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..60e2389aa8 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,25 @@ 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 CustomDeliveryJob < ActionMailer::MailDeliveryJob
+end
+
+class CustomDeliveryMailer < TestHelperMailer
+ self.delivery_job = CustomDeliveryJob
end
class TestHelperMailerTest < ActionMailer::TestCase
@@ -40,11 +61,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 +76,36 @@ class TestHelperMailerTest < ActionMailer::TestCase
end
end
+ def test_assert_emails_with_custom_delivery_job
+ assert_nothing_raised do
+ assert_emails(1) do
+ silence_stream($stdout) do
+ CustomDeliveryMailer.test.deliver_later
+ end
+ end
+ end
+ end
+
+ def test_assert_emails_with_custom_parameterized_delivery_job
+ assert_nothing_raised do
+ assert_emails(1) do
+ silence_stream($stdout) do
+ CustomDeliveryMailer.with(foo: "bar").test_parameter_args.deliver_later
+ end
+ end
+ 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 +142,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 +206,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
@@ -155,6 +228,16 @@ class TestHelperMailerTest < ActionMailer::TestCase
assert_match(/2 .* but 1/, error.message)
end
+ def test_assert_enqueued_emails_with_custom_delivery_job
+ assert_nothing_raised do
+ assert_enqueued_emails(1) do
+ silence_stream($stdout) do
+ CustomDeliveryMailer.test.deliver_later
+ end
+ end
+ end
+ end
+
def test_assert_enqueued_emails_too_many_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_enqueued_emails 1 do
@@ -176,6 +259,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 +278,73 @@ 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_when_mailer_has_custom_delivery_job
+ assert_nothing_raised do
+ assert_enqueued_email_with CustomDeliveryMailer, :test do
+ silence_stream($stdout) do
+ CustomDeliveryMailer.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 +356,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 70bd05055f..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,54 +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
- ActiveSupport::Deprecation.silence do
- get ':controller(/:action(/:id))'
- get '/welcome' => 'foo#bar', as: 'welcome'
- get '/dummy_model' => 'foo#baz', as: 'dummy_model'
- end
- end
-
# string
- 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
- ActiveSupport::Deprecation.silence do
- get ':controller(/:action(/:id))'
- get '/welcome' => "foo#bar", as: "welcome"
- end
- 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"
@@ -128,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 7d400eaa22..79f6320a04 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,795 +1,242 @@
-* ETags: Introduce `Response#strong_etag=` and `#weak_etag=` and analogous
- options for `fresh_when` and `stale?`. `Response#etag=` sets a weak ETag.
+* Raise an `ArgumentError` if a resource custom param contains a colon (`:`).
- Strong ETags are desirable when you're serving byte-for-byte identical
- responses that support Range requests, like PDFs or videos (typically
- done by reproxying the response from a backend storage service).
- Also desirable when fronted by some CDNs that support strong ETags
- only, like Akamai.
+ After this change it's not possible anymore to configure routes like this:
- *Jeremy Daer*
-
-* ETags: No longer strips quotes (") from ETag values before comparing them.
- Quotes are significant, part of the ETag. A quoted ETag and an unquoted
- one are not the same entity.
-
- *Jeremy Daer*
-
-* ETags: Support `If-None-Match: *`. Rarely useful for GET requests; meant
- to provide some optimistic concurrency control for PUT requests.
-
- *Jeremy Daer*
-
-* `ActionDispatch::ParamsParser` is deprecated and was removed from the middleware
- stack. To configure the parameter parsers use `ActionDispatch::Request.parameter_parsers=`.
-
- *tenderlove*
-
-* When a `respond_to` collector with a block doesn't have a response, then
- a `:no_content` response should be rendered. This brings the default
- rendering behavior introduced by https://github.com/rails/rails/issues/19036
- to controller methods employing `respond_to`.
-
- *Justin Coyne*
-
-* Add `ActionController::Parameters#dig` on Ruby 2.3 and greater, which
- behaves the same as `Hash#dig`.
-
- *Sean Griffin*
-
-* Add request headers in the payload of the `start_processing.action_controller`
- and `process_action.action_controller` notifications.
-
- *Gareth du Plooy*
-
-* Add `action_dispatch_integration_test` load hook. The hook can be used to
- extend `ActionDispatch::IntegrationTest` once it has been loaded.
-
- *Yuichiro Kaneko*
-
-* Update default rendering policies when the controller action did
- not explicitly indicate a response.
-
- For API controllers, the implicit render always renders "204 No Content"
- and does not account for any templates.
-
- For other controllers, the following conditions are checked:
-
- First, if a template exists for the controller action, it is rendered.
- This template lookup takes into account the action name, locales, format,
- variant, template handlers, etc. (see `render` for details).
-
- Second, if other templates exist for the controller action but is not in
- the right format (or variant, etc.), an `ActionController::UnknownFormat`
- is raised. The list of available templates is assumed to be a complete
- enumeration of all the possible formats (or variants, etc.); that is,
- having only HTML and JSON templates indicate that the controller action is
- not meant to handle XML requests.
-
- Third, if the current request is an "interactive" browser request (the user
- navigated here by entering the URL in the address bar, submitting a form,
- clicking on a link, etc. as opposed to an XHR or non-browser API request),
- `ActionView::UnknownFormat` is raised to display a helpful error
- message.
-
- Finally, it falls back to the same "204 No Content" behavior as API controllers.
-
- *Godfrey Chan*, *Jon Moss*, *Kasper Timm Hansen*, *Mike Clark*, *Matthew Draper*
-
-## Rails 5.0.0.beta3 (February 24, 2016) ##
-
-* Add "application/gzip" as a default mime type.
-
- *Mehmet Emin İNAÇ*
-
-* Add request encoding and response parsing to integration tests.
-
- What previously was:
-
- ```ruby
- require 'test_helper'
-
- class ApiTest < ActionDispatch::IntegrationTest
- test 'creates articles' do
- assert_difference -> { Article.count } do
- post articles_path(format: :json),
- params: { article: { title: 'Ahoy!' } }.to_json,
- headers: { 'Content-Type' => 'application/json' }
- end
-
- assert_equal({ 'id' => Article.last.id, 'title' => 'Ahoy!' }, JSON.parse(response.body))
- end
- end
```
-
- Can now be written as:
-
- ```ruby
- 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_equal({ 'id' => Article.last.id, 'title' => 'Ahoy!' }, response.parsed_body)
- end
+ routes.draw do
+ resources :users, param: 'name/:sneaky'
end
```
- Passing `as: :json` to integration test request helpers will set the format,
- content type and encode the parameters as JSON.
-
- Then on the response side, `parsed_body` will parse the body according to the
- content type the response has.
-
- Currently JSON is the only supported MIME type. Add your own with
- `ActionDispatch::IntegrationTest.register_encoder`.
-
- *Kasper Timm Hansen*
+ Fixes #30467.
-* Add "image/svg+xml" as a default mime type.
+ *Josua Schmid*
- *DHH*
-## Rails 5.0.0.beta2 (February 01, 2016) ##
+## Rails 6.0.0.beta3 (March 11, 2019) ##
-* Add `-g` and `-c` options to `bin/rails routes`. These options return the url `name`, `verb` and
- `path` field that match the pattern or match a specific controller.
+* No changes.
- Deprecate `CONTROLLER` env variable in `bin/rails routes`.
- See #18902.
+## Rails 6.0.0.beta2 (February 25, 2019) ##
- *Anton Davydov*, *Vipul A M*
+* Make debug exceptions works in an environment where ActiveStorage is not loaded.
-* Response etags to always be weak: Prefixes 'W/' to value returned by
- `ActionDispatch::Http::Cache::Response#etag=`, such that etags set in
- `fresh_when` and `stale?` are weak.
+ *Tomoyuki Kurosawa*
- Fixes #17556.
-
- *Abhishek Yadav*
-
-* Provide the name of HTTP Status code in assertions.
-
- *Sean Collins*
-
-* More explicit error message when running `rake routes`. `CONTROLLER` argument
- can now be supplied in different ways:
- `Rails::WelcomeController`, `Rails::Welcome`, `rails/welcome`.
-
- Fixes #22918.
+* `ActionDispatch::SystemTestCase.driven_by` can now be called with a block
+ to define specific browser capabilities.
*Edouard Chin*
-* Allow `ActionController::Parameters` instances as an argument to URL
- helper methods. An `ArgumentError` will be raised if the passed parameters
- are not secure.
-
- Fixes #22832.
-
- *Prathamesh Sonpatki*
-
-* Add option for per-form CSRF tokens.
-
- *Greg Ose*, *Ben Toews*
-
-* Fix `ActionController::Parameters#convert_parameters_to_hashes` to return filtered
- or unfiltered values based on from where it is called, `to_h` or `to_unsafe_h`
- respectively.
-
- Fixes #22841.
-
- *Prathamesh Sonpatki*
-
-* Add `ActionController::Parameters#include?`
- *Justin Coyne*
+## Rails 6.0.0.beta1 (January 18, 2019) ##
-## Rails 5.0.0.beta1 (December 18, 2015) ##
+* Remove deprecated `fragment_cache_key` helper in favor of `combined_fragment_cache_key`.
-* Deprecate `redirect_to :back` in favor of `redirect_back`, which accepts a
- required `fallback_location` argument, thus eliminating the possibility of a
- `RedirectBackError`.
-
- *Derek Prior*
-
-* Add `redirect_back` method to `ActionController::Redirecting` to provide a
- way to safely redirect to the `HTTP_REFERER` if it is present, falling back
- to a provided redirect otherwise.
-
- *Derek Prior*
-
-* `ActionController::TestCase` will be moved to it's own gem in Rails 5.1
-
- With the speed improvements made to `ActionDispatch::IntegrationTest` we no
- longer need to keep two separate code bases for testing controllers. In
- Rails 5.1 `ActionController::TestCase` will be deprecated and moved into a
- gem outside of Rails source.
-
- This is a documentation deprecation so that going forward so new tests will use
- `ActionDispatch::IntegrationTest` instead of `ActionController::TestCase`.
+ *Rafael Mendonça França*
- *Eileen M. Uchitelle*
+* Remove deprecated methods in `ActionDispatch::TestResponse`.
-* Add a `response_format` option to `ActionDispatch::DebugExceptions`
- to configure the format of the response when errors occur in
- development mode.
+ `#success?`, `missing?` and `error?` were deprecated in Rails 5.2 in favor of
+ `#successful?`, `not_found?` and `server_error?`.
- If `response_format` is `:default` the debug info will be rendered
- in an HTML page. In the other hand, if the provided value is `:api`
- the debug info will be rendered in the original response format.
+ *Rafael Mendonça França*
- *Jorge Bejar*
+* Introduce `ActionDispatch::HostAuthorization`.
-* Change the `protect_from_forgery` prepend default to `false`.
+ This is a new middleware that guards against DNS rebinding attacks by
+ explicitly permitting the hosts a request can be made to.
- Per this comment
- https://github.com/rails/rails/pull/18334#issuecomment-69234050 we want
- `protect_from_forgery` to default to `prepend: false`.
+ Each host is checked with the case operator (`#===`) to support `RegExp`,
+ `Proc`, `IPAddr` and custom objects as host allowances.
- `protect_from_forgery` will now be inserted into the callback chain at the
- point it is called in your application. This is useful for cases where you
- want to `protect_from_forgery` after you perform required authentication
- callbacks or other callbacks that are required to run after forgery protection.
+ *Genadi Samokovarov*
- If you want `protect_from_forgery` callbacks to always run first, regardless of
- position they are called in your application then you can add `prepend: true`
- to your `protect_from_forgery` call.
+* Allow using `parsed_body` in `ActionController::TestCase`.
- Example:
+ In addition to `ActionDispatch::IntegrationTest`, allow using
+ `parsed_body` in `ActionController::TestCase`:
- ```ruby
- protect_from_forgery prepend: true
+ ```
+ class SomeControllerTest < ActionController::TestCase
+ def test_some_action
+ post :action, body: { foo: 'bar' }
+ assert_equal({ "foo" => "bar" }, response.parsed_body)
+ end
+ end
```
- *Eileen M. Uchitelle*
-
-* In url_for, never append a question mark to the URL when the query string
- is empty anyway. (It used to do that when called like `url_for(controller:
- 'x', action: 'y', q: {})`.)
-
- *Paul Grayson*
-
-* Catch invalid UTF-8 querystring values and respond with BadRequest
-
- Check querystring params for invalid UTF-8 characters, and raise an
- ActionController::BadRequest error if present. Previously these strings
- would typically trigger errors further down the stack.
-
- *Grey Baker*
-
-* Parse RSS/ATOM responses as XML, not HTML.
-
- *Alexander Kaupanin*
-
-* Show helpful message in `BadRequest` exceptions due to invalid path
- parameter encodings.
-
- Fixes #21923.
-
- *Agis Anastasopoulos*
-
-* Add the ability of returning arbitrary headers to `ActionDispatch::Static`.
-
- Now ActionDispatch::Static can accept HTTP headers so that developers
- will have control of returning arbitrary headers like
- 'Access-Control-Allow-Origin' when a response is delivered. They can be
- configured with `#config`:
-
- Example:
-
- config.public_file_server.headers = {
- "Cache-Control" => "public, max-age=60",
- "Access-Control-Allow-Origin" => "http://rubyonrails.org"
- }
-
- *Yuki Nishijima*
-
-* Allow multiple `root` routes in same scope level. Example:
-
- Example:
-
- root 'blog#show', constraints: ->(req) { Hostname.blog_site?(req.host) }
- root 'landing#show'
-
- *Rafael Sales*
-
-* Fix regression in mounted engine named routes generation for app deployed to
- a subdirectory. `relative_url_root` was prepended to the path twice (e.g.
- "/subdir/subdir/engine_path" instead of "/subdir/engine_path")
-
- Fixes #20920. Fixes #21459.
-
- *Matthew Erhard*
-
-* `ActionDispatch::Response#new` no longer applies default headers. If you want
- default headers applied to the response object, then call
- `ActionDispatch::Response.create`. This change only impacts people who are
- directly constructing an `ActionDispatch::Response` object.
-
-* Accessing mime types via constants like `Mime::HTML` is deprecated. Please
- change code like this:
-
- Mime::HTML
-
- To this:
-
- Mime[:html]
-
- This change is so that Rails will not manage a list of constants, and fixes
- an issue where if a type isn't registered you could possibly get the wrong
- object.
-
- `Mime[:html]` is available in older versions of Rails, too, so you can
- safely change libraries and plugins and maintain compatibility with
- multiple versions of Rails.
-
-* `url_for` does not modify its arguments when generating polymorphic URLs.
-
- *Bernerd Schaefer*
-
-* 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.
-
- SSL redirect:
- * Move `:host` and `:port` options within `redirect: { … }`. Deprecate.
- * Introduce `:status` and `:body` to customize the redirect response.
- The 301 permanent default makes it difficult to test the redirect and
- back out of it since browsers remember the 301. Test with a 302 or 307
- instead, then switch to 301 once you're confident that all is well.
-
- HTTP Strict Transport Security (HSTS):
- * Shorter max-age. Shorten the default max-age from 1 year to 180 days,
- the low end for https://www.ssllabs.com/ssltest/ grading and greater
- than the 18-week minimum to qualify for browser preload lists.
- * Disabling HSTS. Setting `hsts: false` now sets `hsts { expires: 0 }`
- instead of omitting the header. Omitting does nothing to disable HSTS
- since browsers hang on to your previous settings until they expire.
- Sending `{ hsts: { expires: 0 }}` flushes out old browser settings and
- actually disables HSTS:
- http://tools.ietf.org/html/rfc6797#section-6.1.1
- * HSTS Preload. Introduce `preload: true` to set the `preload` flag,
- indicating that your site may be included in browser preload lists,
- including Chrome, Firefox, Safari, IE11, and Edge. Submit your site:
- https://hstspreload.appspot.com
-
- *Jeremy Daer*
-
-* Update `ActionController::TestSession#fetch` to behave more like
- `ActionDispatch::Request::Session#fetch` when using non-string keys.
-
- *Jeremy Friesen*
-
-* Using strings or symbols for middleware class names is deprecated. Convert
- things like this:
-
- middleware.use "Foo::Bar"
-
- to this:
-
- middleware.use Foo::Bar
-
-* `ActionController::TestSession` now accepts a default value as well as
- a block for generating a default value based off the key provided.
-
- This fixes calls to `session#fetch` in `ApplicationController` instances that
- take more two arguments or a block from raising `ArgumentError: wrong
- number of arguments (2 for 1)` when performing controller tests.
-
- *Matthew Gerrior*
-
-* Fix `ActionController::Parameters#fetch` overwriting `KeyError` returned by
- default block.
-
- *Jonas Schuber Erlandsson*, *Roque Pinel*
-
-* `ActionController::Parameters` no longer inherits from
- `HashWithIndifferentAccess`
-
- Inheriting from `HashWithIndifferentAccess` allowed users to call any
- enumerable methods on `Parameters` object, resulting in a risk of losing the
- `permitted?` status or even getting back a pure `Hash` object instead of
- a `Parameters` object with proper sanitization.
-
- By not inheriting from `HashWithIndifferentAccess`, we are able to make
- sure that all methods that are defined in `Parameters` object will return
- a proper `Parameters` object with a correct `permitted?` flag.
-
- *Prem Sichanugrist*
-
-* Replaced `ActiveSupport::Concurrency::Latch` with `Concurrent::CountDownLatch`
- from the concurrent-ruby gem.
-
- *Jerry D'Antonio*
-
-* Add ability to filter parameters based on parent keys.
-
- # matches {credit_card: {code: "xxxx"}}
- # doesn't match {file: { code: "xxxx"}}
- config.filter_parameters += [ "credit_card.code" ]
-
- See #13897.
-
- *Guillaume Malette*
-
-* Deprecate passing first parameter as `Hash` and default status code for `head` method.
-
- *Mehmet Emin İNAÇ*
-
-* Adds`Rack::Utils::ParameterTypeError` and `Rack::Utils::InvalidParameterError`
- to the rescue_responses hash in `ExceptionWrapper` (Rack recommends
- integrators serve 400s for both of these).
-
- *Grey Baker*
-
-* Add support for API only apps.
- `ActionController::API` is added as a replacement of
- `ActionController::Base` for this kind of applications.
-
- *Santiago Pastorino*, *Jorge Bejar*
-
-* Remove `assigns` and `assert_template`. Both methods have been extracted
- into a gem at https://github.com/rails/rails-controller-testing.
-
- See #18950.
-
- *Alan Guo Xiang Tan*
-
-* `FileHandler` and `Static` middleware initializers accept `index` argument
- to configure the directory index file name. Defaults to `index` (as in
- `index.html`).
-
- See #20017.
-
- *Eliot Sykes*
-
-* Deprecate `:nothing` option for `render` method.
-
- *Mehmet Emin İNAÇ*
-
-* Fix `rake routes` not showing the right format when
- nesting multiple routes.
-
- See #18373.
-
- *Ravil Bayramgalin*
-
-* Add ability to override default form builder for a controller.
-
- 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`.
-
- Additionally 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.
+ Fixes #34676.
- *arthurnn*
+ *Tobias Bühlmann*
-* `ActionController#translate` supports symbols as shortcuts.
- When a shortcut is given it also performs the lookup without the action
- name.
+* Raise an error on root route naming conflicts.
- *Max Melentiev*
+ Raises an `ArgumentError` when multiple root routes are defined in the
+ same context instead of assigning nil names to subsequent roots.
-* 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.
+ *Gannon McGibbon*
- # Before
- def index
- @articles = Article.all
- fresh_when(etag: @articles, last_modified: @articles.maximum(:updated_at))
- end
+* Allow rescue from parameter parse errors:
- # After
- def index
- @articles = Article.all
- fresh_when(@articles)
- end
-
- *claudiob*
+ ```
+ rescue_from ActionDispatch::Http::Parameters::ParseError do
+ head :unauthorized
+ end
+ ```
-* Explicitly ignored wildcard verbs when searching for HEAD routes before fallback
+ *Gannon McGibbon*, *Josh Cheek*
- 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.
+* Reset Capybara sessions if failed system test screenshot raising an exception.
- Example:
+ Reset Capybara sessions if `take_failed_screenshot` raise exception
+ in system test `after_teardown`.
- draw do
- get '/home' => 'test#index'
- mount rack_app, at: '/'
- end
- head '/home'
- assert_response :success
+ *Maxim Perepelitsa*
- 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.
+* Use request object for context if there's no controller
- *Terence Sun*
+ 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.
-* Migrating xhr methods to keyword arguments syntax
- in `ActionController::TestCase` and `ActionDispatch::Integration`
+ Fixes #34200.
- Old syntax:
+ *Andrew White*
- xhr :get, :create, params: { id: 1 }
+* Apply mapping to symbols returned from dynamic CSP sources
- New syntax example:
+ Previously if a dynamic source returned a symbol such as :self it
+ would be converted to a string implicitly, e.g:
- get :create, params: { id: 1 }, xhr: true
+ policy.default_src -> { :self }
- *Kir Shatrov*
+ would generate the header:
-* Migrating to keyword arguments syntax in `ActionController::TestCase` and
- `ActionDispatch::Integration` HTTP request methods.
+ Content-Security-Policy: default-src self
- Example:
+ and now it generates:
- post :create, params: { y: x }, session: { a: 'b' }
- get :view, params: { id: 1 }
- get :view, params: { id: 1 }, format: :json
+ Content-Security-Policy: default-src 'self'
- *Kir Shatrov*
+ *Andrew White*
-* Preserve default url options when generating URLs.
+* Add `ActionController::Parameters#each_value`.
- 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.
+ *Lukáš Zapletal*
- *Tekin Suleyman*
+* Deprecate `ActionDispatch::Http::ParameterFilter` in favor of `ActiveSupport::ParameterFilter`.
-* Deprecate `*_via_redirect` integration test methods.
+ *Yoshiyuki Kinjo*
- Use `follow_redirect!` manually after the request call for the same behavior.
+* 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.
- *Aditya Kapoor*
+ *Fumiaki Matsushima*
-* Add `ActionController::Renderer` to render arbitrary templates
- outside controller actions.
+* Expose `ActionController::Parameters#each_key` which allows iterating over
+ keys without allocating an array.
- Its functionality is accessible through class methods `render` and
- `renderer` of `ActionController::Base`.
+ *Richard Schneeman*
- *Ravil Bayramgalin*
+* Purpose metadata for signed/encrypted cookies.
-* Support `:assigns` option when rendering with controllers/mailers.
+ Rails can now thwart attacks that attempt to copy signed/encrypted value
+ of a cookie and use it as the value of another cookie.
- *Ravil Bayramgalin*
+ 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.
-* Default headers, removed in controller actions, are no longer reapplied on
- the test response.
+ Enable `action_dispatch.use_cookies_with_metadata` to use this feature, which
+ writes cookies with the new purpose and expiry metadata embedded.
- *Jonas Baumann*
+ *Assain Jaleel*
-* Deprecate all `*_filter` callbacks in favor of `*_action` callbacks.
+* Raises `ActionController::RespondToMismatchError` with conflicting `respond_to` invocations.
- *Rafael Mendonça França*
+ `respond_to` can match multiple types and lead to undefined behavior when
+ multiple invocations are made and the types do not match:
-* 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*
+ *Patrick Toomey*
-* Remove `ActionController::HideActions`.
+* `ActionDispatch::Http::UploadedFile` now delegates `to_path` to its tempfile.
- *Ravil Bayramgalin*
+ This allows uploaded file objects to be passed directly to `File.read`
+ without raising a `TypeError`:
-* Remove `respond_to`/`respond_with` placeholder methods, this functionality
- has been extracted to the `responders` gem.
+ uploaded_file = ActionDispatch::Http::UploadedFile.new(tempfile: tmp_file)
+ File.read(uploaded_file)
- *Carlos Antonio da Silva*
+ *Aaron Kromer*
-* Remove deprecated assertion files.
+* Pass along arguments to underlying `get` method in `follow_redirect!`
- *Rafael Mendonça França*
-
-* Remove deprecated usage of string keys in URL helpers.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `only_path` option on `*_path` helpers.
-
- *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 `NamedRouteCollection#helpers`.
+ follow_redirect!(params: { foo: :bar })
- *Rafael Mendonça França*
+ *Remo Fritzsche*
-* Remove deprecated support to define routes with `:to` option that doesn't contain `#`.
+* 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::Response#to_ary`.
+ *Vinicius Stock*
- *Rafael Mendonça França*
+* Introduce `ActionDispatch::DebugExceptions.register_interceptor`.
-* Remove deprecated `ActionDispatch::Request#deep_munge`.
+ 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 `ActionDispatch::Http::Parameters#symbolized_path_parameters`.
-
- *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.
-
- *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.5.0 or newer.
- Fixes #17819.
+ *Jeremy Daer*, *Kasper Timm Hansen*
- *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 8573eb1225..ab7c27c209 100644
--- a/actionpack/MIT-LICENSE
+++ b/actionpack/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2016 David Heinemeier Hansson
+Copyright (c) 2004-2019 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 0720c66cb9..fe85bc5b7a 100644
--- a/actionpack/README.rdoc
+++ b/actionpack/README.rdoc
@@ -23,6 +23,7 @@ by default and Action View rendering is implicitly triggered by Action
Controller. However, these modules are designed to function on their own and
can be used outside of Rails.
+You can read more about Action Pack in the {Action Controller Overview}[https://guides.rubyonrails.org/action_controller_overview.html] guide.
== Download and installation
@@ -30,7 +31,7 @@ The latest version of Action Pack can be installed with RubyGems:
$ gem install actionpack
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/actionpack
@@ -39,16 +40,16 @@ 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
+* https://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/actionpack/Rakefile b/actionpack/Rakefile
index 37a269cffd..e99eb1723a 100644
--- a/actionpack/Rakefile
+++ b/actionpack/Rakefile
@@ -1,16 +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
-task "package:clean"
# Run the unit tests
Rake::TestTask.new do |t|
- t.libs << 'test'
+ t.libs << "test"
t.test_files = test_files
t.warning = true
@@ -21,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 66300754e3..735eb734d0 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.5.0"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "https://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://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', '~> 2.x'
- 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 1e57cbaac4..3a98931167 100644
--- a/actionpack/lib/abstract_controller.rb
+++ b/actionpack/lib/abstract_controller.rb
@@ -1,10 +1,13 @@
-require 'action_pack'
-require 'active_support/rails'
-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 :ActionNotFound, "abstract_controller/base"
autoload :Base
autoload :Caching
autoload :Callbacks
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 16dec31938..bb42f2e119 100644
--- a/actionpack/lib/abstract_controller/base.rb
+++ b/actionpack/lib/abstract_controller/base.rb
@@ -1,9 +1,10 @@
-require 'erubis'
-require 'abstract_controller/error'
-require 'active_support/configurable'
-require 'active_support/descendants_tracker'
-require 'active_support/core_ext/module/anonymous'
-require 'active_support/core_ext/module/attr_internal'
+# frozen_string_literal: true
+
+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.
@@ -15,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
@@ -70,13 +78,15 @@ 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.
def clear_action_methods!
@@ -94,7 +104,7 @@ module AbstractController
# ==== Returns
# * <tt>String</tt>
def controller_path
- @controller_path ||= name.sub(/Controller$/, ''.freeze).underscore unless anonymous?
+ @controller_path ||= name.sub(/Controller$/, "").underscore unless anonymous?
end
# Refresh the cached action_methods when a new action_method is added.
@@ -150,6 +160,13 @@ module AbstractController
_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
# a path. A subclass of +AbstractController::Base+
# may return false. An Email controller for example does not
@@ -165,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
@@ -208,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
@@ -224,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
index 0dea50889a..ce6b757c3c 100644
--- a/actionpack/lib/abstract_controller/caching.rb
+++ b/actionpack/lib/abstract_controller/caching.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbstractController
module Caching
extend ActiveSupport::Concern
@@ -29,13 +31,15 @@ module AbstractController
extend ConfigMethods
config_accessor :default_static_extension
- self.default_static_extension ||= '.html'
+ 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 = []
+ 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
@@ -49,9 +53,9 @@ module AbstractController
self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact
end
- protected
+ private
# Convenience accessor.
- def cache(key, options = {}, &block)
+ def cache(key, options = {}, &block) # :doc:
if cache_configured?
cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block)
else
diff --git a/actionpack/lib/abstract_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb
index 3257a731ed..18677ddd18 100644
--- a/actionpack/lib/abstract_controller/caching/fragments.rb
+++ b/actionpack/lib/abstract_controller/caching/fragments.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbstractController
module Caching
# Fragment caching is used for caching various blocks within
@@ -25,7 +27,9 @@ module AbstractController
self.fragment_cache_keys = []
- helper_method :fragment_cache_key if respond_to?(:helper_method)
+ if respond_to?(:helper_method)
+ helper_method :combined_fragment_cache_key
+ end
end
module ClassMethods
@@ -51,20 +55,24 @@ module AbstractController
# end
# end
def fragment_cache_key(value = nil, &key)
- self.fragment_cache_keys += [key || ->{ value }]
+ 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>,
+ # 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. The key is expanded using
- # ActiveSupport::Cache.expand_cache_key.
- def fragment_cache_key(key)
+ # 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
- ActiveSupport::Cache.expand_cache_key([*head, *tail], :views)
+
+ 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
@@ -72,7 +80,7 @@ module AbstractController
def write_fragment(key, content, options = nil)
return content unless cache_configured?
- key = fragment_cache_key(key)
+ key = combined_fragment_cache_key(key)
instrument_fragment_cache :write_fragment, key do
content = content.to_str
cache_store.write(key, content, options)
@@ -85,7 +93,7 @@ module AbstractController
def read_fragment(key, options = nil)
return unless cache_configured?
- key = fragment_cache_key(key)
+ 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
@@ -96,7 +104,7 @@ module AbstractController
# +key+ exists (see +expire_fragment+ for acceptable formats).
def fragment_exist?(key, options = nil)
return unless cache_configured?
- key = fragment_cache_key(key)
+ key = combined_fragment_cache_key(key)
instrument_fragment_cache :exist_fragment?, key do
cache_store.exist?(key, options)
@@ -123,7 +131,7 @@ module AbstractController
# 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)
+ key = combined_fragment_cache_key(key) unless key.is_a?(Regexp)
instrument_fragment_cache :expire_fragment, key do
if key.is_a?(Regexp)
@@ -135,8 +143,7 @@ module AbstractController
end
def instrument_fragment_cache(name, key) # :nodoc:
- payload = instrument_payload(key)
- ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", payload) { yield }
+ ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key)) { yield }
end
end
end
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb
index d63ce9c1c3..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
@@ -49,30 +71,11 @@ module AbstractController
def _normalize_callback_option(options, from, to) # :nodoc:
if from = options[from]
_from = Array(from).map(&:to_s).to_set
- from = proc {|c| _from.include? c.action_name }
+ 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 Rails 5.1. 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.
@@ -100,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
@@ -107,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
@@ -121,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
@@ -187,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|
@@ -211,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 55654be224..d4a078ab32 100644
--- a/actionpack/lib/abstract_controller/collector.rb
+++ b/actionpack/lib/abstract_controller/collector.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "action_dispatch/http/mime_type"
module AbstractController
@@ -16,15 +18,15 @@ 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)
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 { ... } }"
diff --git a/actionpack/lib/abstract_controller/error.rb b/actionpack/lib/abstract_controller/error.rb
index 7fafce4dd4..89a54f072e 100644
--- a/actionpack/lib/abstract_controller/error.rb
+++ b/actionpack/lib/abstract_controller/error.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AbstractController
class Error < StandardError #:nodoc:
end
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb
index ab4355296b..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
@@ -171,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$/, ''.freeze)
- 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 9f192c54f7..8ba2b25552 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -1,9 +1,9 @@
-require 'abstract_controller/error'
-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
@@ -20,7 +20,6 @@ module AbstractController
# Normalizes arguments, options and then delegates render_to_body and
# sticks the result in <tt>self.response_body</tt>.
- # :api: public
def render(*args, &block)
options = _normalize_render(*args, &block)
rendered_body = render_to_body(options)
@@ -42,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]
end
DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %i(
- @_action_name @_response_body @_formats @_prefixes @_config
- @_view_context_class @_view_renderer @_lookup_context
- @_routes @_db_runtime
+ @_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
@@ -78,11 +71,11 @@ module AbstractController
}
end
+ private
# Normalize args by converting <tt>render "foo"</tt> to
# <tt>render :action => "foo"</tt> and <tt>render "foo/bar"</tt> to
# <tt>render :file => "foo/bar"</tt>.
- # :api: plugin
- def _normalize_args(action=nil, options={})
+ def _normalize_args(action = nil, options = {}) # :doc:
if action.respond_to?(:permitted?)
if action.permitted?
action
@@ -97,20 +90,20 @@ module AbstractController
end
# Normalize options.
- # :api: plugin
- def _normalize_options(options)
+ def _normalize_options(options) # :doc:
options
end
# Process extra options.
- # :api: plugin
- def _process_options(options)
+ def _process_options(options) # :doc:
options
end
# Process the rendered format.
- # :api: private
- def _process_format(format)
+ def _process_format(format) # :nodoc:
+ end
+
+ def _process_variant(options)
end
def _set_html_content_type # :nodoc:
@@ -120,13 +113,9 @@ module AbstractController
end
# Normalize args and options.
- # :api: private
- def _normalize_render(*args, &block)
+ def _normalize_render(*args, &block) # :nodoc:
options = _normalize_args(*args, &block)
- #TODO: remove defined? when we restore AP <=> AV dependency
- if defined?(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..4dad2a2b93 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,12 @@ 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('/', '.')
+ options = options.dup
+ 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 62f5905205..29d61c3ceb 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -1,8 +1,10 @@
-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
@@ -20,9 +22,12 @@ module ActionController
autoload_under "metal" do
autoload :ConditionalGet
+ autoload :ContentSecurityPolicy
autoload :Cookies
autoload :DataStreaming
+ autoload :DefaultHeaders
autoload :EtagWithTemplateDigest
+ autoload :EtagWithFlash
autoload :Flash
autoload :ForceSSL
autoload :Head
@@ -40,6 +45,7 @@ module ActionController
autoload :Rescue
autoload :Streaming
autoload :StrongParameters
+ autoload :ParameterEncoding
autoload :Testing
autoload :UrlFor
end
@@ -48,14 +54,14 @@ module ActionController
autoload :ApiRendering
end
- autoload :TestCase, 'action_controller/test_case'
- autoload :TemplateAssertions, 'action_controller/test_case'
+ 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 ff12705abe..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,18 +74,18 @@ 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!
@@ -120,6 +122,7 @@ module ActionController
ForceSSL,
DataStreaming,
+ DefaultHeaders,
# Before callbacks should also be executed as early as possible, so
# also include them at the bottom.
@@ -141,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
index 3a08d28c39..aca5265313 100644
--- a/actionpack/lib/action_controller/api/api_rendering.rb
+++ b/actionpack/lib/action_controller/api/api_rendering.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
module ApiRendering
extend ActiveSupport::Concern
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 04e5922ce8..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+:
#
@@ -213,23 +215,26 @@ module ActionController
Renderers::All,
ConditionalGet,
EtagWithTemplateDigest,
+ 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,
@@ -251,14 +256,16 @@ module ActionController
setup_renderer!
# Define some internal variables that should not be propagated to the view.
- PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [
- :@_params, :@_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
+ 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 a9a8508abc..bf3b00a7b7 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
# \Caching is a cheap way of speeding up slow applications by keeping the result of
# calculations, renderings, and database calls around for subsequent requests.
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 a0917b4fdb..d8b04d8ddb 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,19 @@ 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(" | ".freeze)})" unless additions.empty?
+
+ 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
@@ -51,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(", ")}"
+ color("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.map { |e| ":#{e}" }.join(", ")}", RED)
end
end
@@ -59,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 f6e67b02d7..b9088e6d86 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -1,7 +1,9 @@
-require 'active_support/core_ext/array/extract_options'
-require 'action_dispatch/middleware/stack'
-require 'action_dispatch/http/request'
-require 'action_dispatch/http/response'
+# 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
@@ -24,39 +26,39 @@ module ActionController
end
end
- def build(action, app = Proc.new)
+ def build(action, app = nil, &block)
action = action.to_s
- middlewares.reverse.inject(app) do |a, middleware|
+ middlewares.reverse.inject(app || block) do |a, middleware|
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
+ 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(get_class(klass), args, list, strategy, block)
- end
+ Middleware.new(klass, args, list, strategy, block)
+ end
end
# <tt>ActionController::Metal</tt> is the simplest possible controller, providing a
@@ -118,11 +120,6 @@ module ActionController
class Metal < AbstractController::Base
abstract!
- def env
- @_request.env
- end
- deprecate :env
-
# Returns the last part of the controller's name, underscored, without the ending
# <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>.
# Namespaces are left out, so Admin::PostsController returns <tt>posts</tt> as well.
@@ -130,24 +127,28 @@ 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.create.tap do |res|
+ ActionDispatch::Response.new.tap do |res|
res.request = request
end
end
- # Delegates to the class' <tt>controller_name</tt>
+ def self.binary_params_for?(action) # :nodoc:
+ false
+ end
+
+ # Delegates to the class' <tt>controller_name</tt>.
def controller_name
self.class.controller_name
end
attr_internal :response, :request
- delegate :session, :to => "@_request"
+ delegate :session, to: "@_request"
delegate :headers, :status=, :location=, :content_type=,
- :status, :location, :content_type, :to => "@_response"
+ :status, :location, :content_type, to: "@_response"
def initialize
@_request = nil
@@ -209,8 +210,7 @@ module ActionController
@_request.reset_session
end
- class_attribute :middleware_stack
- self.middleware_stack = ActionController::MiddlewareStack.new
+ class_attribute :middleware_stack, default: ActionController::MiddlewareStack.new
def self.inherited(base) # :nodoc:
base.middleware_stack = middleware_stack.dup
@@ -228,32 +228,22 @@ module ActionController
middleware_stack
end
- # Makes the controller a Rack endpoint that runs the action in the given
- # +env+'s +action_dispatch.request.path_parameters+ key.
- def self.call(env)
- req = ActionDispatch::Request.new env
- action(req.path_parameters[:action]).call(env)
- end
- class << self; deprecate :call; end
-
# Returns a Rack endpoint for the given action name.
def self.action(name)
+ app = lambda { |env|
+ req = ActionDispatch::Request.new(env)
+ res = make_response! req
+ new.dispatch(name, req, res)
+ }
+
if middleware_stack.any?
- middleware_stack.build(name) do |env|
- req = ActionDispatch::Request.new(env)
- res = make_response! req
- new.dispatch(name, req, res)
- end
+ middleware_stack.build(name, app)
else
- lambda { |env|
- req = ActionDispatch::Request.new(env)
- res = make_response! req
- new.dispatch(name, req, res)
- }
+ app
end
end
- # Direct dispatch to the controller. Instantiates the controller, then
+ # Direct dispatch to the controller. Instantiates the controller, then
# executes the action named +name+.
def self.dispatch(name, req, res)
if middleware_stack.any?
diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb
index cef65a362c..f9a758ff0e 100644
--- a/actionpack/lib/action_controller/metal/basic_implicit_render.rb
+++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module ActionController
module BasicImplicitRender # :nodoc:
def send_action(method, *args)
super.tap { default_render unless performed? }
end
- def default_render(*args)
+ def default_render
head :no_content
end
end
diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb
index 480e265e44..29d1919ec5 100644
--- a/actionpack/lib/action_controller/metal/conditional_get.rb
+++ b/actionpack/lib/action_controller/metal/conditional_get.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
module ActionController
module ConditionalGet
@@ -7,8 +7,7 @@ module ActionController
include Head
included do
- class_attribute :etaggers
- self.etaggers = []
+ class_attribute :etaggers, default: []
end
module ClassMethods
@@ -129,7 +128,7 @@ module ActionController
# * <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
+ # 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
@@ -227,25 +226,34 @@ 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.
+ #
+ # 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 an HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should
- # occur by the browser or intermediate caches (like caching proxy servers).
+ # 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.
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 f8efb2b076..ff46966693 100644
--- a/actionpack/lib/action_controller/metal/cookies.rb
+++ b/actionpack/lib/action_controller/metal/cookies.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module ActionController #:nodoc:
module Cookies
extend ActiveSupport::Concern
included do
- helper_method :cookies
+ helper_method :cookies if defined?(helper_method)
end
private
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
index 957e7a3019..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,17 +57,17 @@ 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
@@ -79,14 +81,14 @@ module ActionController #:nodoc:
# <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.
@@ -109,11 +111,13 @@ module ActionController #:nodoc:
render options.slice(:status, :content_type).merge(body: data)
end
- private
def send_file_headers!(options)
type_provided = options.has_key?(:type)
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)
@@ -123,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 669cf55bca..2f1544c69c 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,8 +24,7 @@ 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
etag do |options|
@@ -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, format: nil, 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 5c0ada37be..e1e0c6f456 100644
--- a/actionpack/lib/action_controller/metal/exceptions.rb
+++ b/actionpack/lib/action_controller/metal/exceptions.rb
@@ -1,22 +1,14 @@
+# frozen_string_literal: true
+
module ActionController
class ActionControllerError < StandardError #:nodoc:
end
class BadRequest < ActionControllerError #:nodoc:
- def initialize(msg = nil, e = nil)
- if e
- ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \
- "Exceptions will automatically capture the original exception.", caller)
- end
-
+ def initialize(msg = nil)
super(msg)
set_backtrace $!.backtrace if $!
end
-
- def original_exception
- ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller)
- cause
- end
end
class RenderError < ActionControllerError #:nodoc:
@@ -24,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)
@@ -61,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 example:
+ #
+ # 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..a4861dc2c0 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,26 +36,26 @@ 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
- def redirect_to(options = {}, response_status_and_flash = {}) #:doc:
+ private
+ def redirect_to(options = {}, response_options_and_flash = {}) #:doc:
self.class._flash_types.each do |flash_type|
- if type = response_status_and_flash.delete(flash_type)
+ if type = response_options_and_flash.delete(flash_type)
flash[flash_type] = type
end
end
- if other_flashes = response_status_and_flash.delete(:flash)
+ if other_flashes = response_options_and_flash.delete(:flash)
flash.update(other_flashes)
end
- super(options, response_status_and_flash)
+ super(options, response_options_and_flash)
end
end
end
diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb
index e31d65aac2..93fd57b640 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 5e9832fd4e..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,13 +20,7 @@ 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
@@ -33,7 +29,7 @@ module ActionController
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,24 +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 include_content?(response_code)
+ self.content_type = content_type || (Mime[formats.first] if formats) || Mime[:html]
+ response.charset = false
end
true
end
private
- 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 d3853e2e83..193b488f6c 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,7 +7,7 @@ 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 which
# matches the name of the controller, e.g., <tt>MyController</tt> will automatically
@@ -32,7 +34,7 @@ module ActionController
# end
# end
#
- # Then, in any view rendered by <tt>EventController</tt>, the <tt>format_time</tt> method can be called:
+ # Then, in any view rendered by <tt>EventsController</tt>, the <tt>format_time</tt> method can be called:
#
# <% @events.each do |event| -%>
# <p>
@@ -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,10 +72,10 @@ 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
- proxy = ActionView::Base.new
+ proxy = ActionView::Base.empty
proxy.config = config.inheritable_copy
proxy.extend(_helpers)
end
@@ -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'.freeze) }
+ 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 35be6d9300..6a274d35cb 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -1,5 +1,7 @@
-require 'base64'
-require 'active_support/security_utils'
+# 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.
@@ -28,7 +30,7 @@ module ActionController
# class ApplicationController < ActionController::Base
# before_action :set_account, :authenticate
#
- # protected
+ # private
# def set_account
# @account = Account.find_by(url_name: request.subdomains.first)
# end
@@ -54,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
@@ -66,21 +69,20 @@ module ActionController
extend ActiveSupport::Concern
module ClassMethods
- 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|
- # This comparison uses & so that it doesn't short circuit and
- # uses `variable_size_secure_compare` so that length information
- # isn't leaked.
- ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) &
- ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password])
- end
- end
+ def http_basic_authenticate_with(name:, password:, realm: nil, **options)
+ before_action(options) { http_basic_authenticate_or_request_with name: name, password: password, realm: realm }
+ end
+ end
+
+ def http_basic_authenticate_or_request_with(name:, password:, realm: nil, message: nil)
+ authenticate_or_request_with_http_basic(realm, message) do |given_name, given_password|
+ ActiveSupport::SecurityUtils.secure_compare(given_name, name) &
+ ActiveSupport::SecurityUtils.secure_compare(given_password, password)
end
end
- def authenticate_or_request_with_http_basic(realm = "Application", message = nil, &login_procedure)
- authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm, message)
+ def authenticate_or_request_with_http_basic(realm = nil, message = nil, &login_procedure)
+ authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm || "Application", message)
end
def authenticate_with_http_basic(&login_procedure)
@@ -99,23 +101,23 @@ module ActionController
end
def has_basic_credentials?(request)
- request.authorization.present? && (auth_scheme(request).downcase == '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)
@@ -124,7 +126,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
@@ -208,7 +210,7 @@ module ActionController
password = password_procedure.call(credentials[:username])
return false unless password
- method = request.get_header('rack.methodoverride.original_method') || request.get_header('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|
@@ -224,19 +226,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)
@@ -244,9 +246,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
@@ -310,11 +312,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
@@ -324,7 +326,6 @@ module ActionController
def opaque(secret_key)
::Digest::MD5.hexdigest(secret_key)
end
-
end
# Makes it dead easy to do HTTP Token authentication.
@@ -347,7 +348,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
@@ -359,7 +362,7 @@ module ActionController
# class ApplicationController < ActionController::Base
# before_action :set_account, :authenticate
#
- # protected
+ # private
# def set_account
# @account = Account.find_by(url_name: request.subdomains.first)
# end
@@ -386,10 +389,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
@@ -401,7 +403,7 @@ module ActionController
#
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
module Token
- TOKEN_KEY = 'token='
+ TOKEN_KEY = "token="
TOKEN_REGEX = /^(Token|Bearer)\s+/
AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/
extend self
@@ -441,7 +443,7 @@ module ActionController
end
end
- # Parses the token and options out of the token authorization header.
+ # Parses the token and options out of the token Authorization header.
# The value for the Authorization header is expected to have the prefix
# <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this:
# Authorization: Token token="abc", nonce="def"
@@ -471,14 +473,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}"
@@ -508,7 +510,7 @@ 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.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"', "")}")
controller.__send__ :render, plain: message, status: :unauthorized
end
end
diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb
index 6192fc0f9c..8365ddca57 100644
--- a/actionpack/lib/action_controller/metal/implicit_render.rb
+++ b/actionpack/lib/action_controller/metal/implicit_render.rb
@@ -1,14 +1,14 @@
-require 'active_support/core_ext/string/strip'
+# 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 204 No Content.
+ # For API controllers, the implicit response is always <tt>204 No Content</tt>.
#
# For all other controllers, we use these heuristics to decide whether to
# render a template, raise an error for a missing template, or respond with
- # 204 No Content:
+ # <tt>204 No Content</tt>:
#
# First, if we DO find a template, it's rendered. Template lookup accounts
# for the action name, locales, format, variant, template handlers, and more
@@ -25,15 +25,14 @@ module ActionController
# <tt>ActionView::UnknownFormat</tt> with an explanation.
#
# Finally, if we DON'T find a template AND the request isn't a browser page
- # load, then we implicitly respond with 204 No Content.
+ # load, then we implicitly respond with <tt>204 No Content</tt>.
module ImplicitRender
-
# :stopdoc:
include BasicImplicitRender
- def default_render(*args)
+ def default_render
if template_exists?(action_name.to_s, _prefixes, variants: request.variant)
- render(*args)
+ render
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" \
@@ -42,18 +41,8 @@ module ActionController
raise ActionController::UnknownFormat, message
elsif interactive_browser_request?
- message = "#{self.class.name}\##{action_name} is missing a template " \
- "for this request format and variant.\n\n" \
- "request.formats: #{request.formats.map(&:to_s).inspect}\n" \
- "request.variant: #{request.variant.inspect}\n\n" \
- "NOTE! For XHR/Ajax or API requests, this action would normally " \
- "respond with 204 No Content: an empty white screen. Since you're " \
- "loading it in a web browser, we assume that you expected to " \
- "actually render a template, not… nothing, so we're showing an " \
- "error to be extra-clear. If you expect 204 No Content, carry on. " \
- "That's what you'll get from an XHR or API request. Give it a shot."
-
- raise ActionController::UnknownFormat, message
+ message = "#{self.class.name}\##{action_name} is missing a template for request formats: #{request.formats.map(&:to_s).join(',')}"
+ raise ActionController::MissingExactTemplate, message
else
logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
super
@@ -62,8 +51,8 @@ module ActionController
def method_for_action(action_name)
super || if template_exists?(action_name.to_s, _prefixes)
- "default_render"
- end
+ "default_render"
+ end
end
private
diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb
index 885ea3fefd..51fac08749 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.
@@ -16,25 +18,23 @@ module ActionController
def process_action(*args)
raw_payload = {
- :controller => self.class.name,
- :action => self.action_name,
- :params => request.filtered_parameters,
- :headers => request.headers,
- :format => request.format.ref,
- :method => request.request_method,
- :path => request.fullpath
+ 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)
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
- begin
- result = super
+ super.tap do
payload[:status] = response.status
- result
- ensure
- append_info_to_payload(payload)
end
+ ensure
+ append_info_to_payload(payload)
end
end
@@ -46,9 +46,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 +72,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 +95,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 fc20e7a421..dd69930e25 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
@@ -84,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
@@ -110,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
@@ -145,7 +146,7 @@ module ActionController
def write(string)
unless @response.committed?
- @response.set_header "Cache-Control", "no-cache"
+ @response.headers["Cache-Control"] ||= "no-cache"
@response.delete_header "Content-Length"
end
@@ -163,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.
#
@@ -210,23 +203,36 @@ module ActionController
def call_on_error
@error_callback.call
end
+
+ private
+
+ 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
+ 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_committed
+ super
+ jar = request.cookie_jar
+ # The response can be committed multiple times
+ jar.write self unless committed?
+ end
- def build_buffer(response, body)
- buf = Live::Buffer.new response
- body.each { |part| buf.write part }
- buf
- end
+ def build_buffer(response, body)
+ buf = Live::Buffer.new response
+ body.each { |part| buf.write part }
+ buf
+ end
end
def process(name)
@@ -235,15 +241,15 @@ module ActionController
error = nil
# This processes the action in a child thread. It lets us return the
- # response code and headers back up the rack stack, and still process
- # the body in parallel with sending data to the client
+ # response code and headers back up the Rack stack, and still process
+ # the body in parallel with sending data to the client.
new_controller_thread {
ActiveSupport::Dependencies.interlock.running do
t2 = Thread.current
# 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 }
+ locals.each { |k, v| t2[k] = v }
begin
super(name)
@@ -274,33 +280,35 @@ module ActionController
raise error if error
end
- # 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
- }
+ def response_body=(body)
+ super
+ response.close if response
end
- def log_error(exception)
- logger = ActionController::Base.logger
- return unless logger
+ private
- 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"
+ # 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
- end
- def response_body=(body)
- super
- response.close if response
- end
+ def log_error(exception)
+ logger = ActionController::Base.logger
+ return unless logger
+
+ logger.fatal do
+ message = +"\n#{exception.class} (#{exception.message}):\n"
+ message << exception.annotated_source_code.to_s if exception.respond_to?(:annotated_source_code)
+ message << " " << exception.backtrace.join("\n ")
+ "#{message}\n\n"
+ 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 2e89af1a5e..bf5e7a433f 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,7 +11,7 @@ module ActionController #:nodoc:
# @people = Person.all
# end
#
- # That action implicitly responds to all formats, but formats can also be whitelisted:
+ # That action implicitly responds to all formats, but formats can also be explicitly enumerated:
#
# def index
# @people = Person.all
@@ -103,7 +105,7 @@ module ActionController #:nodoc:
#
# 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
@@ -122,6 +124,14 @@ module ActionController #:nodoc:
#
# render json: @people
#
+ # +any+ can also be used with no arguments, in which case it will be used for any format requested by
+ # the user:
+ #
+ # respond_to do |format|
+ # format.html
+ # format.any { redirect_to support_path }
+ # end
+ #
# Formats can have different variants.
#
# The request variant is a specialization of the request format, like <tt>:tablet</tt>,
@@ -181,8 +191,8 @@ module ActionController #:nodoc:
#
# request.variant = [:tablet, :phone]
#
- # which will work similarly to formats and MIME types negotiation. If there will be no
- # +:tablet+ variant declared, +:phone+ variant will be picked:
+ # This will work similarly to formats and MIME types negotiation. If there
+ # is no +:tablet+ variant declared, the +:phone+ variant will be used:
#
# respond_to do |format|
# format.html.none
@@ -195,6 +205,9 @@ 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
@@ -280,8 +293,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 c38fc40b81..09716f7588 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -1,7 +1,9 @@
-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
@@ -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,26 +238,10 @@ 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_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
- request.parameters.merge! wrapped_hash
- request.request_parameters.merge! wrapped_hash
-
- # This will display the wrapped hash in the log file
- request.filtered_parameters.merge! wrapped_filtered_hash
- end
+ _perform_parameter_wrapping if _wrapper_enabled?
super
end
@@ -279,7 +276,22 @@ module ActionController
return false unless request.has_content_type?
ref = request.content_mime_type.ref
- _wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key]
+ _wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key)
+ end
+
+ def _perform_parameter_wrapping
+ 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.
+ request.parameters.merge! wrapped_hash
+ request.request_parameters.merge! wrapped_hash
+
+ # This will display the wrapped hash in the log file.
+ request.filtered_parameters.merge! wrapped_filtered_hash
+ rescue ActionDispatch::Http::Parameters::ParseError
+ # swallow parse error exception
end
end
end
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
index 3c7cc15627..67c198d150 100644
--- a/actionpack/lib/action_controller/metal/redirecting.rb
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -1,12 +1,6 @@
-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
@@ -24,27 +18,27 @@ module ActionController
# === 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 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
@@ -58,39 +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")
#
- 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_options = {})
raise ActionControllerError.new("Cannot redirect to nil!") unless options
raise AbstractController::DoubleRenderError if response_body
- self.status = _extract_redirect_to_status(options, response_status)
+ self.status = _extract_redirect_to_status(options, response_options)
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 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: @post
# redirect_back fallback_location: "http://www.rubyonrails.org"
- # redirect_back fallback_location: "/images/screenshot.jpg"
- # redirect_back fallback_location: articles_url
- # redirect_back fallback_location: proc { edit_post_url(@post) }
+ # redirect_back fallback_location: "/images/screenshot.jpg"
+ # redirect_back fallback_location: posts_url
+ # redirect_back fallback_location: proc { edit_post_url(@post) }
+ # redirect_back fallback_location: '/', allow_other_host: false
#
- # All options that can be passed to <tt>redirect_to</tt> are accepted as
+ # ==== Options
+ # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header.
+ # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true.
+ #
+ # All other options that can be passed to <tt>redirect_to</tt> are accepted as
# options and the behavior is identical.
- def redirect_back(fallback_location:, **args)
- if referer = request.headers["Referer"]
- redirect_to referer, **args
- else
- redirect_to fallback_location, **args
- end
+ def redirect_back(fallback_location:, allow_other_host: true, **args)
+ referer = request.headers["Referer"]
+ redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
+ redirect_to redirect_to_referer ? referer : fallback_location, **args
end
def _compute_redirect_to_location(request, options) #:nodoc:
@@ -98,22 +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
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- `redirect_to :back` is deprecated and will be removed from Rails 5.1.
- Please use `redirect_back(fallback_location: fallback_location)` where
- `fallback_location` represents the location to use if the request has
- no HTTP referer information.
- MESSAGE
- 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")
@@ -122,14 +114,20 @@ module ActionController
public :_compute_redirect_to_location
private
- def _extract_redirect_to_status(options, response_status)
+ def _extract_redirect_to_status(options, response_options)
if options.is_a?(Hash) && options.key?(:status)
Rack::Utils.status_code(options.delete(:status))
- elsif response_status.key?(:status)
- Rack::Utils.status_code(response_status[:status])
+ elsif response_options.key?(:status)
+ Rack::Utils.status_code(response_options[:status])
else
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 90fb34e386..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>
@@ -26,8 +28,7 @@ module ActionController
RENDERERS = Set.new
included do
- class_attribute :_renderers
- self._renderers = Set.new.freeze
+ class_attribute :_renderers, default: Set.new.freeze
end
# Used in <tt>ActionController::Base</tt>
@@ -71,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
@@ -86,7 +85,7 @@ 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
def self._render_with_renderer_method_name(key)
@@ -94,7 +93,6 @@ module ActionController
end
module ClassMethods
-
# Adds, by name, a renderer or renderers to the +_renderers+ available
# to call within controller actions.
#
@@ -103,11 +101,11 @@ module ActionController
#
# Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt>
# include <tt>ActionController::Renderers::All</tt>, making all renderers
- # avaialable in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>.
+ # 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 lest one renderer.
+ # and <tt>ActionController::Renderers</tt>, and have at least one renderer.
#
# Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers,
# you may specify which renderers to include by passing the renderer name or names to
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index cce6fe7787..7d0a944381 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -1,10 +1,10 @@
-require 'active_support/core_ext/string/filters'
+# 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
@@ -32,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
@@ -49,84 +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 _set_html_content_type
- self.content_type = Mime[:html].to_s
- end
+ def _render_in_priorities(options)
+ RENDER_FORMATS_IN_PRIORITY.each do |format|
+ return options[format] if options.key?(format)
+ end
- def _set_rendered_content_type(format)
- unless response.content_type
- 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[:text]
- ActiveSupport::Deprecation.warn <<-WARNING.squish
- `render :text` is deprecated because it does not actually render a
- `text/plain` response. Switch to `render plain: 'plain text'` to
- render as `text/plain`, `render html: '<strong>HTML</strong>'` to
- render as `text/html`, or `render body: 'raw'` to match the deprecated
- behavior and render with the default Content-Type, which is
- `text/plain`.
- WARNING
+ def _set_html_content_type
+ self.content_type = Mime[:html].to_s
end
- if options[:html]
- options[:html] = ERB::Util.html_escape(options[:html])
+ def _set_rendered_content_type(format)
+ if format && !response.content_type
+ self.content_type = format.to_s
+ end
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
+ # 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
- if options[:status]
- options[:status] = Rack::Utils.status_code(options[:status])
- end
+ # Normalize both text and status options.
+ def _normalize_options(options)
+ _normalize_text(options)
- super
- end
+ if options[:html]
+ options[:html] = ERB::Util.html_escape(options[:html])
+ end
- 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[: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 b2f0b382b9..4bf8d90b69 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_from_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
@@ -85,6 +94,10 @@ module ActionController #:nodoc:
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
@@ -109,10 +122,10 @@ module ActionController #:nodoc:
# * <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 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).
+ # 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>.
+ # 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:
@@ -128,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
@@ -152,28 +174,28 @@ module ActionController #:nodoc:
request.cookie_jar = NullCookieJar.build(request, {})
end
- protected
+ private
- class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
- def initialize(req)
- super(nil, req)
- @data = {}
- @loaded = true
- end
+ class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
+ def initialize(req)
+ super(nil, req)
+ @data = {}
+ @loaded = true
+ end
- # no-op
- def destroy; end
+ # no-op
+ def destroy; end
- def exists?
- true
+ def exists?
+ true
+ end
end
- end
- class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
- def write(*)
- # nothing
+ class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
+ def write(*)
+ # nothing
+ end
end
- end
end
class ResetSession
@@ -197,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
@@ -229,54 +255,57 @@ 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_request_origin? && any_authenticity_token_valid?)
end
# Checks if any of the authenticity tokens from the request are valid.
- def any_authenticity_token_valid?
+ def any_authenticity_token_valid? # :doc:
request_authenticity_tokens.any? do |token|
valid_authenticity_token?(session, token)
end
end
# Possible authenticity tokens sent in the request.
- def request_authenticity_tokens
+ def request_authenticity_tokens # :doc:
[form_authenticity_param, request.x_csrf_token]
end
@@ -288,7 +317,7 @@ module ActionController #:nodoc:
# Creates a masked version of the authenticity token that varies
# on each request. The masking is used to mitigate SSL attacks
# like BREACH.
- def masked_authenticity_token(session, form_options: {})
+ def masked_authenticity_token(session, form_options: {}) # :doc:
action, method = form_options.values_at(:action, :method)
raw_token = if per_form_csrf_tokens && action && method
@@ -307,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
@@ -325,7 +354,7 @@ module ActionController #:nodoc:
if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
# This is actually an unmasked token. This is expected if
# you have just upgraded to masked tokens, but should stop
- # happening shortly after installing this gem
+ # happening shortly after installing this gem.
compare_with_real_token masked_token, session
elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
@@ -334,23 +363,23 @@ module ActionController #:nodoc:
compare_with_real_token(csrf_token, session) ||
valid_per_form_csrf_token?(csrf_token, session)
else
- false # Token is malformed
+ false # Token is malformed.
end
end
- def unmask_token(masked_token)
+ def unmask_token(masked_token) # :doc:
# Split the token into the one-time pad and the encrypted
- # value and decrypt it
+ # value and decrypt it.
one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
xor_byte_strings(one_time_pad, encrypted_csrf_token)
end
- def compare_with_real_token(token, session)
- ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
+ 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)
+ def valid_per_form_csrf_token?(token, session) # :doc:
if per_form_csrf_tokens
correct_token = per_form_csrf_token(
session,
@@ -358,18 +387,18 @@ module ActionController #:nodoc:
request.request_method
)
- ActiveSupport::SecurityUtils.secure_compare(token, correct_token)
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
else
false
end
end
- def real_csrf_token(session)
+ def real_csrf_token(session) # :doc:
session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
Base64.strict_decode64(session[:_csrf_token])
end
- def per_form_csrf_token(session, action_path, method)
+ def per_form_csrf_token(session, action_path, method) # :doc:
OpenSSL::HMAC.digest(
OpenSSL::Digest::SHA256.new,
real_csrf_token(session),
@@ -377,35 +406,51 @@ module ActionController #:nodoc:
)
end
- def xor_byte_strings(s1, s2)
- s2_bytes = s2.bytes
- s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 }
- s2_bytes.pack('C*')
+ 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-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?
+ 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)
- action_path.split('?').first.to_s.chomp('/')
+ 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 0621a7368c..44f7fb7a07 100644
--- a/actionpack/lib/action_controller/metal/rescue.rb
+++ b/actionpack/lib/action_controller/metal/rescue.rb
@@ -1,27 +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.cause
- handler_index = index_of_handler_for_rescue(exception) || Float::INFINITY
- cause_handler_index = index_of_handler_for_rescue(exception.cause)
- if cause_handler_index && cause_handler_index <= handler_index
- exception = exception.cause
- end
- end
- super(exception)
- end
-
# 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
@@ -30,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 a6115674aa..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,10 +195,10 @@ module ActionController #:nodoc:
module Streaming
extend ActiveSupport::Concern
- protected
+ private
# Set proper cache control and transfer encoding when streaming
- def _process_options(options) #:nodoc:
+ def _process_options(options)
super
if options[:stream]
if request.version == "HTTP/1.0"
@@ -210,7 +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 64672de57e..815f82a1f2 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -1,12 +1,15 @@
-require 'active_support/core_ext/hash/indifferent_access'
-require 'active_support/core_ext/hash/transform_values'
-require 'active_support/core_ext/array/wrap'
-require 'active_support/core_ext/string/filters'
-require 'active_support/rescuable'
-require 'action_dispatch/http/upload'
-require 'rack/test'
-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.
@@ -31,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
@@ -51,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)
@@ -70,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.
#
@@ -87,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
#
@@ -102,15 +116,104 @@ module ActionController
# 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
- cattr_accessor :permit_all_parameters, instance_accessor: false
+ 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: :@parameters
+ :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'
@@ -119,8 +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 )
+ 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
@@ -129,13 +231,13 @@ 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(parameters = {})
@@ -147,29 +249,21 @@ module ActionController
# permitted flag.
def ==(other)
if other.respond_to?(:permitted?)
- self.permitted? == other.permitted? && self.parameters == other.parameters
- elsif other.is_a?(Hash)
- ActiveSupport::Deprecation.warn <<-WARNING.squish
- Comparing equality between `ActionController::Parameters` and a
- `Hash` is deprecated and will be removed in Rails 5.1. Please only do
- comparisons between instances of `ActionController::Parameters`. If
- you need to compare to a hash, first convert it using
- `ActionController::Parameters#new`.
- WARNING
- @parameters == other.with_indifferent_access
+ permitted? == other.permitted? && parameters == other.parameters
else
@parameters == other
end
end
# Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt>
- # representation of this parameter with all unpermitted keys removed.
+ # 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"}
@@ -177,17 +271,66 @@ module ActionController
if permitted?
convert_parameters_to_hashes(@parameters, :to_h)
else
- slice(*self.class.always_permitted_parameters).permit!.to_h
+ raise UnfilteredParameters
end
end
+ # 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 this
- # parameter.
+ # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of the
+ # parameters.
#
# params = ActionController::Parameters.new({
- # name: 'Senjougahara Hitagi',
- # oddity: 'Heavy stone crab'
+ # name: "Senjougahara Hitagi",
+ # oddity: "Heavy stone crab"
# })
# params.to_unsafe_h
# # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"}
@@ -196,15 +339,23 @@ module ActionController
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)
@parameters.each_pair do |key, value|
- yield key, convert_hashes_to_parameters(key, value)
+ yield [key, convert_hashes_to_parameters(key, value)]
end
end
alias_method :each, :each_pair
+ # 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.
@@ -232,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!
@@ -240,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
@@ -254,8 +405,8 @@ module ActionController
# 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)
- # # => {"name"=>"Francesco"}
+ # ActionController::Parameters.new(person: { name: "Francesco" }).require(:person)
+ # # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false>
#
# Otherwise raises <tt>ActionController::ParameterMissing</tt>:
#
@@ -276,18 +427,18 @@ module ActionController
# returned:
#
# params = ActionController::Parameters.new(user: { ... }, profile: { ... })
- # user_params, profile_params = params.require(:user, :profile)
+ # user_params, profile_params = params.require([:user, :profile])
#
- # Otherwise, the method reraises the first exception found:
+ # Otherwise, the method re-raises the first exception found:
#
# params = ActionController::Parameters.new(user: {}, profile: {})
- # user_params, profile_params = params.require(: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' })
+ # 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:
@@ -317,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
@@ -337,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"
# }]
# }
# })
@@ -362,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
@@ -388,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
@@ -401,8 +561,8 @@ 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, @parameters[key])
@@ -417,14 +577,16 @@ module ActionController
# 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_value_to_parameters(
@parameters.fetch(key) {
@@ -437,19 +599,18 @@ module ActionController
)
end
- if Hash.method_defined?(:dig)
- # Extracts the nested parameter from the given +keys+ by calling +dig+
- # at each step. Returns +nil+ if any intermediate step is +nil+.
- #
- # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } })
- # params.dig(:foo, :bar, :baz) # => 1
- # params.dig(:foo, :zot, :xyz) # => nil
- #
- # params2 = ActionController::Parameters.new(foo: [10, 11, 12])
- # params2.dig(:foo, 1) # => 11
- def dig(*keys)
- convert_value_to_parameters(@parameters.dig(*keys))
- end
+ # Extracts the nested parameter from the given +keys+ by calling +dig+
+ # at each step. Returns +nil+ if any intermediate step is +nil+.
+ #
+ # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } })
+ # params.dig(:foo, :bar, :baz) # => 1
+ # params.dig(:foo, :zot, :xyz) # => nil
+ #
+ # params2 = ActionController::Parameters.new(foo: [10, 11, 12])
+ # params2.dig(:foo, 1) # => 11
+ def dig(*keys)
+ convert_hashes_to_parameters(keys.first, @parameters[keys.first])
+ @parameters.dig(*keys)
end
# Returns a new <tt>ActionController::Parameters</tt> instance that
@@ -457,8 +618,8 @@ 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(@parameters.slice(*keys))
end
@@ -474,8 +635,8 @@ module ActionController
# filters out the given +keys+.
#
# params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
- # params.except(:a, :b) # => {"c"=>3}
- # params.except(:d) # => {"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
@@ -483,8 +644,8 @@ module ActionController
# 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(@parameters.extract!(*keys))
end
@@ -494,21 +655,19 @@ module ActionController
#
# params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
# params.transform_values { |x| x * 2 }
- # # => {"a"=>2, "b"=>4, "c"=>6}
- def transform_values(&block)
- if block
- new_instance_with_inherited_permitted_status(
- @parameters.transform_values(&block)
- )
- else
- @parameters.transform_values
- end
+ # # => <ActionController::Parameters {"a"=>2, "b"=>4, "c"=>6} permitted: false>
+ def transform_values
+ 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!(&block)
- @parameters.transform_values!(&block)
+ def transform_values!
+ return to_enum(:transform_values!) unless block_given?
+ @parameters.transform_values! { |v| yield convert_value_to_parameters(v) }
self
end
@@ -531,12 +690,12 @@ module ActionController
self
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.
- def delete(key)
- convert_value_to_parameters(@parameters.delete(key))
+ # 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_value_to_parameters(@parameters.delete(key, &block))
end
# Returns a new instance of <tt>ActionController::Parameters</tt> with only
@@ -545,7 +704,7 @@ module ActionController
new_instance_with_inherited_permitted_status(@parameters.select(&block))
end
- # Equivalent to Hash#keep_if, but returns nil if no changes were made.
+ # Equivalent to Hash#keep_if, but returns +nil+ if no changes were made.
def select!(&block)
@parameters.select!(&block)
self
@@ -571,27 +730,37 @@ module ActionController
convert_value_to_parameters(@parameters.values_at(*keys))
end
- # 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|
- duplicate.permitted = @permitted
- 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
- # +other_hash+ merges into current hash.
- def merge(other_hash)
+ # current hash merged into +other_hash+.
+ def reverse_merge(other_hash)
new_instance_with_inherited_permitted_status(
- @parameters.merge(other_hash)
+ 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
@@ -604,30 +773,44 @@ module ActionController
"<#{self.class} #{@parameters} permitted: #{@permitted}>"
end
- def method_missing(method_sym, *args, &block)
- if @parameters.respond_to?(method_sym)
- message = <<-DEPRECATE.squish
- Method #{method_sym} is deprecated and will be removed in Rails 5.1,
- as `ActionController::Parameters` no longer inherits from
- hash. Using this deprecated behavior exposes potential security
- problems. If you continue to use this method you may be creating
- a security vulnerability in your app that can be exploited. Instead,
- consider using one of these documented methods which are not
- deprecated: http://api.rubyonrails.org/v#{ActionPack.version}/classes/ActionController/Parameters.html
- DEPRECATE
- ActiveSupport::Deprecation.warn(message)
- @parameters.public_send(method_sym, *args, &block)
- else
- super
+ 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
+ # backwards compatibility 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
attr_reader :parameters
- def permitted=(new_permitted)
- @permitted = new_permitted
- end
+ attr_writer :permitted
def fields_for_style?
@parameters.all? { |k, v| k =~ /\A-?\d+\z/ && (v.is_a?(Hash) || v.is_a?(Parameters)) }
@@ -682,7 +865,7 @@ module ActionController
when Parameters
if object.fields_for_style?
hash = object.class.new
- object.each { |k,v| hash[k] = yield v }
+ object.each { |k, v| hash[k] = yield v }
hash
else
yield object
@@ -704,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
#
@@ -735,28 +918,46 @@ 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 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
@@ -770,7 +971,12 @@ module ActionController
array_of_permitted_scalars?(self[key]) do |val|
params[key] = val
end
- else
+ 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|
element.permit(*Array.wrap(filter[key]))
@@ -778,22 +984,59 @@ module ActionController
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
@@ -801,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
@@ -812,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
@@ -821,7 +1064,7 @@ 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. You might want
+ # 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
@@ -839,7 +1082,7 @@ 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: [ :id, :name, :category ])
diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb
index ac37b00010..6e8a95040f 100644
--- a/actionpack/lib/action_controller/metal/testing.rb
+++ b/actionpack/lib/action_controller/metal/testing.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionController
module Testing
extend ActiveSupport::Concern
@@ -10,11 +12,5 @@ module ActionController
self.params = nil
end
end
-
- module ClassMethods
- def before_filters
- _process_action_callbacks.find_all{|x| x.kind == :before}.map(&:name)
- end
- end
end
end
diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb
index dbf7241a14..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)) ||
@@ -42,7 +44,7 @@ module ActionController
options[:original_script_name] = original_script_name
else
if same_origin
- options[:script_name] = request.script_name.empty? ? "".freeze : request.script_name.dup
+ options[:script_name] = request.script_name.empty? ? "" : request.script_name.dup
else
options[:script_name] = script_name
end
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 e4d19e9dba..8c16308ce7 100644
--- a/actionpack/lib/action_controller/renderer.rb
+++ b/actionpack/lib/action_controller/renderer.rb
@@ -1,11 +1,11 @@
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
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 +13,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: { ... }
#
@@ -37,15 +37,15 @@ module ActionController
attr_reader :defaults, :controller
DEFAULTS = {
- http_host: 'example.org',
+ http_host: "example.org",
https: false,
- method: 'get',
- script_name: '',
- input: ''
+ method: "get",
+ script_name: "",
+ input: ""
}.freeze
# Create a new renderer instance for a specific controller class.
- def self.for(controller, env = {}, defaults = DEFAULTS)
+ def self.for(controller, env = {}, defaults = DEFAULTS.dup)
new(controller, env, defaults)
end
@@ -56,11 +56,12 @@ module ActionController
# Create a new renderer for the same controller but with new defaults.
def with_defaults(defaults)
- self.class.new controller, env, self.defaults.merge(defaults)
+ self.class.new controller, @env, self.defaults.merge(defaults)
end
# Accepts a custom Rack environment to render templates in.
- # It will be merged with ActionController::Renderer.defaults
+ # It will be merged with the default Rack environment defined by
+ # +ActionController::Renderer::DEFAULTS+.
def initialize(controller, env, defaults)
@controller = controller
@defaults = defaults
@@ -68,8 +69,23 @@ module ActionController
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 an 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
@@ -83,26 +99,29 @@ module ActionController
private
def normalize_keys(env)
new_env = {}
- env.each_pair { |k,v| new_env[rack_key_for(k)] = rack_value_for(k, v) }
+ 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
RACK_KEY_TRANSLATION = {
- http_host: 'HTTP_HOST',
- https: 'HTTPS',
- method: 'REQUEST_METHOD',
- script_name: 'SCRIPT_NAME',
- input: 'rack.input'
+ 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' },
+ https: ->(v) { v ? "on" : "off" },
method: ->(v) { v.upcase },
}
- def rack_key_for(key); RACK_KEY_TRANSLATION[key]; end
+ def rack_key_for(key)
+ RACK_KEY_TRANSLATION.fetch(key, key.to_s)
+ end
def rack_value_for(key, value)
RACK_VALUE_TRANSLATION.fetch(key, IDENTITY).call value
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 ecd21f29ce..57921f32b7 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -1,44 +1,49 @@
-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/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
- # :stopdoc:
class Metal
include Testing::Functional
end
module Live
- # Disable controller / rendering threads in tests. User tests can access
+ # Disable controller / rendering threads in tests. User tests can access
# the database on the main thread, so they could open a txn, then the
# controller thread will open a new connection and try to access data
- # that's only visible to the main thread's txn. This is the problem in #23483
- remove_method :new_controller_thread
+ # 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.
+ # ActionController::TestCase will be deprecated and moved to a gem in the future.
# 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
@@ -46,13 +51,14 @@ 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'] }
+ xml: lambda { |raw_post| Hash.from_xml(raw_post)["hash"] }
}
end
@@ -61,7 +67,7 @@ module ActionController
end
def content_type=(type)
- set_header 'CONTENT_TYPE', type
+ set_header "CONTENT_TYPE", type
end
def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys)
@@ -83,7 +89,7 @@ module ActionController
end
if get?
- if self.query_string.blank?
+ if query_string.blank?
self.query_string = non_path_parameters.to_query
end
else
@@ -91,8 +97,8 @@ module ActionController
self.content_type = ENCODER.content_type
data = ENCODER.build_multipart non_path_parameters
else
- fetch_header('CONTENT_TYPE') do |k|
- set_header k, 'application/x-www-form-urlencoded'
+ fetch_header("CONTENT_TYPE") do |k|
+ set_header k, "application/x-www-form-urlencoded"
end
case content_mime_type.to_sym
@@ -110,8 +116,9 @@ module ActionController
end
end
- set_header 'CONTENT_LENGTH', data.length.to_s
- set_header '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
fetch_header("PATH_INFO") do |k|
@@ -127,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
@@ -152,9 +159,9 @@ module ActionController
private
- def params_parsers
- super.merge @custom_param_parsers
- end
+ def params_parsers
+ super.merge @custom_param_parsers
+ end
end
class LiveTestResponse < Live::Response
@@ -208,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
#
@@ -241,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
@@ -261,9 +276,6 @@ module ActionController
# after calling +post+. If the various assert methods are not sufficient, then you
# may use this object to inspect the HTTP response in detail.
#
- # (Earlier versions of \Rails required each functional test to subclass
- # Test::Unit::TestCase and define @controller, @request, @response in +setup+.)
- #
# == Controller is automatically inferred
#
# ActionController::TestCase will automatically infer the controller under test
@@ -288,7 +300,7 @@ module ActionController
# assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
# assert flash.empty? # makes sure that there's nothing in the flash
#
- # On top of the collections, you have the complete url that a given action redirected to available in <tt>redirect_to_url</tt>.
+ # On top of the collections, you have the complete URL that a given action redirected to available in <tt>redirect_to_url</tt>.
#
# For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
# action call which can then be asserted against.
@@ -320,7 +332,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.
#
@@ -343,7 +354,7 @@ module ActionController
end
def controller_class
- if current_controller_class = self._controller_class
+ if current_controller_class = _controller_class
current_controller_class
else
self.controller_class = determine_default_controller_class(name)
@@ -377,57 +388,42 @@ 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', '*/*'].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 an HTTP request to +action+ by specifying request method,
# parameters and set/volley the response.
#
@@ -440,6 +436,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:
#
@@ -456,57 +454,40 @@ 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: nil, 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 || session || flash
- 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"
+
+ @request = TestRequest.new scrub_env!(@request.env), @request.session, @controller.class
+ @response = build_response @response_klass
+ @response.request = @request
+ @controller.recycle!
if body
- @request.set_header 'RAW_POST_DATA', body
+ @request.set_header "RAW_POST_DATA", body
end
- if http_method
- http_method = http_method.to_s.upcase
- else
- http_method = "GET"
+ @request.set_header "REQUEST_METHOD", http_method
+
+ if as
+ @request.content_type = Mime[as].to_s
+ format ||= as
end
- parameters ||= {}
+ parameters = (params || {}).symbolize_keys
if format
parameters[:format] = format
end
- @html_document = nil
-
- self.cookies.update @request.cookies
- self.cookies.update_cookies_from_jar
- @request.set_header 'HTTP_COOKIE', cookies.to_header
- @request.delete_header 'action_dispatch.cookies'
-
- @request = TestRequest.new scrub_env!(@request.env), @request.session
- @response = build_response @response_klass
- @response.request = @request
- @controller.recycle!
-
- @request.set_header 'REQUEST_METHOD', http_method
-
- parameters = parameters.symbolize_keys
-
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)
@@ -517,9 +498,9 @@ module ActionController
@request.flash.update(flash || {})
if xhr
- @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(', ')
+ @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
@@ -527,32 +508,35 @@ module ActionController
@request.set_header k, @controller.config.relative_url_root
end
- @controller.recycle!
- @controller.dispatch(action, @request, @response)
- @request = @controller.request
- @response = @controller.response
-
- @request.delete_header '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 @request.have_cookie_jar?
- unless @request.cookie_jar.committed?
- @request.cookie_jar.write(@response)
- self.cookies.update(@request.cookie_jar.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.delete_header 'HTTP_X_REQUESTED_WITH'
- @request.delete_header 'HTTP_ACCEPT'
+ @response.sent!
end
- @request.query_string = ''
@response
end
@@ -587,7 +571,7 @@ module ActionController
end
end
- @request = TestRequest.create
+ @request = TestRequest.create(@controller.class)
@response = build_response @response_klass
@response.request = @request
@@ -606,71 +590,37 @@ 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
end
- # :startdoc:
end
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 01d49475de..8f39b88d56 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2019 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,24 @@ 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 :HostAuthorization
autoload :RequestId
autoload :Callbacks
autoload :Cookies
autoload :DebugExceptions
+ autoload :DebugLocks
+ autoload :DebugView
autoload :ExceptionWrapper
autoload :Executor
autoload :Flash
- autoload :ParamsParser
autoload :PublicExceptions
autoload :Reloader
autoload :RemoteIp
@@ -63,7 +68,7 @@ module ActionDispatch
end
autoload :Journey
- autoload :MiddlewareStack, 'action_dispatch/middleware/stack'
+ autoload :MiddlewareStack, "action_dispatch/middleware/stack"
autoload :Routing
module Http
@@ -75,31 +80,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 9fa2e38ae3..8cc84ff36c 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -1,10 +1,11 @@
+# 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 = get_header(HTTP_IF_MODIFIED_SINCE)
@@ -27,7 +28,7 @@ module ActionDispatch
def etag_matches?(etag)
if etag
validators = if_none_match_etags
- validators.include?(etag) || validators.include?('*')
+ validators.include?(etag) || validators.include?("*")
end
end
@@ -96,17 +97,17 @@ module ActionDispatch
# 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.
+ # 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)
+ set_header "ETag", generate_weak_etag(weak_validators)
end
def strong_etag=(strong_validators)
- set_header 'ETag', generate_strong_etag(strong_validators)
+ set_header "ETag", generate_strong_etag(strong_validators)
end
def etag?; etag; end
@@ -123,8 +124,8 @@ module ActionDispatch
private
- DATE = 'Date'.freeze
- LAST_MODIFIED = "Last-Modified".freeze
+ 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)
@@ -132,12 +133,12 @@ module ActionDispatch
end
def generate_strong_etag(validators)
- %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
end
def cache_control_segments
if cache_control = _cache_control
- cache_control.delete(' ').split(',')
+ cache_control.delete(" ").split(",")
else
[]
end
@@ -147,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] ||= []
@@ -165,19 +166,23 @@ module ActionDispatch
@cache_control = cache_control_headers
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!(@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!(cache_control)
+ def merge_and_normalize_cache_control!(cache_control)
control = {}
cc_headers = cache_control_headers
if extras = cc_headers.delete(:extras)
@@ -190,20 +195,26 @@ module ActionDispatch
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
- if control[:extras]
- self._cache_control = _cache_control + ", #{control[:extras].join(', ')}"
- end
+ options = []
+ options << PUBLIC if control[:public]
+ options << NO_CACHE
+ options.concat(control[:extras]) if control[:extras]
+
+ self._cache_control = options.join(", ")
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(", ")
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 041eca48ca..cbb772175c 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -1,4 +1,6 @@
-require 'action_dispatch/http/parameter_filter'
+# frozen_string_literal: true
+
+require "active_support/parameter_filter"
module ActionDispatch
module Http
@@ -7,8 +9,8 @@ module ActionDispatch
# sub-hashes of the params hash to filter. Filtering only certain sub-keys
# from a hash is possible by using the dot notation: 'credit_card.number'.
# If a block is given, each key and value of the params hash and all
- # sub-hashes is passed to it, the value or key can be replaced using
- # String#replace or similar method.
+ # sub-hashes are passed to it, where the value or the key can be replaced using
+ # String#replace or similar methods.
#
# env["action_dispatch.parameter_filter"] = [:password]
# => replaces the value to all keys matching /password/i with "[FILTERED]"
@@ -26,8 +28,8 @@ module ActionDispatch
# => 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
super
@@ -39,6 +41,8 @@ module ActionDispatch
# 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
# Returns a hash of request.env with all sensitive data replaced.
@@ -46,35 +50,35 @@ module ActionDispatch
@filtered_env ||= env_filter.filter(@env)
end
- # Reconstructed a path with all sensitive GET parameters replaced.
+ # Reconstructs a path with all sensitive GET parameters replaced.
def filtered_path
@filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}"
end
- protected
+ private
- def parameter_filter
+ def parameter_filter # :doc:
parameter_filter_for fetch_header("action_dispatch.parameter_filter") {
return NULL_PARAM_FILTER
}
end
- def env_filter
+ def env_filter # :doc:
user_key = fetch_header("action_dispatch.parameter_filter") {
return NULL_ENV_FILTER
}
parameter_filter_for(Array(user_key) + ENV_MATCH)
end
- def parameter_filter_for(filters)
- 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 f4b806b8b5..8c4e852235 100644
--- a/actionpack/lib/action_dispatch/http/filter_redirect.rb
+++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb
@@ -1,8 +1,9 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Http
module FilterRedirect
-
- FILTERED = '[FILTERED]'.freeze # :nodoc:
+ FILTERED = "[FILTERED]" # :nodoc:
def filtered_location # :nodoc:
if location_filter_match?
@@ -16,7 +17,7 @@ module ActionDispatch
def location_filters
if request
- request.get_header('action_dispatch.redirect_filter') || []
+ request.get_header("action_dispatch.redirect_filter") || []
else
[]
end
@@ -31,7 +32,6 @@ module ActionDispatch
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 69a934b7cd..6c7d24d2d0 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Http
# Provides access to the request's HTTP headers from the environment.
#
# env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
- # headers = ActionDispatch::Http::Headers.new(env)
+ # headers = ActionDispatch::Http::Headers.from_hash(env)
# headers["Content-Type"] # => "text/plain"
# headers["User-Agent"] # => "curl/7.43.0"
#
@@ -86,7 +88,7 @@ module ActionDispatch
@req.fetch_header(env_name(key)) do
return default unless default == DEFAULT
return yield if block_given?
- raise NameError, key
+ raise KeyError, key
end
end
@@ -115,16 +117,16 @@ module ActionDispatch
private
- # 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 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 e9b25339dc..4e81ba12a5 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -1,22 +1,25 @@
-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.
def content_mime_type
fetch_header("action_dispatch.request.content_type") do |k|
- v = if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/
+ v = if get_header("CONTENT_TYPE") =~ /^([^,\;]*)/
Mime::Type.lookup($1.strip.downcase)
else
nil
@@ -29,14 +32,14 @@ module ActionDispatch
content_mime_type && content_mime_type.to_s
end
- def has_content_type?
- has_header? 'CONTENT_TYPE'
+ def has_content_type? # :nodoc:
+ get_header "CONTENT_TYPE"
end
# Returns the accepted MIME type for the request.
def accepts
fetch_header("action_dispatch.request.accepts") do |k|
- header = get_header('HTTP_ACCEPT').to_s.strip
+ header = get_header("HTTP_ACCEPT").to_s.strip
v = if header.empty?
[content_mime_type]
@@ -61,7 +64,7 @@ module ActionDispatch
fetch_header("action_dispatch.request.formats") do |k|
params_readable = begin
parameters[:format]
- rescue ActionController::BadRequest
+ rescue *RESCUABLE_MIME_FORMAT_ERRORS
false
end
@@ -76,6 +79,11 @@ module ActionDispatch
else
[Mime[:html]]
end
+
+ v = v.select do |format|
+ format.symbol || format.ref == "*/*"
+ end
+
set_header k, v
end
end
@@ -87,10 +95,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
@@ -135,9 +140,7 @@ module ActionDispatch
}
end
- # Receives an array of mimes and return the first user sent mime that
- # matches the order array.
- #
+ # Returns the first MIME type that matches the provided array of MIME types.
def negotiate_mime(order)
formats.each do |priority|
if priority == Mime::ALL
@@ -150,25 +153,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
- path = @env['action_dispatch.original_path'] || @env['PATH_INFO']
- if match = path && path.match(/\.(\w+)\z/)
- Mime[match.captures.first]
+ 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
end
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index 4672ea7199..88b3a93211 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -1,8 +1,7 @@
-# -*- frozen-string-literal: true -*-
+# frozen_string_literal: true
-require 'singleton'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/core_ext/string/starts_ends_with'
+require "singleton"
+require "active_support/core_ext/string/starts_ends_with"
module Mime
class Mimes
@@ -45,35 +44,9 @@ module Mime
return type if type.is_a?(Type)
EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k }
end
-
- def const_missing(sym)
- ext = sym.downcase
- if Mime[ext]
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Accessing mime types via constants is deprecated.
- Please change `Mime::#{sym}` to `Mime[:#{ext}]`.
- MSG
- Mime[ext]
- else
- super
- end
- end
-
- def const_defined?(sym, inherit = true)
- ext = sym.downcase
- if Mime[ext]
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Accessing mime types via constants is deprecated.
- Please change `Mime.const_defined?(#{sym})` to `Mime[:#{ext}]`.
- MSG
- true
- else
- super
- end
- 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
@@ -91,7 +64,7 @@ module Mime
@register_callbacks = []
- # A simple helper class used in parsing the accept header
+ # A simple helper class used in parsing the accept header.
class AcceptItem #:nodoc:
attr_accessor :index, :name, :q
alias :to_s :name
@@ -99,7 +72,7 @@ module Mime
def initialize(index, name, q = nil)
@index = index
@name = name
- q ||= 0.0 if @name == '*/*'.freeze # default wildcard match to end of list
+ q ||= 0.0 if @name == "*/*" # Default wildcard match to end of list.
@q = ((q || 1.0).to_f * 100).to_i
end
@@ -114,25 +87,25 @@ module Mime
def self.sort!(list)
list.sort!
- text_xml_idx = find_item_by_name list, 'text/xml'
+ text_xml_idx = find_item_by_name list, "text/xml"
app_xml_idx = find_item_by_name list, Mime[:xml].to_s
- # Take care of the broken text/xml entry by renaming or deleting it
+ # Take care of the broken text/xml entry by renaming or deleting it.
if text_xml_idx && app_xml_idx
app_xml = list[app_xml_idx]
text_xml = list[text_xml_idx]
- app_xml.q = [text_xml.q, app_xml.q].max # set the q value to the max of the two
- if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list
+ app_xml.q = [text_xml.q, app_xml.q].max # Set the q value to the max of the two.
+ if app_xml_idx > text_xml_idx # Make sure app_xml is ahead of text_xml in the list.
list[app_xml_idx], list[text_xml_idx] = text_xml, app_xml
app_xml_idx, text_xml_idx = text_xml_idx, app_xml_idx
end
- list.delete_at(text_xml_idx) # delete text_xml from the list
+ list.delete_at(text_xml_idx) # Delete text_xml from the list.
elsif text_xml_idx
list[text_xml_idx].name = Mime[:xml].to_s
end
- # Look for more specific XML-based types and sort them ahead of app/xml
+ # Look for more specific XML-based types and sort them ahead of app/xml.
if app_xml_idx
app_xml = list[app_xml_idx]
idx = app_xml_idx
@@ -141,7 +114,7 @@ module Mime
type = list[idx]
break if type.q < app_xml.q
- if type.name.ends_with? '+xml'
+ if type.name.ends_with? "+xml"
list[app_xml_idx], list[idx] = list[idx], app_xml
app_xml_idx = idx
end
@@ -174,7 +147,7 @@ module Mime
EXTENSION_LOOKUP[extension.to_s]
end
- # Registers an alias that's not used on mime type lookup, but can be referenced directly. Especially useful for
+ # Registers an alias that's not used on MIME type lookup, but can be referenced directly. Especially useful for
# rendering different HTML versions depending on the user agent, like an iPhone.
def register_alias(string, symbol, extension_synonyms = [])
register(string, symbol, [], extension_synonyms, true)
@@ -195,12 +168,13 @@ module Mime
end
def parse(accept_header)
- if !accept_header.include?(',')
+ if !accept_header.include?(",")
accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first
+ return [] unless accept_header
parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact
else
list, index = [], 0
- accept_header.split(',').each do |header|
+ accept_header.split(",").each do |header|
params, q = header.split(PARAMETER_SEPARATOR_REGEXP)
next unless params
@@ -248,7 +222,18 @@ module Mime
attr_reader :hash
+ MIME_NAME = "[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}"
+ MIME_PARAMETER_KEY = "[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}"
+ MIME_PARAMETER_VALUE = "#{Regexp.escape('"')}?[a-zA-Z0-9][a-zA-Z0-9#{Regexp.escape('!#$&-^_.+')}]{0,126}#{Regexp.escape('"')}?"
+ MIME_PARAMETER = "\s*\;\s+#{MIME_PARAMETER_KEY}(?:\=#{MIME_PARAMETER_VALUE})?"
+ MIME_REGEXP = /\A(?:\*\/\*|#{MIME_NAME}\/(?:\*|#{MIME_NAME})(?:\s*#{MIME_PARAMETER}\s*)*)\z/
+
+ class InvalidMimeType < StandardError; end
+
def initialize(string, symbol = nil, synonyms = [])
+ unless MIME_REGEXP.match?(string)
+ raise InvalidMimeType, "#{string.inspect} is not a valid MIME type"
+ end
@symbol, @synonyms = symbol, synonyms
@string = string
@hash = [@string, @synonyms, @symbol].hash
@@ -306,31 +291,31 @@ module Mime
protected
- attr_reader :string, :synonyms
+ 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? '?'
- end
+ 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
+ super "*/*", nil
end
def all?; true; end
@@ -351,15 +336,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 66cea88256..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,6 +10,7 @@ Mime::Type.register "text/css", :css
Mime::Type.register "text/calendar", :ics
Mime::Type.register "text/csv", :csv
Mime::Type.register "text/vcard", :vcf
+Mime::Type.register "text/vtt", :vtt, %w(vtt)
Mime::Type.register "image/png", :png, [], %w(png)
Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)
@@ -18,15 +21,27 @@ 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 )
diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb
index e826551f4b..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?("\\.".freeze) }
- deep_strings, strings = strings.partition { |s| s.include?("\\.".freeze) }
+require "active_support/deprecation/constant_accessor"
+require "active_support/parameter_filter"
- regexps << Regexp.new(strings.join('|'.freeze), true) unless strings.empty?
- deep_regexps << Regexp.new(deep_strings.join('|'.freeze), 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 ff5031d7d5..13d0963a33 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -1,19 +1,30 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Http
module Parameters
extend ActiveSupport::Concern
- PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
+ 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}
+ 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
@@ -21,7 +32,16 @@ module ActionDispatch
end
module ClassMethods
- def parameter_parsers=(parsers) # :nodoc:
+ # Configure the parameter parser for a given MIME type.
+ #
+ # It accepts a hash where the key is the symbol of the MIME type
+ # and the value is a proc.
+ #
+ # original_parsers = ActionDispatch::Request.parameter_parsers
+ # xml_parser = -> (raw_post) { Hash.from_xml(raw_post) || {} }
+ # new_parsers = original_parsers.merge(xml: xml_parser)
+ # ActionDispatch::Request.parameter_parsers = new_parsers
+ def parameter_parsers=(parsers)
@parameter_parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key }
end
end
@@ -37,14 +57,23 @@ module ActionDispatch
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:
- delete_header('action_dispatch.request.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.
@@ -57,24 +86,51 @@ module ActionDispatch
private
- def parse_formatted_parameters(parsers)
- return yield if content_length.zero?
+ 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
+ params
+ end
+
+ def binary_params_for?(controller, action)
+ controller_class_for(controller).binary_params_for?(action)
+ rescue NameError
+ false
+ end
- strategy = parsers.fetch(content_mime_type.symbol) { return yield }
+ def parse_formatted_parameters(parsers)
+ return yield if content_length.zero? || content_mime_type.nil?
- begin
- strategy.call(raw_post)
- rescue # JSON or Ruby code block errors
- my_logger = logger || ActiveSupport::Logger.new($stderr)
- my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}"
+ strategy = parsers.fetch(content_mime_type.symbol) { return yield }
- raise ParamsParser::ParseError
+ begin
+ strategy.call(raw_post)
+ rescue # JSON or Ruby code block errors.
+ log_parse_error_once
+ raise ParseError
+ end
end
- end
- def params_parsers
- ActionDispatch::Request.parameter_parsers
- 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 b0ed681623..44f23940d3 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -1,16 +1,18 @@
-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
@@ -20,10 +22,11 @@ module ActionDispatch
include ActionDispatch::Http::Parameters
include ActionDispatch::Http::FilterParameters
include ActionDispatch::Http::URL
+ include ActionDispatch::ContentSecurityPolicy::Request
include Rack::Request::Env
- autoload :Session, 'action_dispatch/request/session'
- 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(%.*)?$/]
@@ -66,29 +69,21 @@ module ActionDispatch
def commit_cookie_jar! # :nodoc:
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 encoding: #{key} => #{value.inspect}"
- end
- end
- end
-
PASS_NOT_FOUND = Class.new { # :nodoc:
def self.action(_); self; end
- def self.call(_); [404, {'X-Cascade' => 'pass'}, []]; end
+ def self.call(_); [404, { "X-Cascade" => "pass" }, []]; end
+ def self.binary_params_for?(action); false; end
}
def controller_class
- check_path_parameters!
params = path_parameters
+ params[:action] ||= "index"
+ controller_class_for(params[:controller])
+ end
- if params.key?(:controller)
- controller_param = params[:controller].underscore
- params[:action] ||= 'index'
+ def controller_class_for(name)
+ if name
+ controller_param = name.underscore
const_name = "#{controller_param.camelize}Controller"
ActiveSupport::Dependencies.constantize(const_name)
else
@@ -96,19 +91,22 @@ module ActionDispatch
end
end
+ # Returns true if the request has a header matching the given key parameter.
+ #
+ # request.key? :ip_spoofing_check # => true
def key?(key)
has_header? key
end
# 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)
@@ -122,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
}
@@ -138,11 +136,11 @@ module ActionDispatch
end
def routes # :nodoc:
- get_header("action_dispatch.routes".freeze)
+ get_header("action_dispatch.routes")
end
def routes=(routes) # :nodoc:
- set_header("action_dispatch.routes".freeze, routes)
+ set_header("action_dispatch.routes", routes)
end
def engine_script_name(_routes) # :nodoc:
@@ -160,11 +158,11 @@ module ActionDispatch
end
def controller_instance # :nodoc:
- get_header('action_controller.instance'.freeze)
+ get_header("action_controller.instance")
end
def controller_instance=(controller) # :nodoc:
- set_header('action_controller.instance'.freeze, controller)
+ set_header("action_controller.instance", controller)
end
def http_auth_salt
@@ -173,12 +171,12 @@ module ActionDispatch
def show_exceptions? # :nodoc:
# We're treating `nil` as "unset", and we want the default setting to be
- # `true`. This logic should be extracted to `env_config` and calculated
+ # `true`. This logic should be extracted to `env_config` and calculated
# once.
- !(get_header('action_dispatch.show_exceptions'.freeze) == false)
+ !(get_header("action_dispatch.show_exceptions") == false)
end
- # Returns a symbol form of the #request_method
+ # Returns a symbol form of the #request_method.
def request_method_symbol
HTTP_METHOD_LOOKUP[request_method]
end
@@ -187,10 +185,10 @@ module ActionDispatch
# even if it was overridden by middleware. See #request_method for
# more information.
def method
- @method ||= check_method(get_header("rack.methodoverride.original_method") || get_header('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
@@ -202,6 +200,23 @@ module ActionDispatch
@headers ||= Http::Headers.new(self)
end
+ # Early Hints is an HTTP/2 status code that indicates hints to help a client start
+ # making preparations for processing the final response.
+ #
+ # If the env contains +rack.early_hints+ then the server accepts HTTP2 push for Link headers.
+ #
+ # The +send_early_hints+ method accepts a hash of links as follows:
+ #
+ # send_early_hints("Link" => "</style.css>; rel=preload; as=style\n</script.js>; rel=preload")
+ #
+ # If you are using +javascript_include_tag+ or +stylesheet_link_tag+ the
+ # Early Hints headers are included by default if supported.
+ def send_early_hints(links)
+ return unless env["rack.early_hints"]
+
+ env["rack.early_hints"].call(links)
+ end
+
# Returns a +String+ with the last requested path including their params.
#
# # get '/foo'
@@ -249,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?
- get_header('HTTP_X_REQUESTED_WITH') =~ /XMLHttpRequest/i
+ get_header("HTTP_X_REQUESTED_WITH") =~ /XMLHttpRequest/i
end
alias :xhr? :xml_http_request?
@@ -265,17 +280,17 @@ module ActionDispatch
end
def remote_ip=(remote_ip)
- set_header "action_dispatch.remote_ip".freeze, remote_ip
+ set_header "action_dispatch.remote_ip", remote_ip
end
- ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # :nodoc:
+ 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
get_header ACTION_DISPATCH_REQUEST_ID
end
@@ -288,25 +303,25 @@ module ActionDispatch
# Returns the lowercase name of the HTTP server software.
def server_software
- (get_header('SERVER_SOFTWARE') && /^([a-zA-Z]+)/ =~ get_header('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 has_header? 'RAW_POST_DATA'
+ unless has_header? "RAW_POST_DATA"
raw_post_body = body
- set_header('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
- get_header '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 = get_header('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
body_stream
@@ -326,7 +341,7 @@ module ActionDispatch
end
def body_stream #:nodoc:
- get_header('rack.input')
+ get_header("rack.input")
end
# TODO This should be broken apart into AD::Request::Session and probably
@@ -347,7 +362,7 @@ module ActionDispatch
Session::Options.set self, options
end
- # Override Rack's GET method to support indifferent access
+ # Override Rack's GET method to support indifferent access.
def GET
fetch_header("action_dispatch.request.query_parameters") do |k|
rack_query_params = super || {}
@@ -360,7 +375,7 @@ module ActionDispatch
end
alias :query_parameters :GET
- # Override Rack's POST method to support indifferent access
+ # Override Rack's POST method to support indifferent access.
def POST
fetch_header("action_dispatch.request.request_parameters") do
pr = parse_formatted_parameters(params_parsers) do |params|
@@ -368,9 +383,6 @@ module ActionDispatch
end
self.request_parameters = Request::Utils.normalize_encode_params(pr)
end
- rescue ParamsParser::ParseError # one of the parse strategies blew up
- self.request_parameters = Request::Utils.normalize_encode_params(super || {})
- raise
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}")
end
@@ -379,10 +391,10 @@ module ActionDispatch
# Returns the authorization header regardless of whether it was specified directly or through one of the
# proxy alternatives.
def authorization
- get_header('HTTP_AUTHORIZATION') ||
- get_header('X-HTTP_AUTHORIZATION') ||
- get_header('X_HTTP_AUTHORIZATION') ||
- get_header('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, or ::1.
@@ -392,18 +404,18 @@ module ActionDispatch
def request_parameters=(params)
raise if params.nil?
- set_header("action_dispatch.request.request_parameters".freeze, params)
+ set_header("action_dispatch.request.request_parameters", params)
end
def logger
- get_header("action_dispatch.logger".freeze)
+ get_header("action_dispatch.logger")
end
def commit_flash
end
def ssl?
- super || scheme == 'wss'.freeze
+ super || scheme == "wss"
end
private
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index fa4c54701a..69798f99e0 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -1,7 +1,9 @@
-require 'active_support/core_ext/module/attribute_accessors'
-require 'action_dispatch/http/filter_redirect'
-require 'action_dispatch/http/cache'
-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
@@ -39,9 +41,9 @@ module ActionDispatch # :nodoc:
super(header)
end
- def []=(k,v)
+ def []=(k, v)
if @response.sending? || @response.sent?
- raise ActionDispatch::IllegalStateError, 'header already sent'
+ raise ActionDispatch::IllegalStateError, "header already sent"
end
super
@@ -67,19 +69,26 @@ module ActionDispatch # :nodoc:
alias_method :headers, :header
- delegate :[], :[]=, :to => :@header
- delegate :each, :to => :@stream
+ delegate :[], :[]=, to: :@header
+
+ def each(&block)
+ sending!
+ x = @stream.each(&block)
+ sent!
+ x
+ end
- CONTENT_TYPE = "Content-Type".freeze
- SET_COOKIE = "Set-Cookie".freeze
- LOCATION = "Location".freeze
+ CONTENT_TYPE = "Content-Type"
+ SET_COOKIE = "Set-Cookie"
+ LOCATION = "Location"
NO_CONTENT_CODES = [100, 101, 102, 204, 205, 304]
+ CONTENT_TYPE_PARSER = /\A(?<type>[^;\s]+)?(?:.*;\s*charset=(?<quote>"?)(?<charset>[^;\s]+)\k<quote>)?/ # :nodoc:
- 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
+ # Aliasing these off because AD::Http::Cache::Response defines them.
alias :_cache_control :cache_control
alias :_cache_control= :cache_control=
@@ -97,10 +106,10 @@ module ActionDispatch # :nodoc:
def body
@str_body ||= begin
- buf = ''
- each { |chunk| buf << chunk }
- buf
- end
+ buf = +""
+ each { |chunk| buf << chunk }
+ buf
+ end
end
def write(string)
@@ -112,10 +121,13 @@ module ActionDispatch # :nodoc:
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
@@ -129,6 +141,12 @@ 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)
@@ -207,12 +225,6 @@ module ActionDispatch # :nodoc:
@status = Rack::Utils.status_code(status)
end
- # Sets the HTTP content type.
- def content_type=(content_type)
- header_info = parse_content_type
- set_content_type content_type.to_s, header_info.charset || self.class.default_charset
- end
-
# Sets the HTTP response's content MIME type. For example, in the controller
# you could write this:
#
@@ -221,9 +233,19 @@ module ActionDispatch # :nodoc:
# 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)
+ 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
+ # Content type of response.
+ # It returns just MIME type and does NOT contain charset part.
def content_type
- parse_content_type.mime_type
+ parsed_content_type_header.mime_type
end
def sending_file=(v)
@@ -232,17 +254,16 @@ module ActionDispatch # :nodoc:
end
end
- # Sets the HTTP character set. In case of nil parameter
- # it sets the charset to utf-8.
+ # 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)
- header_info = parse_content_type
+ content_type = parsed_content_type_header.mime_type
if false == charset
- set_header CONTENT_TYPE, header_info.mime_type
+ set_content_type content_type, nil
else
- content_type = header_info.mime_type
set_content_type content_type, charset || self.class.default_charset
end
end
@@ -250,7 +271,7 @@ module ActionDispatch # :nodoc:
# 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 = parse_content_type
+ header_info = parsed_content_type_header
header_info.charset || self.class.default_charset
end
@@ -314,7 +335,7 @@ module ActionDispatch # :nodoc:
# Stream the file's contents if Rack::Sendfile isn't present.
def each
- File.open(to_path, 'rb') do |file|
+ File.open(to_path, "rb") do |file|
while chunk = file.read(16384)
yield chunk
end
@@ -374,7 +395,7 @@ module ActionDispatch # :nodoc:
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
@@ -388,26 +409,30 @@ module ActionDispatch # :nodoc:
ContentTypeHeader = Struct.new :mime_type, :charset
NullContentTypeHeader = ContentTypeHeader.new nil, nil
- def parse_content_type
- content_type = get_header CONTENT_TYPE
- if content_type
- type, charset = content_type.split(/;\s*charset=/)
- type = nil if type.empty?
- ContentTypeHeader.new(type, charset)
+ def parse_content_type(content_type)
+ if content_type && match = CONTENT_TYPE_PARSER.match(content_type)
+ ContentTypeHeader.new(match[:type], match[: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}" if 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
@@ -435,7 +460,7 @@ module ActionDispatch # :nodoc:
def assign_default_content_type_and_charset!
return if content_type
- ct = parse_content_type
+ ct = parsed_content_type_header
set_content_type(ct.mime_type || Mime[:html].to_s,
ct.charset || self.class.default_charset)
end
@@ -460,7 +485,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
@@ -479,7 +504,7 @@ module ActionDispatch # :nodoc:
def handle_no_content!
if NO_CONTENT_CODES.include?(@status)
@header.delete CONTENT_TYPE
- @header.delete 'Content-Length'
+ @header.delete "Content-Length"
end
end
diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb
index a221f4c5af..0da8f5c14e 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.
@@ -18,29 +20,32 @@ module ActionDispatch
# A +Tempfile+ object with the actual uploaded file. Note that some of
# its interface is available directly.
attr_accessor :tempfile
- alias :to_io :tempfile
# A string with the headers of the multipart request.
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
- @original_filename = hash[:filename]
- if @original_filename
begin
@original_filename.encode!(Encoding::UTF_8)
rescue EncodingError
@original_filename.force_encoding(Encoding::UTF_8)
end
+ else
+ @original_filename = nil
end
+
@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
@@ -50,7 +55,7 @@ module ActionDispatch
end
# Shortcut for +tempfile.close+.
- def close(unlink_now=false)
+ def close(unlink_now = false)
@tempfile.close(unlink_now)
end
@@ -59,6 +64,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
@@ -73,6 +83,10 @@ module ActionDispatch
def eof?
@tempfile.eof?
end
+
+ def to_io
+ @tempfile.to_io
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index 37f41ae988..8227749986 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/module/attribute_accessors'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors"
module ActionDispatch
module Http
@@ -7,8 +9,7 @@ module ActionDispatch
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.
@@ -42,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)
@@ -59,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]
@@ -78,110 +79,108 @@ module ActionDispatch
private
- def add_params(path, params)
- params = { params: params } unless params.is_a?(Hash)
- params.reject! { |_,v| v.to_param.nil? }
- query = params.to_query
- path << "?#{query}" unless query.empty?
- end
-
- def add_anchor(path, anchor)
- if anchor
- path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}"
+ def add_params(path, params)
+ params = { params: params } unless params.is_a?(Hash)
+ params.reject! { |_, v| v.to_param.nil? }
+ query = params.to_query
+ path << "?#{query}" unless query.empty?
end
- end
- def extract_domain_from(host, tld_length)
- host.split('.').last(1 + tld_length).join('.')
- end
+ def add_anchor(path, anchor)
+ if anchor
+ path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}"
+ end
+ end
- def extract_subdomains_from(host, tld_length)
- parts = host.split('.')
- parts[0..-(tld_length + 2)]
- end
+ def extract_domain_from(host, tld_length)
+ host.split(".").last(1 + tld_length).join(".")
+ end
- def add_trailing_slash(path)
- # includes querysting
- if path.include?('?')
- path.sub!(/\?/, '/\&')
- # does not have a .format
- elsif !path.include?(".")
- path.sub!(/[^\/]\z|\A\z/, '\&/')
+ def extract_subdomains_from(host, tld_length)
+ parts = host.split(".")
+ parts[0..-(tld_length + 2)]
end
- end
- def build_host_url(host, port, protocol, options, path)
- if match = host.match(HOST_REGEXP)
- protocol ||= match[1] unless protocol == false
- host = match[2]
- port = match[3] unless options.key? :port
+ def add_trailing_slash(path)
+ if path.include?("?")
+ path.sub!(/\?/, '/\&')
+ elsif !path.include?(".")
+ path.sub!(/[^\/]\z|\A\z/, '\&/')
+ end
end
- protocol = normalize_protocol protocol
- host = normalize_host(host, options)
+ def build_host_url(host, port, protocol, options, path)
+ if match = host.match(HOST_REGEXP)
+ protocol ||= match[1] unless protocol == false
+ host = match[2]
+ port = match[3] unless options.key? :port
+ end
- result = protocol.dup
+ protocol = normalize_protocol protocol
+ host = normalize_host(host, options)
- if options[:user] && options[:password]
- result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
- end
+ result = protocol.dup
- result << host
- normalize_port(port, protocol) { |normalized_port|
- result << ":#{normalized_port}"
- }
+ if options[:user] && options[:password]
+ result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
+ end
- result.concat path
- end
+ result << host
+ normalize_port(port, protocol) { |normalized_port|
+ result << ":#{normalized_port}"
+ }
- def named_host?(host)
- IP_HOST_REGEXP !~ host
- end
+ result.concat path
+ end
- def normalize_protocol(protocol)
- case protocol
- when nil
- "http://"
- when false, "//"
- "//"
- when PROTOCOL_REGEXP
- "#{$1}://"
- else
- raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}"
+ def named_host?(host)
+ IP_HOST_REGEXP !~ host
+ end
+
+ def normalize_protocol(protocol)
+ case protocol
+ when nil
+ "http://"
+ when false, "//"
+ "//"
+ when PROTOCOL_REGEXP
+ "#{$1}://"
+ else
+ raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}"
+ end
end
- end
- def normalize_host(_host, options)
- return _host unless named_host?(_host)
+ def normalize_host(_host, options)
+ return _host unless named_host?(_host)
- tld_length = options[:tld_length] || @@tld_length
- subdomain = options.fetch :subdomain, true
- domain = options[:domain]
+ tld_length = options[:tld_length] || @@tld_length
+ subdomain = options.fetch :subdomain, true
+ domain = options[:domain]
- host = ""
- if subdomain == true
- return _host if domain.nil?
+ host = +""
+ if subdomain == true
+ return _host if domain.nil?
- host << extract_subdomains_from(_host, tld_length).join('.')
- elsif subdomain
- host << subdomain.to_param
+ host << extract_subdomains_from(_host, tld_length).join(".")
+ elsif subdomain
+ host << subdomain.to_param
+ end
+ host << "." unless host.empty?
+ host << (domain || extract_domain_from(_host, tld_length))
+ host
end
- host << "." unless host.empty?
- host << (domain || extract_domain_from(_host, tld_length))
- host
- end
- def normalize_port(port, protocol)
- return unless port
+ def normalize_port(port, protocol)
+ return unless port
- case protocol
- when "//" then yield port
- when "https://"
- yield port unless port.to_i == 443
- else
- yield port unless port.to_i == 80
+ case protocol
+ when "//" then yield port
+ when "https://"
+ yield port unless port.to_i == 443
+ else
+ yield port unless port.to_i == 80
+ end
end
- end
end
def initialize
@@ -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 = x_forwarded_host.presence
forwarded.split(/,\s?/).last
else
- get_header('HTTP_HOST') || "#{server_name || server_addr}:#{get_header('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+$/, ''.freeze)
+ 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
- get_header('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/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
index 0323360faa..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 = nil # need for variable scope
+ missing_keys = nil
match_route(name, constraints) do |route|
parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
@@ -32,15 +35,24 @@ module ActionDispatch
defaults = route.defaults
required_parts = route.required_parts
- parameterized_parts.keep_if do |key, value|
- (defaults[key].nil? && value.present?) || 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}" if missing_keys && !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
@@ -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
@@ -169,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 7063b44bb5..56e9e3c83d 100644
--- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb
+++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Journey # :nodoc:
module NFA # :nodoc:
@@ -7,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];
@@ -26,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 2793c5668d..086d6a3e07 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:
@@ -18,7 +20,7 @@ module ActionDispatch
end
def to_s
- Visitors::String::INSTANCE.accept(self, '')
+ Visitors::String::INSTANCE.accept(self, "")
end
def to_dot
@@ -30,7 +32,7 @@ module ActionDispatch
end
def name
- left.tr '*:'.freeze, ''.freeze
+ -left.tr("*:", "")
end
def type
@@ -63,12 +65,12 @@ module ActionDispatch
def literal?; false; end
end
- %w{ Symbol Slash Dot }.each do |t|
- class_eval <<-eoruby, __FILE__, __LINE__ + 1
- class #{t} < Terminal;
- def type; :#{t.upcase}; end
- end
- eoruby
+ class Slash < Terminal # :nodoc:
+ def type; :SLASH; end
+ end
+
+ class Dot < Terminal # :nodoc:
+ def type; :DOT; end
end
class Symbol < Terminal # :nodoc:
@@ -80,13 +82,14 @@ module ActionDispatch
def initialize(left)
super
@regexp = DEFAULT_EXP
- @name = left.tr '*:'.freeze, ''.freeze
+ @name = -left.tr("*:", "")
end
def default_regexp?
regexp == DEFAULT_EXP
end
+ def type; :SYMBOL; end
def symbol?; true; end
end
@@ -104,7 +107,7 @@ module ActionDispatch
def type; :STAR; end
def name
- left.name.tr '*:', ''
+ left.name.tr "*:", ""
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 fff0299812..18ec6c9b9b 100644
--- a/actionpack/lib/action_dispatch/journey/parser_extras.rb
+++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb
@@ -1,9 +1,12 @@
-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)
@@ -24,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 018b89a2b7..697f5b9d8b 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Journey # :nodoc:
module Path # :nodoc:
class Pattern # :nodoc:
attr_reader :spec, :requirements, :anchored
- def self.from_string string
+ def self.from_string(string)
build(string, {}, "/.?", true)
end
@@ -31,6 +33,13 @@ module ActionDispatch
Visitors::FormatBuilder.new.accept(spec)
end
+ def eager_load!
+ required_names
+ offsets
+ to_regexp
+ nil
+ end
+
def ast
@spec.find_all(&:symbol?).each do |node|
re = @requirements[node.to_sym]
@@ -81,7 +90,7 @@ module ActionDispatch
return @separator_re unless @matchers.key?(node)
re = @matchers[node]
- "(#{re})"
+ "(#{Regexp.union(re)})"
end
def visit_GROUP(node)
@@ -98,7 +107,7 @@ module ActionDispatch
end
def visit_STAR(node)
- re = @matchers[node.left.to_sym] || '.+'
+ re = @matchers[node.left.to_sym] || ".+"
"(#{re})"
end
@@ -110,7 +119,7 @@ module ActionDispatch
class UnanchoredRegexp < AnchoredRegexp # :nodoc:
def accept(node)
- %r{\A#{visit node}}
+ %r{\A#{visit node}(?:\b|\Z)}
end
end
@@ -174,8 +183,8 @@ module ActionDispatch
node = node.to_sym
if @requirements.key?(node)
- re = /#{@requirements[node]}|/
- @offsets.push((re.match('').length - 1) + @offsets.last)
+ re = /#{Regexp.union(@requirements[node])}|/
+ @offsets.push((re.match("").length - 1) + @offsets.last)
else
@offsets << @offsets.last
end
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index cfd6681dd1..8165709a3d 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
module ActionDispatch
- module Journey # :nodoc:
- class Route # :nodoc:
+ # :stopdoc:
+ module Journey
+ class Route
attr_reader :app, :path, :defaults, :name, :precedence
attr_reader :constraints, :internal
@@ -9,11 +12,11 @@ module ActionDispatch
module VerbMatchers
VERBS = %w{ DELETE GET HEAD OPTIONS LINK PATCH POST PUT TRACE UNLINK }
VERBS.each do |v|
- class_eval <<-eoc
- class #{v}
- def self.verb; name.split("::").last; end
- def self.call(req); req.#{v.downcase}?; end
- end
+ 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
@@ -29,16 +32,15 @@ module ActionDispatch
class All
def self.call(_); true; end
- def self.verb; ''; end
+ def self.verb; ""; end
end
- VERB_TO_CLASS = VERBS.each_with_object({ :all => All }) do |verb, hash|
+ 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)
@@ -73,6 +75,14 @@ module ActionDispatch
@internal = internal
end
+ def eager_load!
+ path.eager_load!
+ ast
+ parts
+ required_defaults
+ nil
+ end
+
def ast
@decorated_ast ||= begin
decorated_ast = path.ast
@@ -81,9 +91,16 @@ module ActionDispatch
end
end
- def requirements # :nodoc:
- # needed for rails `rails 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
@@ -96,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
@@ -124,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
@@ -164,17 +186,18 @@ module ActionDispatch
end
def verb
- verbs.join('|')
+ verbs.join("|")
end
private
- def verbs
- @request_method_match.map(&:verb)
- end
+ def verbs
+ @request_method_match.map(&:verb)
+ end
- def match_verb(request)
- @request_method_match.any? { |m| m.call request }
- 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 06cdce1724..89a164f968 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -1,27 +1,33 @@
-require 'action_dispatch/journey/router/utils'
-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
-
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
@@ -29,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
@@ -48,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)
@@ -58,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
@@ -72,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
@@ -92,7 +105,7 @@ 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)
}
@@ -107,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]
diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb
index 9793ca1c7a..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!('/'.freeze)
- path.sub!(%r{/+\Z}, ''.freeze)
+ path ||= ""
+ encoding = path.encoding
+ path = +"/#{path}"
+ path.squeeze!("/")
+ path.sub!(%r{/+\Z}, "")
path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
- path = '/' if path == ''.freeze
+ 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 f7b009109e..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
@@ -49,11 +51,12 @@ 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)
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 306d2e674a..d2619cbf3a 100644
--- a/actionpack/lib/action_dispatch/journey/visitors.rb
+++ b/actionpack/lib/action_dispatch/journey/visitors.rb
@@ -1,10 +1,13 @@
+# 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
@@ -21,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
@@ -37,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
@@ -57,7 +60,7 @@ module ActionDispatch
private
- def visit node
+ def visit(node)
send(DISPATCH_CACHE[node.type], node)
end
@@ -97,7 +100,7 @@ module ActionDispatch
visit(node, seed)
end
- def visit node, seed
+ def visit(node, seed)
send(DISPATCH_CACHE[node.type], node, seed)
end
@@ -153,7 +156,7 @@ module ActionDispatch
end
end
- # Loop through the requirements AST
+ # Loop through the requirements AST.
class Each < FunctionalVisitor # :nodoc:
def visit(node, block)
block.call(node)
@@ -166,28 +169,28 @@ module ActionDispatch
class String < FunctionalVisitor # :nodoc:
private
- def binary(node, seed)
- visit(node.right, visit(node.left, seed))
- end
+ def binary(node, seed)
+ visit(node.right, visit(node.left, seed))
+ end
- def nary(node, seed)
- last_child = node.children.last
- node.children.inject(seed) { |s, c|
- string = visit(c, s)
- string << "|".freeze unless last_child == c
- string
- }
- 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, seed)
- seed + node.left
- end
+ def terminal(node, seed)
+ seed + node.left
+ end
- def visit_GROUP(node, seed)
- visit(node.left, seed << "(".freeze) << ")".freeze
- end
+ def visit_GROUP(node, seed)
+ visit(node.left, seed.dup << "(") << ")"
+ end
- INSTANCE = new
+ INSTANCE = new
end
class Dot < FunctionalVisitor # :nodoc:
@@ -261,4 +264,5 @@ module ActionDispatch
end
end
end
+ # :startdoc:
end
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb
index c782779b34..87fe19225b 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,17 +8,6 @@ module ActionDispatch
define_callbacks :call
class << self
- def to_prepare(*args, &block)
- ActiveSupport::Reloader.to_prepare(*args, &block)
- end
-
- def to_cleanup(*args, &block)
- ActiveSupport::Reloader.to_complete(*args, &block)
- end
-
- deprecate to_prepare: 'use ActiveSupport::Reloader.to_prepare instead',
- to_cleanup: 'use ActiveSupport::Reloader.to_complete instead'
-
def before(*args, &block)
set_callback(:call, :before, *args, &block)
end
@@ -34,10 +24,8 @@ module ActionDispatch
def call(env)
error = nil
result = run_callbacks :call do
- begin
- @app.call(env)
- rescue => error
- end
+ @app.call(env)
+ rescue => error
end
raise error if error
result
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index f2f3150b56..b69bcab05c 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -1,13 +1,15 @@
-require 'active_support/core_ext/hash/keys'
-require 'active_support/key_generator'
-require 'active_support/message_verifier'
-require 'active_support/json'
-require 'rack/utils'
+# 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
def cookie_jar
- fetch_header('action_dispatch.cookies'.freeze) do
+ fetch_header("action_dispatch.cookies") do
self.cookie_jar = Cookies::CookieJar.build(self, cookies)
end
end
@@ -20,11 +22,11 @@ module ActionDispatch
}
def have_cookie_jar?
- has_header? 'action_dispatch.cookies'.freeze
+ has_header? "action_dispatch.cookies"
end
def cookie_jar=(jar)
- set_header 'action_dispatch.cookies'.freeze, jar
+ set_header "action_dispatch.cookies", jar
end
def key_generator
@@ -43,8 +45,20 @@ module ActionDispatch
get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT
end
- def secret_token
- get_header Cookies::SECRET_TOKEN
+ 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_key_base
@@ -58,6 +72,15 @@ module ActionDispatch
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
@@ -77,16 +100,17 @@ 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.
- # # The cookie is signed by your app's `secrets.secret_key_base` value.
# # It can be read using the encrypted method `cookies.encrypted[:name]`
# cookies.encrypted[:discount] = 45
#
@@ -94,7 +118,7 @@ module ActionDispatch
# 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:
#
@@ -112,7 +136,7 @@ module ActionDispatch
#
# cookies[:name] = {
# value: 'a yummy cookie',
- # expires: 1.year.from_now,
+ # expires: 1.year,
# domain: 'domain.com'
# }
#
@@ -137,22 +161,27 @@ 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_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
@@ -160,7 +189,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:
#
@@ -179,12 +208,9 @@ module ActionDispatch
# 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.
- #
- # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
- # legacy cookies signed with the old key generator will be transparently upgraded.
+ # cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
#
- # 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:
#
@@ -193,35 +219,25 @@ module ActionDispatch
#
# cookies.signed[:discount] # => 45
def signed
- @signed ||=
- if upgrade_legacy_signed_cookies?
- UpgradeLegacySignedCookieJar.new(self)
- else
- SignedCookieJar.new(self)
- end
+ @signed ||= SignedKeyRotatingCookieJar.new(self)
end
# Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
- # 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,
- # legacy cookies signed with the old key generator will be transparently upgraded.
+ # 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 +secrets.secret_key_base+.
+ # 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 upgrade_legacy_signed_cookies?
- UpgradeLegacyEncryptedCookieJar.new(self)
- else
- EncryptedCookieJar.new(self)
- end
+ @encrypted ||= EncryptedKeyRotatingCookieJar.new(self)
end
# Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
@@ -237,32 +253,19 @@ module ActionDispatch
private
- def upgrade_legacy_signed_cookies?
- request.secret_token.present? && request.secret_key_base.present?
- end
- end
-
- # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
- # to the Message{Encryptor,Verifier} allows us to handle the
- # (de)serialization step within the cookie jar, which gives us the
- # opportunity to detect and migrate legacy cookies.
- module VerifyAndUpgradeLegacySignedMessage # :nodoc:
- def initialize(*args)
- super
- @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
- end
+ def 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 verify_and_upgrade_legacy_signed_message(name, signed_message)
- deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value|
- self[name] = { value: value }
+ def encrypted_cookie_cipher
+ request.encrypted_cookie_cipher || "aes-256-gcm"
end
- rescue ActiveSupport::MessageVerifier::InvalidSignature
- nil
- end
- private
- def parse(name, signed_message)
- super || verify_and_upgrade_legacy_signed_message(name, signed_message)
+ def signed_cookie_digest
+ request.signed_cookie_digest || "SHA1"
end
end
@@ -325,6 +328,9 @@ module ActionDispatch
end
alias :has_key? :key?
+ # Returns the cookies as Hash.
+ alias :to_hash :to_h
+
def update(other_hash)
@cookies.update other_hash.stringify_keys
self
@@ -332,30 +338,34 @@ module ActionDispatch
def update_cookies_from_jar
request_jar = @request.cookie_jar.instance_variable_get(:@cookies)
- set_cookies = request_jar.reject { |k,_| @delete_cookies.key?(k) }
+ set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) || @set_cookies.key?(k) }
@cookies.update set_cookies if set_cookies
end
def to_header
- @cookies.map { |k,v| "#{escape(k)}=#{escape(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 (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| request.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
@@ -367,12 +377,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)
@@ -404,9 +414,9 @@ 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)
@@ -415,31 +425,30 @@ module ActionDispatch
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 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 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)
- request.ssl? || !cookie[:secure] || always_write_cookie
- end
+ def write_cookie?(cookie)
+ request.ssl? || !cookie[:secure] || always_write_cookie
+ end
end
class AbstractCookieJar # :nodoc:
@@ -451,7 +460,7 @@ module ActionDispatch
def [](name)
if data = @parent_jar[name.to_s]
- parse name, data
+ parse(name, data, purpose: "cookie.#{name}") || parse(name, data)
end
end
@@ -462,7 +471,7 @@ module ActionDispatch
options = { value: options }
end
- commit(options)
+ commit(name, options)
@parent_jar[name] = options
end
@@ -470,13 +479,27 @@ module ActionDispatch
def request; @parent_jar.request; end
private
- def parse(name, data); data; end
- def commit(options); end
+ 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)
+ expiry_options(options).tap do |metadata|
+ metadata[:purpose] = "cookie.#{name}" if request.use_cookies_with_metadata
+ end
+ end
+
+ def parse(name, data, purpose: nil); data; end
+ def commit(name, options); end
end
class PermanentCookieJar < AbstractCookieJar # :nodoc:
private
- def commit(options)
+ def commit(name, options)
options[:expires] = 20.years.from_now
end
end
@@ -492,7 +515,8 @@ module ActionDispatch
end
module SerializedCookieJars # :nodoc:
- MARSHAL_SIGNATURE = "\x04\x08".freeze
+ MARSHAL_SIGNATURE = "\x04\x08"
+ SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
protected
def needs_migration?(value)
@@ -503,12 +527,16 @@ module ActionDispatch
serializer.dump(value)
end
- def deserialize(name, value)
+ def deserialize(name)
+ rotate = false
+ value = yield -> { rotate = true }
+
if value
- if needs_migration?(value)
- Marshal.load(value).tap do |v|
- self[name] = { value: v }
- end
+ case
+ when needs_migration?(value)
+ self[name] = Marshal.load(value)
+ when rotate
+ self[name] = serializer.load(value)
else
serializer.load(value)
end
@@ -528,81 +556,84 @@ module ActionDispatch
end
def digest
- request.cookies_digest || 'SHA1'
- end
-
- def key_generator
- request.key_generator
+ request.cookies_digest || "SHA1"
end
end
- class SignedCookieJar < AbstractCookieJar # :nodoc:
+ class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
include SerializedCookieJars
def initialize(parent_jar)
super
- secret = key_generator.generate_key(request.signed_cookie_salt)
- @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
+
+ secret = request.key_generator.generate_key(request.signed_cookie_salt)
+ @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
+
+ request.cookies_rotations.signed.each do |*secrets, **options|
+ @verifier.rotate(*secrets, serializer: SERIALIZER, **options)
+ end
end
private
- def parse(name, signed_message)
- deserialize name, @verifier.verified(signed_message)
+ def parse(name, signed_message, purpose: nil)
+ deserialize(name) do |rotate|
+ @verifier.verified(signed_message, on_rotation: rotate, purpose: purpose)
+ end
end
- def commit(options)
- options[:value] = @verifier.generate(serialize(options[:value]))
+ def commit(name, options)
+ options[:value] = @verifier.generate(serialize(options[:value]), cookie_metadata(name, options))
raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
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
- end
-
- class EncryptedCookieJar < AbstractCookieJar # :nodoc:
+ class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
include SerializedCookieJars
def initialize(parent_jar)
super
- if ActiveSupport::LegacyKeyGenerator === key_generator
- raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " +
- "Read the upgrade documentation to learn more about this new config option."
+ if request.use_authenticated_cookie_encryption
+ key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
+ secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len)
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER)
+ else
+ key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len)
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER)
end
- secret = key_generator.generate_key(request.encrypted_cookie_salt || '')
- sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '')
- @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
+ request.cookies_rotations.encrypted.each do |*secrets, **options|
+ @encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
+ end
+
+ if upgrade_legacy_hmac_aes_cbc_cookies?
+ legacy_cipher = "aes-256-cbc"
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher))
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
+
+ @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
+ end
end
private
- def parse(name, encrypted_message)
- deserialize name, @encryptor.decrypt_and_verify(encrypted_message)
- rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
+ 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
nil
end
- def commit(options)
- options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]))
+ def commit(name, options)
+ options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), cookie_metadata(name, options))
raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
end
end
- # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
- # instead of EncryptedCookieJar if secrets.secret_token and 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
- end
-
def initialize(app)
@app = app
end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index 51a471fb23..59113e13f4 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -1,185 +1,183 @@
-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"
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__)
-
- class DebugView < ActionView::Base
- def debug_params(params)
- clean_params = params.clone
- clean_params.delete("action")
- clean_params.delete("controller")
-
- if clean_params.empty?
- 'None'
- else
- PP.pp(clean_params, "", 200)
- end
- end
-
- def debug_headers(headers)
- if headers.present?
- headers.inspect.gsub(',', ",\n")
- else
- 'None'
- end
- end
+ cattr_reader :interceptors, instance_accessor: false, default: []
- 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 self.register_interceptor(object = nil, &block)
+ interceptor = object || block
+ interceptors << interceptor
end
- def initialize(app, routes_app = nil, response_format = :default)
+ 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
+ invoke_interceptors(request, exception)
raise exception unless request.show_exceptions?
render_exception(request, exception)
end
private
- 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')
- case @response_format
- when :api
- render_for_api_application(request, wrapper)
- when :default
- render_for_default_application(request, wrapper)
+ def invoke_interceptors(request, exception)
+ backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
+
+ @interceptors.each do |interceptor|
+ interceptor.call(request, exception)
+ rescue Exception
+ log_error(request, wrapper)
end
- else
- raise exception
end
- end
- def render_for_default_application(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')
- format = "text/html"
+ 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")
+ begin
+ content_type = request.formats.first
+ rescue Mime::Type::InvalidMimeType
+ render_for_api_request(Mime[:text], wrapper)
+ end
+
+ 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
- render(wrapper.status_code, body, format)
- end
- def render_for_api_application(request, 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
- }
-
- content_type = request.formats.first
- 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]
+ 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")
+ format = "text/html"
+ end
+ render(wrapper.status_code, body, format)
end
- render(wrapper.status_code, formatted_body, format)
- 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 create_template(request, wrapper)
- traces = wrapper.traces
+ render(wrapper.status_code, formatted_body, format)
+ end
- trace_to_show = 'Application Trace'
- if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error'
- trace_to_show = 'Full Trace'
+ def create_template(request, wrapper)
+ DebugView.new(
+ 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
- if source_to_show = traces[trace_to_show].first
- source_to_show_id = source_to_show[:id]
+ def render(status, body, format)
+ [status, { "Content-Type" => "#{format}; charset=#{Response.default_charset}", "Content-Length" => body.bytesize.to_s }, [body]]
end
- 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(wrapper.exception),
- source_extracts: wrapper.source_extracts,
- line_number: wrapper.line_number,
- file: wrapper.file
- )
- end
+ def log_error(request, wrapper)
+ logger = logger(request)
+ return unless logger
- def render(status, body, format)
- [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
- end
+ exception = wrapper.exception
- def log_error(request, wrapper)
- logger = logger(request)
- return unless logger
+ trace = wrapper.application_trace
+ trace = wrapper.framework_trace if trace.empty?
- exception = wrapper.exception
+ ActiveSupport::Deprecation.silence do
+ message = []
+ message << " "
+ message << "#{exception.class} (#{exception.message}):"
+ message.concat(exception.annotated_source_code) if exception.respond_to?(:annotated_source_code)
+ message << " "
+ message.concat(trace)
- trace = wrapper.application_trace
- trace = wrapper.framework_trace if trace.empty?
+ log_array(logger, message)
+ end
+ end
- 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
+ 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
- end
- def log_array(logger, array)
- array.map { |line| logger.fatal line }
- end
+ def logger(request)
+ request.logger || ActionView::Base.logger || stderr_logger
+ end
- def logger(request)
- request.logger || ActionView::Base.logger || stderr_logger
- end
+ def stderr_logger
+ @stderr_logger ||= ActiveSupport::Logger.new($stderr)
+ end
- 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 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 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/debug_view.rb b/actionpack/lib/action_dispatch/middleware/debug_view.rb
new file mode 100644
index 0000000000..43c0a84504
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/debug_view.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require "pp"
+
+require "action_view"
+require "action_view/base"
+
+module ActionDispatch
+ class DebugView < ActionView::Base # :nodoc:
+ RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
+
+ def initialize(assigns)
+ paths = [RESCUES_TEMPLATE_PATH]
+ lookup_context = ActionView::LookupContext.new(paths)
+ super(lookup_context, assigns)
+ end
+
+ def compiled_method_container
+ self.class
+ end
+
+ def debug_params(params)
+ clean_params = params.clone
+ clean_params.delete("action")
+ clean_params.delete("controller")
+
+ if clean_params.empty?
+ "None"
+ else
+ PP.pp(clean_params, +"", 200)
+ end
+ end
+
+ def debug_headers(headers)
+ if headers.present?
+ headers.inspect.gsub(",", ",\n")
+ else
+ "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
+end
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index 59edc66086..0cc56f5013 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -1,50 +1,65 @@
-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,
+ "Mime::Type::InvalidMimeType" => :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 :backtrace_cleaner, :exception, :line_number, :file
+ cattr_accessor :wrapper_exceptions, default: [
+ "ActionView::Template::Error"
+ ]
+
+ attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
def initialize(backtrace_cleaner, exception)
@backtrace_cleaner = backtrace_cleaner
- @exception = original_exception(exception)
+ @exception = exception
+ @wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner)
expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
end
+ def unwrapped_exception
+ if wrapper_exceptions.include?(exception.class.to_s)
+ exception.cause
+ else
+ exception
+ end
+ end
+
def rescue_template
@@rescue_templates[@exception.class.name]
end
def status_code
- self.class.status_code_for_exception(@exception.class.name)
+ self.class.status_code_for_exception(unwrapped_exception.class.name)
end
def application_trace
@@ -65,7 +80,11 @@ module ActionDispatch
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)
application_trace_with_ids << trace_with_id
@@ -98,51 +117,65 @@ module ActionDispatch
end
end
- private
+ def trace_to_show
+ if traces["Application Trace"].empty? && rescue_template != "routing_error"
+ "Full Trace"
+ else
+ "Application Trace"
+ end
+ end
- def backtrace
- Array(@exception.backtrace)
+ def source_to_show_id
+ (traces[trace_to_show].first || {})[:id]
end
- def original_exception(exception)
- if @@rescue_responses.has_key?(exception.cause.class.name)
- exception.cause
- else
- exception
+ private
+
+ def backtrace
+ Array(@exception.backtrace)
end
- end
- def clean_backtrace(*args)
- if backtrace_cleaner
- backtrace_cleaner.clean(backtrace, *args)
- else
- backtrace
+ 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 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 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
- 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 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
+ 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
index 06245b403b..129b18d3d9 100644
--- a/actionpack/lib/action_dispatch/middleware/executor.rb
+++ b/actionpack/lib/action_dispatch/middleware/executor.rb
@@ -1,4 +1,6 @@
-require 'rack/body_proxy'
+# frozen_string_literal: true
+
+require "rack/body_proxy"
module ActionDispatch
class Executor
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index 06038af571..cf9165d008 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/hash/keys'
+# frozen_string_literal: true
+
+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
@@ -36,7 +38,7 @@ 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
@@ -60,18 +62,18 @@ module ActionDispatch
session = self.session || {}
flash_hash = self.flash_hash
- if flash_hash && (flash_hash.present? || session.key?('flash'))
+ 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')
+ 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
+ def reset_session # :nodoc:
super
self.flash = nil
end
@@ -118,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)
@@ -129,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:
@@ -277,15 +279,16 @@ module ActionDispatch
end
protected
- def now_is_loaded?
- @now
- end
+ def now_is_loaded?
+ @now
+ end
- def stringify_array(array)
- array.map do |item|
- item.kind_of?(Symbol) ? item.to_s : item
+ private
+ def stringify_array(array) # :doc:
+ array.map do |item|
+ item.kind_of?(Symbol) ? item.to_s : item
+ end
end
- end
end
def self.new(app) app; end
diff --git a/actionpack/lib/action_dispatch/middleware/host_authorization.rb b/actionpack/lib/action_dispatch/middleware/host_authorization.rb
new file mode 100644
index 0000000000..b7dff1df41
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/host_authorization.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require "action_dispatch/http/request"
+
+module ActionDispatch
+ # This middleware guards from DNS rebinding attacks by explicitly permitting
+ # the hosts a request can be sent to.
+ #
+ # When a request comes to an unauthorized host, the +response_app+
+ # application will be executed and rendered. If no +response_app+ is given, a
+ # default one will run, which responds with +403 Forbidden+.
+ class HostAuthorization
+ class Permissions # :nodoc:
+ def initialize(hosts)
+ @hosts = sanitize_hosts(hosts)
+ end
+
+ def empty?
+ @hosts.empty?
+ end
+
+ def allows?(host)
+ @hosts.any? do |allowed|
+ allowed === host
+ rescue
+ # IPAddr#=== raises an error if you give it a hostname instead of
+ # IP. Treat similar errors as blocked access.
+ false
+ end
+ end
+
+ private
+
+ def sanitize_hosts(hosts)
+ Array(hosts).map do |host|
+ case host
+ when Regexp then sanitize_regexp(host)
+ when String then sanitize_string(host)
+ else host
+ end
+ end
+ end
+
+ def sanitize_regexp(host)
+ /\A#{host}\z/
+ end
+
+ def sanitize_string(host)
+ if host.start_with?(".")
+ /\A(.+\.)?#{Regexp.escape(host[1..-1])}\z/
+ else
+ host
+ end
+ end
+ end
+
+ DEFAULT_RESPONSE_APP = -> env do
+ request = Request.new(env)
+
+ format = request.xhr? ? "text/plain" : "text/html"
+ template = DebugView.new(host: request.host)
+ body = template.render(template: "rescues/blocked_host", layout: "rescues/layout")
+
+ [403, {
+ "Content-Type" => "#{format}; charset=#{Response.default_charset}",
+ "Content-Length" => body.bytesize.to_s,
+ }, [body]]
+ end
+
+ def initialize(app, hosts, response_app = nil)
+ @app = app
+ @permissions = Permissions.new(hosts)
+ @response_app = response_app || DEFAULT_RESPONSE_APP
+ end
+
+ def call(env)
+ return @app.call(env) if @permissions.empty?
+
+ request = Request.new(env)
+
+ if authorized?(request)
+ mark_as_authorized(request)
+ @app.call(env)
+ else
+ @response_app.call(env)
+ end
+ end
+
+ private
+
+ def authorized?(request)
+ origin_host = request.get_header("HTTP_HOST").to_s.sub(/:\d+\z/, "")
+ forwarded_host = request.x_forwarded_host.to_s.split(/,\s?/).last.to_s.sub(/:\d+\z/, "")
+
+ @permissions.allows?(origin_host) &&
+ (forwarded_host.blank? || @permissions.allows?(forwarded_host))
+ end
+
+ def mark_as_authorized(request)
+ request.set_header("action_dispatch.authorized_host", request.host)
+ 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 faf3262b8f..0000000000
--- a/actionpack/lib/action_dispatch/middleware/params_parser.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-require 'action_dispatch/http/request'
-
-module ActionDispatch
- # ActionDispatch::ParamsParser works for all the requests having any Content-Length
- # (like POST). It takes raw data from the request and puts it through the parser
- # that is picked based on Content-Type header.
- #
- # In case of any error while parsing data ParamsParser::ParseError is raised.
- class ParamsParser
- # 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(message = nil, original_exception = nil)
- if message
- ActiveSupport::Deprecation.warn("Passing #message is deprecated and has no effect. " \
- "#{self.class} will automatically capture the message " \
- "of the original exception.", caller)
- end
-
- if original_exception
- ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \
- "Exceptions will automatically capture the original exception.", caller)
- end
-
- super($!.message)
- end
-
- def original_exception
- ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller)
- cause
- end
- end
-
- # Create a new +ParamsParser+ middleware instance.
- #
- # The +parsers+ argument can take Hash of parsers where key is identifying
- # content mime type, and value is a lambda that is going to process data.
- def self.new(app, parsers = {})
- ActiveSupport::Deprecation.warn('ActionDispatch::ParamsParser is deprecated and will be removed in Rails 5.1. Configure the parameter parsing in ActionDispatch::Request.parameter_parsers.')
- parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key }
- ActionDispatch::Request.parameter_parsers = ActionDispatch::Request::DEFAULT_PARSERS.merge(parsers)
- app
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
index 0f27984550..a88ad40f21 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.
@@ -19,37 +21,41 @@ module ActionDispatch
def call(env)
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]) }
+ begin
+ content_type = request.formats.first
+ rescue Mime::Type::InvalidMimeType
+ content_type = Mime[:text]
+ end
+ 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 112bde6596..8bb3ba7504 100644
--- a/actionpack/lib/action_dispatch/middleware/reloader.rb
+++ b/actionpack/lib/action_dispatch/middleware/reloader.rb
@@ -1,54 +1,12 @@
+# 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.
- #
+ # is false.
class Reloader < Executor
- def self.to_prepare(*args, &block)
- ActiveSupport::Reloader.to_prepare(*args, &block)
- end
-
- def self.to_cleanup(*args, &block)
- ActiveSupport::Reloader.to_complete(*args, &block)
- end
-
- def self.prepare!
- default_reloader.prepare!
- end
-
- def self.cleanup!
- default_reloader.reload!
- end
-
- class << self
- attr_accessor :default_reloader # :nodoc:
-
- deprecate to_prepare: 'use ActiveSupport::Reloader.to_prepare instead',
- to_cleanup: 'use ActiveSupport::Reloader.to_complete instead',
- prepare!: 'use Rails.application.reloader.prepare! instead',
- cleanup!: 'use Rails.application.reloader.reload! instead of cleanup + prepare'
- end
-
- self.default_reloader = ActiveSupport::Reloader
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
index 31b75498b6..a5667573f4 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
@@ -131,8 +133,8 @@ module ActionDispatch
should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
if should_check_ip && !forwarded_ips.include?(client_ips.last)
# We don't know which came from the proxy, and which from the user
- raise IpSpoofAttackError, "IP spoofing attack?! " +
- "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " +
+ raise IpSpoofAttackError, "IP spoofing attack?! " \
+ "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \
"HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}"
end
@@ -153,31 +155,27 @@ module ActionDispatch
@ip ||= calculate_ip
end
- protected
+ private
- def ips_from(header)
+ def ips_from(header) # :doc:
return [] unless header
- # Split the comma-separated list into an array of strings
+ # Split the comma-separated list into an array of strings.
ips = header.strip.split(/[,\s]+/)
ips.select do |ip|
- begin
- # Only return IPs that are valid according to the IPAddr#new method
- range = IPAddr.new(ip).to_range
- # we want to make sure nobody is sneaking a netmask in
- range.begin == range.end
- rescue ArgumentError
- nil
- end
+ # 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.
+ range.begin == range.end
+ rescue ArgumentError
+ nil
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 5fb5953811..5b0be96223 100644
--- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -1,34 +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:
-
- def initialize(const_error = nil)
- if const_error
- ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \
- "Exceptions will automatically capture the original exception.", caller)
- end
-
- super("Session contains objects whose class definition isn't available.\n" +
- "Remember to require the classes for all objects kept in the session.\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
-
- def original_exception
- ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller)
- cause
- end
end
module Compatibility
def initialize(app, options = {})
- options[:key] ||= '_session_id'
+ options[:key] ||= "_session_id"
super
end
@@ -38,14 +29,13 @@ module ActionDispatch
sid
end
- protected
+ private
- def initialize_sid
+ def initialize_sid # :doc:
@default_options.delete(:sidbits)
@default_options.delete(:secure_random)
end
- private
def make_request(env)
ActionDispatch::Request.new env
end
@@ -65,7 +55,7 @@ module ActionDispatch
rescue ArgumentError => argument_error
if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
begin
- # Note that the regexp does not allow $1 to end with a ':'
+ # Note that the regexp does not allow $1 to end with a ':'.
$1.constantize
rescue LoadError, NameError
raise ActionDispatch::Session::SessionRestoreError
@@ -94,9 +84,9 @@ module ActionDispatch
private
- def set_cookie(request, session_id, cookie)
- 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 589ae46e38..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
@@ -19,7 +21,7 @@ module ActionDispatch
# Get a session from the cache.
def find_session(env, sid)
- unless sid and session = @cache.read(cache_key(sid))
+ unless sid && (session = @cache.read(cache_key(sid)))
sid, session = generate_sid, {}
end
[sid, session]
@@ -29,7 +31,7 @@ module ActionDispatch
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
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index dec9c60ef2..7c43c781c7 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
@@ -14,44 +16,26 @@ module ActionDispatch
# The cookie jar used for storage is automatically configured to be the
# best possible option given your application's configuration.
#
- # If you only have secret_token set, your cookies will be signed, but
- # not encrypted. This means a user cannot alter their +user_id+ without
- # 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'
- #
- # To generate a secret key for an existing application, run `rails secret`.
+ # In the development and test environments your application's secret key base is
+ # generated by Rails and stored in a temporary file in <tt>tmp/development_secret.txt</tt>.
+ # In all other environments, it is stored encrypted in the
+ # <tt>config/credentials.yml.enc</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.
+ # 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.
#
- # Note that changing the secret key will invalidate all existing sessions!
+ # 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.
#
# Because CookieStore extends Rack::Session::Abstract::Persisted, many of the
# options described there can be used to customize the session cookie that
@@ -63,8 +47,8 @@ module ActionDispatch
# Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
# <tt>:httponly</tt>.
class CookieStore < AbstractStore
- def initialize(app, options={})
- super(app, options.merge!(:cookie_only => true))
+ def initialize(app, options = {})
+ super(app, options.merge!(cookie_only: true))
end
def delete_session(req, session_id, options)
@@ -84,46 +68,46 @@ module ActionDispatch
private
- def extract_session_id(req)
- stale_session_check! do
- unpacked_cookie_data(req)["session_id"]
+ def extract_session_id(req)
+ stale_session_check! do
+ unpacked_cookie_data(req)["session_id"]
+ end
end
- end
- 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!
+ 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
- req.set_header k, v
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 write_session(req, 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(request, session_id, cookie)
- cookie_jar(request)[@key] = cookie
- end
+ def set_cookie(request, session_id, cookie)
+ cookie_jar(request)[@key] = cookie
+ end
- def get_cookie(req)
- cookie_jar(req)[@key]
- end
+ def get_cookie(req)
+ cookie_jar(req)[@key]
+ end
- def cookie_jar(request)
- 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 64695f9738..767143a368 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 " \
@@ -39,22 +41,22 @@ module ActionDispatch
private
- 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 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.unwrapped_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 ab3077b308..00902ede21 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -1,78 +1,69 @@
+# frozen_string_literal: true
+
module ActionDispatch
- # This middleware is added to the stack when `config.force_ssl = true`, and is passed
- # the options set in `config.ssl_options`. It does three jobs to enforce secure HTTP
+ # This middleware is added to the stack when <tt>config.force_ssl = true</tt>, and is passed
+ # the options set in +config.ssl_options+. It does three jobs to enforce secure HTTP
# requests:
#
- # 1. TLS redirect: Permanently redirects http:// requests to https://
- # with the same URL host, path, etc. Enabled by default. Set `config.ssl_options`
- # to modify the destination URL
- # (e.g. `redirect: { host: "secure.widgets.com", port: 8080 }`), or set
- # `redirect: false` to disable this feature.
+ # 1. <b>TLS redirect</b>: Permanently redirects +http://+ requests to +https://+
+ # with the same URL host, path, etc. Enabled by default. Set +config.ssl_options+
+ # to modify the destination URL
+ # (e.g. <tt>redirect: { host: "secure.widgets.com", port: 8080 }</tt>), or set
+ # <tt>redirect: false</tt> to disable this feature.
+ #
+ # 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. Secure cookies: Sets the `secure` flag on cookies to tell browsers they
- # mustn't be sent along with http:// requests. Enabled by default. Set
- # `config.ssl_options` with `secure_cookies: false` to disable this feature.
+ # 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. HTTP Strict Transport Security (HSTS): Tells the browser to remember
- # this site as TLS-only and automatically redirect non-TLS requests.
- # Enabled by default. Configure `config.ssl_options` with `hsts: false` to disable.
+ # 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 `hsts: { … }` to configure HSTS:
- # * `expires`: How long, in seconds, these settings will stick. Defaults to
- # `180.days` (recommended). The minimum required to qualify for browser
- # preload lists is `18.weeks`.
- # * `subdomains`: Set to `true` to tell the browser to apply these settings
- # to all subdomains. This protects your cookies from interception by a
- # vulnerable site on a subdomain. Defaults to `true`.
- # * `preload`: Advertise that this site may be included in browsers'
- # preloaded HSTS lists. HSTS protects your site on every visit *except the
- # first visit* since it hasn't seen your HSTS header yet. To close this
- # gap, browser vendors include a baked-in list of HSTS-enabled sites.
- # Go to https://hstspreload.appspot.com to submit your site for inclusion.
+ # Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS:
#
- # To turn off HSTS, omitting the header is not enough. Browsers will remember the
- # original HSTS directive until it expires. Instead, use the header to tell browsers to
- # expire HSTS immediately. Setting `hsts: false` is a shortcut for
- # `hsts: { expires: 0 }`.
+ # * +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).
#
- # Requests can opt-out of redirection with `exclude`:
+ # * +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+.
#
- # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
+ # * +preload+: Advertise that this site may be included in browsers'
+ # preloaded HSTS lists. HSTS protects your site on every visit <i>except the
+ # first visit</i> since it hasn't seen your HSTS header yet. To close this
+ # gap, browser vendors include a baked-in list of HSTS-enabled sites.
+ # Go to https://hstspreload.org to submit your site for inclusion.
+ # Defaults to +false+.
+ #
+ # To turn off HSTS, omitting the header is not enough. Browsers will remember the
+ # original HSTS directive until it expires. Instead, use the header to tell browsers to
+ # expire HSTS immediately. Setting <tt>hsts: false</tt> is a shortcut for
+ # <tt>hsts: { expires: 0 }</tt>.
class SSL
- # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/
- # and greater than the 18-week requirement for browser preload lists.
- HSTS_EXPIRES_IN = 15552000
+ # :stopdoc:
+
+ # Default to 1 year, the minimum for browser preload lists.
+ HSTS_EXPIRES_IN = 31536000
def self.default_hsts_options
- { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false }
+ { expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
end
- def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, **options)
+ def initialize(app, redirect: {}, hsts: {}, secure_cookies: true)
@app = app
- if options[:host] || options[:port]
- ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc
- The `:host` and `:port` options are moving within `:redirect`:
- `config.ssl_options = { redirect: { host: …, port: … } }`.
- end_warning
- @redirect = options.slice(:host, :port)
- else
- @redirect = redirect
- end
+ @redirect = redirect
@exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
@secure_cookies = secure_cookies
- if hsts != true && hsts != false && hsts[:subdomains].nil?
- hsts[:subdomains] = false
-
- ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc
- In Rails 5.1, The `:subdomains` option of HSTS config will be treated as true if
- unspecified. Set `config.ssl_options = { hsts: { subdomains: false } }` to opt out
- of this behavior.
- end_warning
- end
-
@hsts_header = build_hsts_header(normalize_hsts_options(hsts))
end
@@ -82,7 +73,7 @@ module ActionDispatch
if request.ssl?
@app.call(env).tap do |status, headers, body|
set_hsts_header! headers
- flag_cookies_as_secure! headers if @secure_cookies
+ flag_cookies_as_secure! headers if @secure_cookies && !@exclude.call(request)
end
else
return redirect_to_https request unless @exclude.call(request)
@@ -92,7 +83,7 @@ module ActionDispatch
private
def set_hsts_header!(headers)
- headers['Strict-Transport-Security'.freeze] ||= @hsts_header
+ headers["Strict-Transport-Security"] ||= @hsts_header
end
def normalize_hsts_options(options)
@@ -109,40 +100,48 @@ module ActionDispatch
end
end
- # http://tools.ietf.org/html/rfc6797#section-6.1
+ # https://tools.ietf.org/html/rfc6797#section-6.1
def build_hsts_header(hsts)
- value = "max-age=#{hsts[:expires].to_i}"
+ 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'.freeze]
- cookies = cookies.split("\n".freeze)
+ if cookies = headers["Set-Cookie"]
+ cookies = cookies.split("\n")
- headers['Set-Cookie'.freeze] = 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
end
- }.join("\n".freeze)
+ }.join("\n")
end
end
def redirect_to_https(request)
- [ @redirect.fetch(:status, 301),
- { 'Content-Type' => 'text/html',
- 'Location' => https_location_for(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 = +"https://#{host}"
location << ":#{port}" if port != 80 && port != 443
location << request.fullpath
location
diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb
index 0b4bee5462..f0c869fba0 100644
--- a/actionpack/lib/action_dispatch/middleware/stack.rb
+++ b/actionpack/lib/action_dispatch/middleware/stack.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_support/inflector/methods"
require "active_support/dependencies"
@@ -32,7 +34,28 @@ module ActionDispatch
end
def build(app)
- klass.new(app, *args, &block)
+ InstrumentationProxy.new(klass.new(app, *args, &block), inspect)
+ end
+ end
+
+ # This class is used to instrument the execution of a single middleware.
+ # It proxies the `call` method transparently and instruments the method
+ # call.
+ class InstrumentationProxy
+ EVENT_NAME = "process_middleware.action_dispatch"
+
+ def initialize(middleware, class_name)
+ @middleware = middleware
+
+ @payload = {
+ middleware: class_name,
+ }
+ end
+
+ def call(env)
+ ActiveSupport::Notifications.instrument(EVENT_NAME, @payload) do
+ @middleware.call(env)
+ end
end
end
@@ -88,7 +111,6 @@ module ActionDispatch
end
def delete(target)
- target = get_class target
middlewares.delete_if { |m| m.klass == target }
end
@@ -96,38 +118,20 @@ module ActionDispatch
middlewares.push(build_middleware(klass, args, block))
end
- def build(app = Proc.new)
- middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }
+ def build(app = nil, &block)
+ middlewares.freeze.reverse.inject(app || block) { |a, e| e.build(a) }
end
private
- def assert_index(index, where)
- index = get_class index
- 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 get_class(klass)
- if klass.is_a?(String) || klass.is_a?(Symbol)
- classcache = ActiveSupport::Dependencies::Reference
- converted_klass = classcache[klass.to_s]
- ActiveSupport::Deprecation.warn <<-eowarn
-Passing strings or symbols to the middleware builder is deprecated, please change
-them to actual class references. For example:
-
- "#{klass}" => #{converted_klass}
-
- eowarn
- converted_klass
- else
- klass
+ 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
- end
- def build_middleware(klass, args, block)
- Middleware.new(get_class(klass), args, block)
- 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 41c220236a..1f2f7757a3 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -1,20 +1,22 @@
-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 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, index: 'index', headers: {})
- @root = root.chomp('/')
+ def initialize(root, index: "index", headers: {})
+ @root = root.chomp("/").b
@file_server = ::Rack::File.new(@root, headers)
@index = index
end
@@ -23,30 +25,29 @@ module ActionDispatch
# 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 = ::Rack::Utils.unescape_path path
- return false unless valid_path?(path)
- path = Rack::Utils.clean_path_info 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'.freeze))
+ path = File.join(@root, p.b)
begin
File.file?(path) && File.readable?(path)
rescue SystemCallError
false
end
-
}
- return ::Rack::Utils.escape_path(match)
+ return ::Rack::Utils.escape_path(match).b
end
end
def call(env)
- serve ActionDispatch::Request.new env
+ serve(Rack::Request.new(env))
end
def serve(request)
@@ -59,15 +60,15 @@ module ActionDispatch
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(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
request.path_info = path
end
@@ -78,11 +79,11 @@ module ActionDispatch
end
def content_type(path)
- ::Rack::Mime.mime_type(::File.extname(path), 'text/plain'.freeze)
+ ::Rack::Mime.mime_type(::File.extname(path), "text/plain")
end
def gzip_encoding_accepted?(request)
- request.accept_encoding =~ /\bgzip\b/i
+ request.accept_encoding.any? { |enc, quality| enc =~ /\bgzip\b/i }
end
def gzip_file_path(path)
@@ -94,39 +95,28 @@ module ActionDispatch
false
end
end
-
- def valid_path?(path)
- path.valid_encoding? && !path.include?("\0")
- end
end
# 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, deprecated_cache_control = :not_set, index: 'index', headers: {})
- if deprecated_cache_control != :not_set
- ActiveSupport::Deprecation.warn("The `cache_control` argument is deprecated," \
- "replaced by `headers: { 'Cache-Control' => #{deprecated_cache_control} }`, " \
- " and will be removed in Rails 5.1.")
- headers['Cache-Control'.freeze] = deprecated_cache_control
- end
-
+ def initialize(app, path, index: "index", headers: {})
@app = app
@file_handler = FileHandler.new(path, index: index, headers: headers)
end
def call(env)
- req = ActionDispatch::Request.new env
+ req = Rack::Request.new env
if req.get? || req.head?
- path = req.path_info.chomp('/'.freeze)
+ path = req.path_info.chomp("/")
if match = @file_handler.match?(path)
req.path_info = match
return @file_handler.serve(req)
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb
index e7b913bbe4..88a8e6ad83 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.html.erb
@@ -1,6 +1,8 @@
-<% @source_extracts.each_with_index do |source_extract, index| %>
+<% 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-<%=index%>">
+ <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>
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/blocked_host.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb
new file mode 100644
index 0000000000..1fbc107e28
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb
@@ -0,0 +1,7 @@
+<header>
+ <h1>Blocked host: <%= @host %></h1>
+</header>
+<div id="container">
+ <h2>To allow requests to <%= @host %>, add the following to your environment configuration:</h2>
+ <pre>config.hosts &lt;&lt; "<%= @host %>"</pre>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb
new file mode 100644
index 0000000000..a94dd982a7
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb
@@ -0,0 +1,5 @@
+Blocked host: <%= @host %>
+
+To allow requests to <%= @host %>, add the following to your environment configuration:
+
+ config.hosts << "<%= @host %>"
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..77cfdd20c8
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb
@@ -0,0 +1,24 @@
+<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 defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %>
+ <br />To resolve this issue run: rails active_storage:install
+ <% end %>
+ <% if defined?(ActionMailbox) && @exception.message.match?(%r{#{ActionMailbox::InboundEmail.table_name}}) %>
+ <br />To resolve this issue run: rails action_mailbox: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..16c3ecc331
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb
@@ -0,0 +1,15 @@
+<%= @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 defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %>
+To resolve this issue run: rails active_storage:install
+<% if defined?(ActionMailbox) && @exception.message.match?(%r{#{ActionMailbox::InboundEmail.table_name}}) %>
+To resolve this issue run: rails action_mailbox: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 5060da9369..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
@@ -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/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 e9e6a2e597..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|
@@ -39,8 +50,6 @@ module ActionDispatch
config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil?
ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie
- ActionDispatch::Reloader.default_reloader = app.reloader
-
ActionDispatch.test_app = app
end
end
diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb
index 42890225fa..bc5e0670e0 100644
--- a/actionpack/lib/action_dispatch/request/session.rb
+++ b/actionpack/lib/action_dispatch/request/session.rb
@@ -1,4 +1,6 @@
-require 'rack/session/abstract/id'
+# frozen_string_literal: true
+
+require "rack/session/abstract/id"
module ActionDispatch
class Request
@@ -7,10 +9,10 @@ module ActionDispatch
ENV_SESSION_KEY = Rack::RACK_SESSION # :nodoc:
ENV_SESSION_OPTIONS_KEY = Rack::RACK_SESSION_OPTIONS # :nodoc:
- # Singleton object used to determine if an optional param wasn't specified
+ # Singleton object used to determine if an optional param wasn't specified.
Unspecified = Object.new
-
- # Creates a session hash, merging the properties of the previous session if any
+
+ # Creates a session hash, merging the properties of the previous session if any.
def self.create(store, req, default_options)
session_was = find req
session = Request::Session.new(store, req)
@@ -53,7 +55,7 @@ module ActionDispatch
}
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
@@ -63,7 +65,7 @@ module ActionDispatch
@req = req
@delegate = {}
@loaded = false
- @exists = nil # we haven't checked yet
+ @exists = nil # We haven't checked yet.
end
def id
@@ -79,18 +81,26 @@ module ActionDispatch
options = self.options || {}
@by.send(:delete_session, @req, options.id(@req), options)
- # Load the new sid to be written with the response
+ # Load the new sid to be written with the response.
@loaded = false
load_for_write!
end
# 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,11 +111,13 @@ 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
@@ -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.
#
@@ -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)
@@ -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 @req
- 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 bb3df3c311..fb0efb9a58 100644
--- a/actionpack/lib/action_dispatch/request/utils.rb
+++ b/actionpack/lib/action_dispatch/request/utils.rb
@@ -1,9 +1,22 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+
module ActionDispatch
class Request
class Utils # :nodoc:
+ mattr_accessor :perform_deep_munge, default: true
- mattr_accessor :perform_deep_munge
- self.perform_deep_munge = true
+ def self.each_param_value(params, &block)
+ case params
+ when Array
+ params.each { |element| each_param_value(element, &block) }
+ when Hash
+ params.each_value { |value| each_param_value(value, &block) }
+ when String
+ block.call params
+ end
+ end
def self.normalize_encode_params(params)
if perform_deep_munge
@@ -23,14 +36,13 @@ module ActionDispatch
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, "Non UTF-8 value: #{params}"
+ 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
@@ -53,7 +65,7 @@ module ActionDispatch
end
end
- # Remove nils from the params hash
+ # Remove nils from the params hash.
class NoNilParamEncoder < ParamEncoder # :nodoc:
def self.handle_array(params)
list = super
@@ -64,4 +76,3 @@ module ActionDispatch
end
end
end
-
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index 79d2f1f13c..d78b1c4f71 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -1,3 +1,7 @@
+# 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
# redirect incoming requests to controllers and actions. This replaces
@@ -70,8 +74,8 @@ module ActionDispatch
# For routes that don't fit the <tt>resources</tt> mold, you can use the HTTP helper
# methods <tt>get</tt>, <tt>post</tt>, <tt>patch</tt>, <tt>put</tt> and <tt>delete</tt>.
#
- # get 'post/:id' => 'posts#show'
- # post 'post/:id' => 'posts#create_comment'
+ # get 'post/:id', to: 'posts#show'
+ # post 'post/:id', to: 'posts#create_comment'
#
# Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same
# URL will route to the <tt>show</tt> action.
@@ -79,7 +83,7 @@ module ActionDispatch
# If your route needs to respond to more than one HTTP method (or all methods) then using the
# <tt>:via</tt> option on <tt>match</tt> is preferable.
#
- # match 'post/:id' => 'posts#show', via: [:get, :post]
+ # match 'post/:id', to: 'posts#show', via: [:get, :post]
#
# == Named routes
#
@@ -89,8 +93,8 @@ module ActionDispatch
#
# Example:
#
- # # In routes.rb
- # get '/login' => 'accounts#login', as: 'login'
+ # # In config/routes.rb
+ # get '/login', to: 'accounts#login', as: 'login'
#
# # With render, redirect_to, tests, etc.
# redirect_to login_url
@@ -101,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
@@ -114,21 +118,21 @@ 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/show', to: :list
+ # get 'blog/delete', to: :delete
+ # get 'blog/edit', to: :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
#
# Routes can generate pretty URLs. For example:
#
- # get '/articles/:year/:month/:day' => 'articles#find_by_id', constraints: {
+ # get '/articles/:year/:month/:day', to: 'articles#find_by_id', constraints: {
# year: /\d{4}/,
# month: /\d{1,2}/,
# day: /\d{1,2}/
@@ -143,7 +147,7 @@ module ActionDispatch
# You can specify a regular expression to define a format for a parameter.
#
# controller 'geocode' do
- # get 'geocode/:postalcode' => :show, constraints: {
+ # get 'geocode/:postalcode', to: :show, constraints: {
# postalcode: /\d{5}(-\d{4})?/
# }
# end
@@ -152,13 +156,13 @@ module ActionDispatch
# expression modifiers:
#
# controller 'geocode' do
- # get 'geocode/:postalcode' => :show, constraints: {
+ # get 'geocode/:postalcode', to: :show, constraints: {
# postalcode: /hx\d\d\s\d[a-z]{2}/i
# }
# end
#
# controller 'geocode' do
- # get 'geocode/:postalcode' => :show, constraints: {
+ # get 'geocode/:postalcode', to: :show, constraints: {
# postalcode: /# Postalcode format
# \d{5} #Prefix
# (-\d{4})? #Suffix
@@ -174,13 +178,13 @@ module ActionDispatch
#
# You can redirect any path to another path using the redirect helper in your router:
#
- # get "/stories" => redirect("/posts")
+ # get "/stories", to: redirect("/posts")
#
# == Unicode character routes
#
# You can specify unicode character routes in your router:
#
- # get "ã“ã‚“ã«ã¡ã¯" => "welcome#index"
+ # get "ã“ã‚“ã«ã¡ã¯", to: "welcome#index"
#
# == Routing to Rack Applications
#
@@ -188,7 +192,7 @@ module ActionDispatch
# index action in the PostsController, you can specify any Rack application
# as the endpoint for a matcher:
#
- # get "/application.js" => Sprockets
+ # get "/application.js", to: Sprockets
#
# == Reloading routes
#
@@ -196,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
@@ -206,8 +210,8 @@ module ActionDispatch
# === +assert_routing+
#
# def test_movie_route_properly_splits
- # opts = {controller: "plugin", action: "checkout", id: "2"}
- # assert_routing "plugin/checkout/2", opts
+ # opts = {controller: "plugin", action: "checkout", id: "2"}
+ # assert_routing "plugin/checkout/2", opts
# end
#
# +assert_routing+ lets you test whether or not the route properly resolves into options.
@@ -215,8 +219,8 @@ module ActionDispatch
# === +assert_recognizes+
#
# def test_route_has_options
- # opts = {controller: "plugin", action: "show", id: "12"}
- # assert_recognizes opts, "/plugins/show/12"
+ # opts = {controller: "plugin", action: "show", id: "12"}
+ # assert_recognizes opts, "/plugins/show/12"
# end
#
# Note the subtle difference between the two: +assert_routing+ tests that
@@ -239,8 +243,9 @@ module ActionDispatch
#
# rails routes
#
- # Target specific controllers by prefixing the command with <tt>-c</tt> option.
- #
+ # Target a specific controller with <tt>-c</tt>, or grep routes
+ # using <tt>-g</tt>. Useful in conjunction with <tt>--expanded</tt>
+ # which displays routes vertically.
module Routing
extend ActiveSupport::Autoload
diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb
index 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 5d30a545a2..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,7 +15,7 @@ module ActionDispatch
end
def rack_app
- app.app
+ app.rack_app
end
def path
@@ -33,11 +35,11 @@ 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?
@@ -45,7 +47,7 @@ module ActionDispatch
end
def engine?
- rack_app.respond_to?(:routes)
+ app.engine?
end
end
@@ -59,11 +61,11 @@ module ActionDispatch
@routes = routes
end
- def format(formatter, filter = nil)
+ def format(formatter, filter = {})
routes_to_display = filter_routes(normalize_filter(filter))
routes = collect_routes(routes_to_display)
if routes.none?
- formatter.no_routes(collect_routes(@routes))
+ formatter.no_routes(collect_routes(@routes), filter)
return formatter.result
end
@@ -79,107 +81,156 @@ module ActionDispatch
end
private
-
- def normalize_filter(filter)
- if filter.is_a?(Hash) && filter[:controller]
- { controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ }
- elsif filter
- { controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ }
+ 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
- end
- def filter_routes(filter)
- if filter
- @routes.select do |route|
- route_wrapper = RouteWrapper.new(route)
- filter.any? { |default, value| route_wrapper.send(default) =~ value }
+ 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
- else
- @routes
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(routes)
- @buffer <<
- if routes.none?
- <<-MESSAGE.strip_heredoc
- You don't have any routes defined!
+ class Sheet < Base
+ def section_title(title)
+ @buffer << "\n#{title}:"
+ end
- Please add some routes in config/routes.rb.
- MESSAGE
- else
- "No routes were found for this controller"
+ def section(routes)
+ @buffer << draw_section(routes)
end
- @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html."
- end
- private
- def draw_section(routes)
- header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length)
- name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
+ def header(routes)
+ @buffer << draw_header(routes)
+ end
+
+ private
- routes.map do |r|
- "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ def draw_section(routes)
+ header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length)
+ name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
+
+ routes.map do |r|
+ "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ end
end
- 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
@@ -196,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
+ @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 81ffea4d5d..08560ccf7d 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -1,10 +1,11 @@
-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/regexp'
-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
@@ -18,9 +19,9 @@ module ActionDispatch
CALL = ->(app, req) { app.call req.env }
def initialize(app, constraints, strategy)
- # Unwrap Constraints objects. I don't actually think it's possible
+ # Unwrap Constraints objects. I don't actually think it's possible
# to pass a Constraints object to this constructor, but there were
- # multiple places that kept testing children of this object. I
+ # multiple places that kept testing children of this object. I
# *think* they were just being defensive, but I have no idea.
if app.is_a?(self.class)
constraints += app.constraints
@@ -42,19 +43,32 @@ module ActionDispatch
end
def serve(req)
- return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req)
+ return [ 404, { "X-Cascade" => "pass" }, [] ] unless matches?(req)
@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, :defaults
attr_reader :to, :default_controller, :default_action
@@ -94,7 +108,7 @@ module ActionDispatch
end
def self.optional_format?(path, format)
- format != false && !path.include?(':format') && !path.end_with?('/')
+ 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)
@@ -107,7 +121,7 @@ module ActionDispatch
@ast = ast
@anchor = anchor
@via = via
- @internal = options[:internal]
+ @internal = options.delete(:internal)
path_params = ast.find_all(&:symbol?).map(&:to_sym)
@@ -121,7 +135,7 @@ module ActionDispatch
if options_constraints.is_a?(Hash)
@defaults = Hash[options_constraints.find_all { |key, default|
- URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
+ URL_OPTIONS.include?(key) && (String === default || Integer === default)
}].merge @defaults
@blocks = blocks
constraints.merge! options_constraints
@@ -138,21 +152,16 @@ module ActionDispatch
@conditions = Hash[conditions]
@defaults = formats[:defaults].merge(@defaults).merge(normalize_defaults(options))
+ if path_params.include?(:action) && !@requirements.key?(:action)
+ @defaults[:action] ||= "index"
+ end
+
@required_defaults = (split_options[:required_defaults] || []).map(&:first)
end
def make_route(name, precedence)
- route = Journey::Route.new(name,
- application,
- path,
- conditions,
- required_defaults,
- defaults,
- request_method,
- precedence,
- @internal)
-
- route
+ Journey::Route.new(name, application, path, conditions, required_defaults,
+ defaults, request_method, precedence, @internal)
end
def application
@@ -215,7 +224,7 @@ module ActionDispatch
private
def add_wildcard_options(options, formatted, path_ast)
# Add a constraint for wildcard route to make it non-greedy and match the
- # optional format part of the route by default
+ # optional format part of the route by default.
if formatted != false
path_ast.grep(Journey::Nodes::Star).each_with_object({}) { |node, hash|
hash[node.name.to_sym] ||= /.+?/
@@ -236,7 +245,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
@@ -267,13 +276,13 @@ module ActionDispatch
{ requirements: { format: Regexp.compile(formatted) },
defaults: { format: formatted } }
else
- { requirements: { }, defaults: { } }
+ { requirements: {}, defaults: {} }
end
end
def verify_regexp_requirements(requirements)
requirements.each do |requirement|
- if requirement.source =~ ANCHOR_CHARACTERS_REGEX
+ if ANCHOR_CHARACTERS_REGEX.match?(requirement.source)
raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
end
@@ -288,24 +297,22 @@ module ActionDispatch
end
def app(blocks)
- if to.is_a?(Class) && to < ActionController::Metal
+ if to.respond_to?(:action)
Routing::RouteSet::StaticDispatcher.new to
+ elsif to.respond_to?(:call)
+ Constraints.new(to, blocks, Constraints::CALL)
+ elsif blocks.any?
+ Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
else
- if to.respond_to?(:call)
- Constraints.new(to, blocks, Constraints::CALL)
- elsif blocks.any?
- Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE)
- else
- dispatcher(defaults.key?(:controller))
- end
+ dispatcher(defaults.key?(:controller))
end
end
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
}
@@ -329,8 +336,8 @@ module ActionDispatch
end
def split_to(to)
- if to =~ /#/
- to.split('#')
+ if /#/.match?(to)
+ to.split("#")
else
[]
end
@@ -338,7 +345,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("/")
@@ -386,7 +393,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
@@ -395,7 +402,7 @@ module ActionDispatch
end
module Base
- # Matches a url pattern to one or more routes.
+ # Matches a URL pattern to one or more routes.
#
# You should not use the +match+ method in your router
# without specifying an HTTP method.
@@ -405,7 +412,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:
@@ -454,7 +461,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.
@@ -469,7 +476,17 @@ module ActionDispatch
# <tt>params[<:param>]</tt>.
# In your router:
#
- # resources :user, param: :name
+ # 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:
@@ -480,8 +497,8 @@ module ActionDispatch
# end
# end
#
- # user = User.find_by(name: 'Phusion')
- # user_path(user) # => "/users/Phusion"
+ # user = User.find_by(name: 'Phusion')
+ # user_path(user) # => "/users/Phusion"
#
# [:path]
# The path prefix for the routes.
@@ -539,10 +556,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.
@@ -565,7 +582,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.
@@ -597,7 +614,7 @@ module ActionDispatch
end
raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
- raise ArgumentError, <<-MSG.strip_heredoc unless path
+ raise ArgumentError, <<~MSG unless path
Must be called with mount point
mount SomeRackApp, at: "some_route"
@@ -611,7 +628,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
@@ -630,7 +647,7 @@ module ActionDispatch
# Query if the following named route was already defined.
def has_named_route?(name)
- @set.named_routes.key? name
+ @set.named_routes.key?(name)
end
private
@@ -650,18 +667,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)
- prefix_options[:relative_url_root] = ''.freeze
- # 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
}
@@ -713,11 +743,7 @@ module ActionDispatch
def map_method(method, args, &block)
options = args.extract_options!
options[:via] = method
- if options.key?(:defaults)
- defaults(options.delete(:defaults)) { match(*args, options, &block) }
- else
- match(*args, options, &block)
- end
+ match(*args, options, &block)
self
end
end
@@ -811,7 +837,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?
@@ -821,10 +847,10 @@ module ActionDispatch
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))
+ 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
@@ -835,7 +861,7 @@ module ActionDispatch
end
if options.key? :anchor
- raise ArgumentError, 'anchor is ignored unless passed to `match`'
+ raise ArgumentError, "anchor is ignored unless passed to `match`"
end
@scope.options.each do |option|
@@ -982,7 +1008,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:
@@ -998,67 +1024,71 @@ module ActionDispatch
end
private
- def merge_path_scope(parent, child) #:nodoc:
+ def merge_path_scope(parent, child)
Mapper.normalize_path("#{parent}/#{child}")
end
- def merge_shallow_path_scope(parent, child) #:nodoc:
+ def merge_shallow_path_scope(parent, child)
Mapper.normalize_path("#{parent}/#{child}")
end
- def merge_as_scope(parent, child) #:nodoc:
+ def merge_as_scope(parent, child)
parent ? "#{parent}_#{child}" : child
end
- def merge_shallow_prefix_scope(parent, child) #:nodoc:
+ def merge_shallow_prefix_scope(parent, child)
parent ? "#{parent}_#{child}" : child
end
- def merge_module_scope(parent, child) #:nodoc:
+ def merge_module_scope(parent, child)
parent ? "#{parent}/#{child}" : child
end
- def merge_controller_scope(parent, child) #:nodoc:
+ def merge_controller_scope(parent, child)
child
end
- def merge_action_scope(parent, child) #:nodoc:
+ def merge_action_scope(parent, child)
child
end
- def merge_via_scope(parent, child) #:nodoc:
+ def merge_via_scope(parent, child)
child
end
- def merge_format_scope(parent, child) #:nodoc:
+ def merge_format_scope(parent, child)
child
end
- def merge_path_names_scope(parent, child) #:nodoc:
+ def merge_path_names_scope(parent, child)
merge_options_scope(parent, child)
end
- def merge_constraints_scope(parent, child) #:nodoc:
+ def merge_constraints_scope(parent, child)
merge_options_scope(parent, child)
end
- def merge_defaults_scope(parent, child) #:nodoc:
+ def merge_defaults_scope(parent, child)
merge_options_scope(parent, child)
end
- def merge_blocks_scope(parent, child) #:nodoc:
+ def merge_blocks_scope(parent, child)
merged = parent ? parent.dup : []
merged << child if child
merged
end
- def merge_options_scope(parent, child) #:nodoc:
+ def merge_options_scope(parent, child)
(parent || {}).merge(child)
end
- def merge_shallow_scope(parent, child) #:nodoc:
+ def merge_shallow_scope(parent, child)
child ? true : false
end
+
+ def merge_to_scope(parent, child)
+ child
+ end
end
# Resource routing allows you to quickly declare all of the common routes
@@ -1111,6 +1141,10 @@ module ActionDispatch
attr_reader :controller, :path, :param
def initialize(entities, api_only, shallow, options = {})
+ if options[:param].to_s.include?(":")
+ raise ArgumentError, ":param option can't contain colons"
+ end
+
@name = entities.to_s
@path = (options[:path] || @name).to_s
@controller = (options[:controller] || @name).to_s
@@ -1132,10 +1166,16 @@ module ActionDispatch
end
def actions
+ if @except
+ available_actions - Array(@except).map(&:to_sym)
+ else
+ available_actions
+ end
+ end
+
+ def available_actions
if @only
Array(@only).map(&:to_sym)
- elsif @except
- default_actions - Array(@except).map(&:to_sym)
else
default_actions
end
@@ -1237,19 +1277,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
@@ -1264,15 +1304,15 @@ module ActionDispatch
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)
set_member_mappings_for_resource
+
+ collection do
+ post :create
+ end if parent_resource.actions.include?(:create)
end
end
@@ -1314,7 +1354,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.
@@ -1322,14 +1362,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.
@@ -1406,6 +1446,9 @@ module ActionDispatch
# Allows you to specify the default value for optional +format+
# segment or disable it by supplying +false+.
#
+ # [:param]
+ # Allows you to override the default param name of +:id+ in the URL.
+ #
# === Examples
#
# # routes call <tt>Admin::PostsController</tt>
@@ -1526,7 +1569,7 @@ module ActionDispatch
end
end
- # See ActionDispatch::Routing::Mapper::Scoping#namespace
+ # See ActionDispatch::Routing::Mapper::Scoping#namespace.
def namespace(path, options = {})
if resource_scope?
nested { super }
@@ -1546,22 +1589,24 @@ module ActionDispatch
!parent_resource.singleton? && @scope[:shallow]
end
- # Matches a url pattern to one or more routes.
+ # Matches a URL pattern to one or more routes.
# For more information, see match[rdoc-ref:Base#match].
#
- # match 'path' => 'controller#action', via: patch
+ # 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
@@ -1577,110 +1622,13 @@ module ActionDispatch
paths = [path] + rest
end
- 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
-
- 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
- ActiveSupport::Deprecation.warn <<-eowarn
-Specifying strings for both :path and the route path is deprecated. Change things like this:
-
- match #{_path.inspect}, :path => #{option_path.inspect}
-
-to this:
-
- match #{option_path.inspect}, :as => #{_path.inspect}, :action => #{path.inspect}
- eowarn
- route_options[:action] = _path
- route_options[:as] = _path
- _path = option_path
- 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("-", "_")
+ if options.key?(:defaults)
+ defaults(options.delete(:defaults)) { map_match(paths, options, &block) }
else
- nil
+ map_match(paths, options, &block)
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) # :nodoc:
- 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) # :nodoc:
- 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 action =~ /^[\w\-\/]+$/
- 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, ast, as, anchor)
- end
-
# You can specify what Rails should route "/" to with the root method:
#
# root to: 'pages#main'
@@ -1697,7 +1645,7 @@ to this:
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"
@@ -1714,13 +1662,13 @@ to this:
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
@@ -1754,48 +1702,48 @@ to this:
false
end
- def apply_action_options(options) # :nodoc:
+ def apply_action_options(options)
return options if action_options? options
options.merge scope_action_options
end
- def action_options?(options) #:nodoc:
+ def action_options?(options)
options[:only] || options[:except]
end
- def scope_action_options #:nodoc:
+ def scope_action_options
@scope[:action_options] || {}
end
- def resource_scope? #:nodoc:
+ def resource_scope?
@scope.resource_scope?
end
- def resource_method_scope? #:nodoc:
+ def resource_method_scope?
@scope.resource_method_scope?
end
- def nested_scope? #:nodoc:
+ def nested_scope?
@scope.nested?
end
- def with_scope_level(kind)
+ def with_scope_level(kind) # :doc:
@scope = @scope.new_level(kind)
yield
ensure
@scope = @scope.parent
end
- def resource_scope(resource) #:nodoc:
- @scope = @scope.new(:scope_level_resource => resource)
+ def resource_scope(resource)
+ @scope = @scope.new(scope_level_resource: resource)
controller(resource.resource_scope) { yield }
ensure
@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?
@@ -1803,27 +1751,27 @@ to this:
options
end
- def shallow_nesting_depth #:nodoc:
+ def shallow_nesting_depth
@scope.find_all { |node|
node.frame[:scope_level_resource]
}.count { |node| node.frame[:scope_level_resource].shallow? }
end
- def param_constraint? #:nodoc:
+ def param_constraint?
@scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
end
- def param_constraint #:nodoc:
+ def param_constraint
@scope[:constraints][parent_resource.param]
end
- def canonical_action?(action) #:nodoc:
+ def canonical_action?(action)
resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
end
- def shallow_scope #:nodoc:
- 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
yield
@@ -1831,7 +1779,7 @@ to this:
@scope = @scope.parent
end
- def path_for_action(action, path) #:nodoc:
+ def path_for_action(action, path)
return "#{@scope[:path]}/#{path}" if path
if canonical_action?(action)
@@ -1841,23 +1789,23 @@ to this:
end
end
- def action_path(name) #:nodoc:
+ def action_path(name)
@scope[:path_names][name.to_sym] || name
end
- def prefix_name_for_action(as, action) #:nodoc:
+ def prefix_name_for_action(as, action)
if as
prefix = as
elsif !canonical_action?(action)
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]
@@ -1869,7 +1817,7 @@ to this:
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
@@ -1883,7 +1831,7 @@ to this:
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)
@@ -1895,22 +1843,119 @@ to this:
end
end
- def api_only?
+ def api_only? # :doc:
@set.api_only?
end
- private
- def path_scope(path)
- @scope = @scope.new(path: merge_path_scope(@scope[:path], path))
- yield
- ensure
- @scope = @scope.parent
- end
+ def path_scope(path)
+ @scope = @scope.new(path: merge_path_scope(@scope[:path], path))
+ yield
+ ensure
+ @scope = @scope.parent
+ end
- def match_root_route(options)
- name = has_named_route?(:root) ? nil : :root
- match '/', { :as => name, :via => :get }.merge!(options)
- 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)
+ args = ["/", { as: :root, via: :get }.merge(options)]
+ match(*args)
+ end
end
# Routing Concerns allow you to declare common routes that can be reused
@@ -2001,7 +2046,7 @@ to this:
# 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
@@ -2018,10 +2063,124 @@ to this:
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, :via, :format, :options]
+ :shallow, :blocks, :defaults, :via, :format, :options, :to]
RESOURCE_SCOPES = [:resource, :resources]
RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
@@ -2038,6 +2197,14 @@ to this:
scope_level == :nested
end
+ def null?
+ @hash.nil? && @parent.nil?
+ end
+
+ def root?
+ @parent.null?
+ end
+
def resources?
scope_level == :resources
end
@@ -2088,8 +2255,7 @@ to this:
def each
node = self
- loop do
- break if node.equal? NULL
+ until node.equal? NULL
yield node
node = node.parent
end
@@ -2102,7 +2268,7 @@ to this:
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 = {}
end
@@ -2112,6 +2278,7 @@ to this:
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 d6987f4d09..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,7 +24,6 @@ module ActionDispatch
end
def serve(req)
- req.check_path_parameters!
uri = URI.parse(path(req.path_parameters, req))
unless uri.host
@@ -37,12 +38,14 @@ module ActionDispatch
uri.host ||= req.host
uri.port ||= req.port unless req.standard_port?
+ req.commit_flash
+
body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>)
headers = {
- '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*\}/)
@@ -129,21 +132,23 @@ module ActionDispatch
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 85f202b823..d0a7eadf45 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -1,12 +1,13 @@
-require 'action_dispatch/journey'
-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
@@ -34,7 +35,7 @@ module ActionDispatch
if @raise_on_name_error
raise
else
- return [404, {'X-Cascade' => 'pass'}, []]
+ [404, { "X-Cascade" => "pass" }, []]
end
end
@@ -59,7 +60,7 @@ module ActionDispatch
private
- def controller(_); @controller_class; end
+ def controller(_); @controller_class; end
end
# A NamedRouteCollection instance is a collection of named routes, and also
@@ -71,7 +72,7 @@ module ActionDispatch
private :routes
def initialize
- @routes = {}
+ @routes = {}
@path_helpers = Set.new
@url_helpers = Set.new
@url_helpers_module = Module.new
@@ -89,11 +90,11 @@ module ActionDispatch
def clear!
@path_helpers.each do |helper|
- @path_helpers_module.send :undef_method, helper
+ @path_helpers_module.remove_method helper
end
@url_helpers.each do |helper|
- @url_helpers_module.send :undef_method, helper
+ @url_helpers_module.remove_method helper
end
@routes.clear
@@ -107,8 +108,8 @@ module ActionDispatch
url_name = :"#{name}_url"
if routes.key? key
- @path_helpers_module.send :undef_method, path_name
- @url_helpers_module.send :undef_method, url_name
+ @path_helpers_module.undef_method path_name
+ @url_helpers_module.undef_method url_name
end
routes[key] = route
define_url_helper @path_helpers_module, route, path_name, route.defaults, name, PATH
@@ -144,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)
@@ -172,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
@@ -180,40 +216,40 @@ module ActionDispatch
private
- def optimized_helper(args)
- params = parameterize_args(args) do
- raise_generation_error(args)
- end
+ 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)
@@ -248,6 +284,8 @@ 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)
@@ -264,53 +302,50 @@ 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|
- last = args.last
- options = case last
- when Hash
- args.pop
- when ActionController::Parameters
- if last.permitted?
- args.pop.to_h
- else
- raise ArgumentError, "Generating a URL from non sanitized request parameters is insecure!"
- end
- end
- 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
+ # strategy for building URLs to send to the client
PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) }
UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) }
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)
@@ -342,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 self
+ @polymorphic_mappings = {}
+ end
+
+ def eager_load!
+ router.eager_load!
+ routes.each(&:eager_load!)
+ nil
end
def relative_url_root
@@ -402,6 +444,7 @@ module ActionDispatch
named_routes.clear
set.clear
formatter.clear
+ @polymorphic_mappings.clear
@prepend.each { |blk| eval_block(blk) }
end
@@ -418,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
@@ -426,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
@@ -446,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
@@ -480,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
@@ -500,7 +576,7 @@ module ActionDispatch
routes.empty?
end
- def add_route(mapping, path_ast, name, anchor)
+ def add_route(mapping, name)
raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
if name && named_routes[name]
@@ -508,7 +584,7 @@ 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
route = @set.add_route(name, mapping)
@@ -517,20 +593,58 @@ module ActionDispatch
if route.segment_keys.include?(:controller)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
Using a dynamic :controller segment in a route is deprecated and
- will be removed in Rails 5.1
+ will be removed in Rails 6.0.
MSG
end
if route.segment_keys.include?(:action)
ActiveSupport::Deprecation.warn(<<-MSG.squish)
Using a dynamic :action segment in a route is deprecated and
- will be removed in Rails 5.1
+ will be removed in Rails 6.0.
MSG
end
route
end
+ def add_polymorphic_mapping(klass, options, &block)
+ @polymorphic_mappings[klass] = CustomUrlHelper.new(klass, options, &block)
+ end
+
+ def add_url_helper(name, options, &block)
+ named_routes.add_url_helper(name, options, &block)
+ end
+
+ class CustomUrlHelper
+ attr_reader :name, :defaults, :block
+
+ def initialize(name, defaults, &block)
+ @name = name
+ @defaults = defaults
+ @block = block
+ end
+
+ def call(t, args, only_path = false)
+ options = args.extract_options!
+ url = t.full_url_for(eval_block(t, args, options))
+
+ if only_path
+ "/" + url.partition(%r{(?<!/)/(?!/)}).last
+ else
+ url
+ end
+ end
+
+ private
+ def eval_block(t, args, options)
+ t.instance_exec(*args, merge_defaults(options), &block)
+ end
+
+ def merge_defaults(options)
+ defaults ? defaults.merge(options) : options
+ end
+ end
+
class Generator
PARAMETERIZE = lambda do |name, value|
if name == :controller
@@ -548,12 +662,10 @@ module ActionDispatch
@recall = recall
@set = set
- normalize_recall!
normalize_options!
normalize_controller_action_id!
use_relative_controller!
normalize_controller!
- normalize_action!
end
def controller
@@ -572,11 +684,6 @@ module ActionDispatch
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
@@ -588,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
@@ -603,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
@@ -612,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("/")
@@ -622,7 +729,7 @@ module ActionDispatch
# Remove leading slashes from controllers
def normalize_controller!
if controller
- if controller.start_with?("/".freeze)
+ if controller.start_with?("/")
@options[:controller] = controller[1..-1]
else
@options[:controller] = controller
@@ -630,13 +737,6 @@ module ActionDispatch
end
end
- # Move 'index' action from options to recall
- def normalize_action!
- if @options[:action] == 'index'.freeze
- @recall[:action] = @options.delete(:action)
- end
- end
-
# Generates a path from routes, returns [path, params].
# If no route is generated the formatter will raise ActionController::UrlGenerationError
def generate
@@ -660,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
@@ -684,7 +784,7 @@ module ActionDispatch
end
def find_script_name(options)
- options.delete(:script_name) || find_relative_url_root(options) || ''
+ options.delete(:script_name) || find_relative_url_root(options) || ""
end
def find_relative_url_root(options)
@@ -706,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
@@ -745,12 +845,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 = 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|
@@ -759,8 +863,7 @@ module ActionDispatch
params[key] = URI.parser.unescape(value)
end
end
- old_params = req.path_parameters
- req.path_parameters = old_params.merge params
+ req.path_parameters = params
app = route.app
if app.matches?(req) && app.dispatcher?
begin
@@ -770,10 +873,15 @@ module ActionDispatch
end
return req.path_parameters
+ elsif app.matches?(req) && app.engine?
+ path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras, raise_on_missing: false)
+ return path_parameters if path_parameters
end
end
- raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ if raise_on_missing
+ raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ end
end
end
# :startdoc:
diff --git a/actionpack/lib/action_dispatch/routing/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 28be189f93..fcb8ae296b 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Routing
# In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse
@@ -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
@@ -131,6 +133,7 @@ module ActionDispatch
# <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1.
# * <tt>:port</tt> - Optionally specify the port to connect to.
# * <tt>:anchor</tt> - An anchor name to be appended to the path.
+ # * <tt>:params</tt> - The query parameters to be appended to the path.
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
# * <tt>:script_name</tt> - Specifies application path relative to domain root. If provided, prepends application path.
#
@@ -153,7 +156,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,20 +167,17 @@ 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
- route_name = options.delete :use_route
- _routes.url_for(options.symbolize_keys.reverse_merge!(url_options),
- route_name)
- when ActionController::Parameters
- unless options.permitted?
- raise ArgumentError.new("Generating a URL from non sanitized request parameters is insecure!")
- end
+ when Hash, ActionController::Parameters
route_name = options.delete :use_route
- _routes.url_for(options.to_h.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
@@ -192,22 +192,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..066daa4a12
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_test_case.rb
@@ -0,0 +1,167 @@
+# 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
+ #
+ # Some drivers require browser capabilities to be passed as a block instead
+ # of through the +options+ hash.
+ #
+ # As an example, if you want to add mobile emulation on chrome, you'll have to
+ # create an instance of selenium's +Chrome::Options+ object and add
+ # capabilities with a block.
+ #
+ # The block will be passed an instance of <tt><Driver>::Options</tt> where you can
+ # define the capabilities you want. Please refer to your driver documentation
+ # to learn about supported options.
+ #
+ # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ # driven_by :selenium, using: :chrome, screen_size: [1024, 768] do |driver_option|
+ # driver_option.add_emulation(device_name: 'iPhone 6')
+ # driver_option.add_extension('path/to/chrome_extension.crx')
+ # end
+ # end
+ #
+ # Because <tt>ActionDispatch::SystemTestCase</tt> is a shim between Capybara
+ # and Rails, any driver that is supported by Capybara is supported by system
+ # tests as long as you include the required gems and files.
+ 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: {}, &capabilities)
+ driver_options = { using: using, screen_size: screen_size, options: options }
+
+ self.driver = SystemTesting::Driver.new(driver, driver_options, &capabilities)
+ 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..c34907b6cb
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/browser.rb
@@ -0,0 +1,57 @@
+# 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
+
+ def capabilities
+ @option ||=
+ case type
+ when :chrome
+ ::Selenium::WebDriver::Chrome::Options.new
+ when :firefox
+ ::Selenium::WebDriver::Firefox::Options.new
+ end
+ end
+
+ private
+ def headless_chrome_browser_options
+ capabilities.args << "--headless"
+ capabilities.args << "--disable-gpu" if Gem.win_platform?
+
+ capabilities
+ end
+
+ def headless_firefox_browser_options
+ capabilities.args << "-headless"
+
+ capabilities
+ 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..25a09dd918
--- /dev/null
+++ b/actionpack/lib/action_dispatch/system_testing/driver.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module ActionDispatch
+ module SystemTesting
+ class Driver # :nodoc:
+ def initialize(name, **options, &capabilities)
+ @name = name
+ @browser = Browser.new(options[:using])
+ @screen_size = options[:screen_size]
+ @options = options[:options]
+ @capabilities = capabilities
+ end
+
+ def use
+ register if registerable?
+
+ setup
+ end
+
+ private
+ def registerable?
+ [:selenium, :poltergeist, :webkit].include?(@name)
+ end
+
+ def register
+ define_browser_capabilities(@browser.capabilities)
+
+ 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 define_browser_capabilities(capabilities)
+ @capabilities.call(capabilities) if @capabilities
+ 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..79359a0c8b
--- /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-to-html/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
index 3fb81ff083..dc019db6ac 100644
--- a/actionpack/lib/action_dispatch/testing/assertion_response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertion_response.rb
@@ -1,14 +1,9 @@
+# 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.
- #
- # As an input to the initializer, we take a Fixnum, a String, or a Symbol.
- # If it's a Fixnum or String, we figure out what its symbolized name.
- # If it's a Symbol, we figure out what its corresponding code is.
- # The resulting code will be a Fixnum, for real HTTP codes, and it will
- # be a String for the pseudo-HTTP codes, such as:
- # :success, :missing, :redirect and :error
+ # 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
@@ -19,6 +14,9 @@ module ActionDispatch
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
@@ -38,12 +36,12 @@ module ActionDispatch
private
- def code_from_name(name)
- GENERIC_RESPONSE_CODES[name] || Rack::Utils::SYMBOL_TO_STATUS_CODE[name]
- end
+ 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
+ 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 fae266273e..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.to_s =~ /xml\z/
+ @html_document ||= if @response.content_type.to_s.end_with?("xml")
Nokogiri::XML::Document.parse(@response.body)
else
Nokogiri::HTML::Document.parse(@response.body)
diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb
index cd55b7d975..8595ea03cf 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionDispatch
module Assertions
# A small suite of assertions that test responses from \Rails applications.
@@ -45,12 +47,12 @@ module ActionDispatch
# # Asserts that the redirection was to the named route login_url
# assert_redirected_to login_url
#
- # # Asserts that the redirection was to the url for @customer
+ # # Asserts that the redirection was to the URL for @customer
# assert_redirected_to @customer
#
# # Asserts that the redirection matches the regular expression
# 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,13 +79,17 @@ module ActionDispatch
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
+ (+"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?
+ return "" unless @response.redirection? && @response.location.present?
location = normalize_argument_to_redirection(@response.location)
" redirect to <#{location}>"
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index 44ad2c10d8..28cde6704e 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+.
#
@@ -18,8 +25,8 @@ module ActionDispatch
# assert_recognizes({controller: 'items', action: 'create'}, {path: 'items', method: :post})
#
# You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
- # to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the
- # extras argument, appending the query string on the path directly will not work. For example:
+ # to assert that values in the query string will end up in the params hash correctly. To test query strings you must use the extras
+ # argument because appending the query string on the path directly will not work. For example:
#
# # Asserts that a path of '/items/list/1?view=print' returns the correct options
# assert_recognizes({controller: 'items', action: 'list', id: '1', view: 'print'}, 'items/list/1', { view: "print" })
@@ -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,14 +82,14 @@ 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.
@@ -119,7 +126,7 @@ module ActionDispatch
#
# # 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]
@@ -127,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>:
@@ -152,8 +158,18 @@ 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
+ view_context_class = Class.new(@controller.view_context_class) do
+ include _routes.url_helpers
+ end
+
+ custom_view_context = Module.new {
+ define_method(:view_context_class) do
+ view_context_class
+ end
+ }
+ @controller.extend(custom_view_context)
end
end
yield @routes
@@ -183,10 +199,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"
@@ -202,7 +217,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 60c562d7cd..bb8b43ad4d 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)
- end
-
- # Performs an XMLHttpRequest request with the given parameters, mirroring
- # an AJAX request made from JavaScript.
- #
- # 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)
+ process(:head, path, *args)
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)
+ get(response.location, **args)
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?
- 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 Rails 5.1. 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 Rails 5.1. 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 Rails 5.1. 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 Rails 5.1. 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 Rails 5.1. 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,168 +175,132 @@ 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)
- 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
-
- 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) }
- 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(', ')}
-
- Examples:
-
- get '/profile',
- params: { id: 1 },
- headers: { 'X-Extra-Header' => '123' },
- env: { 'action_dispatch.custom' => 'custom' },
- xhr: true
- MSG
+ # - +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 appropriate 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
- # Performs the actual request.
- def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil)
- request_encoder = RequestEncoder.encoder(as)
-
- if path =~ %r{://}
- location = URI.parse(path)
+ if %r{://}.match?(path)
+ path = build_expanded_path(path) do |location|
https! URI::HTTPS === location if location.scheme
+
if url_host = location.host
default = Rack::Request::DEFAULT_PORTS[location.scheme]
url_host += ":#{location.port}" if default != location.port
host! url_host
end
- path = request_encoder.append_format_to location.path
- path = location.query ? "#{path}?#{location.query}" : path
- else
- path = request_encoder.append_format_to path
- end
-
- hostname, port = host.split(':')
-
- request_env = {
- :method => method,
- :params => request_encoder.encode_params(params),
-
- "SERVER_NAME" => hostname,
- "SERVER_PORT" => port || (https? ? "443" : "80"),
- "HTTPS" => https? ? "on" : "off",
- "rack.url_scheme" => https? ? "https" : "http",
-
- "REQUEST_URI" => path,
- "HTTP_HOST" => host,
- "REMOTE_ADDR" => remote_addr,
- "CONTENT_TYPE" => request_encoder.content_type,
- "HTTP_ACCEPT" => accept
- }
-
- if xhr
- headers ||= {}
- headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
- headers['HTTP_ACCEPT'] ||= [Mime[:js], Mime[:html], Mime[:xml], 'text/xml', '*/*'].join(', ')
end
+ end
- # this modifies the passed request_env directly
- if headers.present?
- Http::Headers.from_hash(request_env).merge!(headers)
- end
- if env.present?
- Http::Headers.from_hash(request_env).merge!(env)
- end
+ hostname, port = host.split(":")
- session = Rack::Test::Session.new(_mock_session)
+ request_env = {
+ :method => method,
+ :params => request_encoder.encode_params(params),
- # 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)
+ "SERVER_NAME" => hostname,
+ "SERVER_PORT" => port || (https? ? "443" : "80"),
+ "HTTPS" => https? ? "on" : "off",
+ "rack.url_scheme" => https? ? "https" : "http",
- @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
- @response.response_parser = RequestEncoder.parser(@response.content_type)
- @html_document = nil
- @url_options = nil
+ "REQUEST_URI" => path,
+ "HTTP_HOST" => host,
+ "REMOTE_ADDR" => remote_addr,
+ "CONTENT_TYPE" => request_encoder.content_type,
+ "HTTP_ACCEPT" => request_encoder.accept_header || accept
+ }
- @controller = @request.controller_instance
+ wrapped_headers = Http::Headers.from_hash({})
+ wrapped_headers.merge!(headers) if headers
- response.status
+ if xhr
+ wrapped_headers["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest"
+ wrapped_headers["HTTP_ACCEPT"] ||= [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(", ")
end
- def build_full_uri(path, env)
- "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
+ # 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
- class RequestEncoder # :nodoc:
- @encoders = {}
-
- attr_reader :response_parser
-
- def initialize(mime_name, param_encoder, response_parser, url_encoded_form = false)
- @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
-
- @url_encoded_form = url_encoded_form
- @path_format = ".#{@mime.symbol}" unless @url_encoded_form
- @response_parser = response_parser || -> body { body }
- @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc
- end
+ session = Rack::Test::Session.new(_mock_session)
- def append_format_to(path)
- path << @path_format unless @url_encoded_form
- path
- 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)
- def content_type
- @mime.to_s
- end
+ @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
- def encode_params(params)
- @param_encoder.call(params)
- end
+ @controller = @request.controller_instance
- def self.parser(content_type)
- mime = Mime::Type.lookup(content_type)
- encoder(mime ? mime.ref : nil).response_parser
- end
+ response.status
+ end
- def self.encoder(name)
- @encoders[name] || WWWFormEncoder
- end
+ # Set the host name to use in the next request.
+ #
+ # session.host! "www.example.com"
+ alias :host! :host=
- def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil)
- @encoders[mime_name] = new(mime_name, param_encoder, response_parser)
- end
+ private
+ def _mock_session
+ @_mock_session ||= Rack::MockSession.new(@app, host)
+ end
- register_encoder :json, response_parser: -> body { JSON.parse(body) }
+ def build_full_uri(path, env)
+ "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
+ end
- WWWFormEncoder = new(:url_encoded_form, -> params { params }, nil, true)
+ 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
@@ -452,9 +311,13 @@ module ActionDispatch
attr_reader :app
+ def initialize(*args, &blk)
+ super(*args, &blk)
+ @integration_session = nil
+ end
+
def before_setup # :nodoc:
@app = nil
- @integration_session = nil
super
end
@@ -470,9 +333,9 @@ 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 app.respond_to?(:routes)
+ # 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) && app.routes.is_a?(ActionDispatch::Routing::RouteSet)
include app.routes.url_helpers
include app.routes.mounted_helpers
end
@@ -484,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
@@ -510,6 +372,7 @@ module ActionDispatch
# simultaneously.
def open_session
dup.tap do |session|
+ session.reset!
yield session if block_given?
end
end
@@ -530,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
@@ -699,42 +563,50 @@ 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'
+ # require "test_helper"
#
# class ApiTest < ActionDispatch::IntegrationTest
- # test 'creates articles' do
+ # test "creates articles" do
# assert_difference -> { Article.count } do
- # post articles_path, params: { article: { title: 'Ahoy!' } }, as: :json
+ # post articles_path, params: { article: { title: "Ahoy!" } }, as: :json
# end
#
# assert_response :success
- # assert_equal({ id: Arcticle.last.id, title: 'Ahoy!' }, response.parsed_body)
+ # assert_equal({ id: Article.last.id, title: "Ahoy!" }, response.parsed_body)
# end
# end
#
- # The `as` option sets the format to JSON, sets the content type to
- # 'application/json' and encodes the parameters as JSON.
+ # 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 as what
- # the last request was encoded as. If the request wasn't encoded `as` something,
- # it's the same as calling `body`.
+ # Calling +parsed_body+ on the response parses the response body based on the
+ # last response MIME type.
#
- # For any custom MIME Types you've registered, you can even add your own encoders with:
+ # Out of the box, only <tt>:json</tt> is supported. But for any custom MIME
+ # types you've registered, you can add your own encoders with:
#
# ActionDispatch::IntegrationTest.register_encoder :wibble,
# param_encoder: -> params { params.to_wibble },
# 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`.
+ # 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 TestProcess::FixtureFile
+
module UrlOptions
extend ActiveSupport::Concern
def url_options
@@ -757,7 +629,11 @@ module ActionDispatch
module ClassMethods
def app
- defined?(@@app) ? @@app : ActionDispatch.test_app
+ if defined?(@@app) && @@app
+ @@app
+ else
+ ActionDispatch.test_app
+ end
end
def app=(app)
@@ -765,7 +641,7 @@ module ActionDispatch
end
def register_encoder(*args)
- Integration::Session::RequestEncoder.register_encoder(*args)
+ RequestEncoder.register_encoder(*args)
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..6c65bec62f
--- /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)
+ type = Mime::Type.lookup(content_type).ref if content_type
+ encoder(type).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 1ecd7d14a7..0b98f27f11 100644
--- a/actionpack/lib/action_dispatch/testing/test_process.rb
+++ b/actionpack/lib/action_dispatch/testing/test_process.rb
@@ -1,8 +1,30 @@
-require 'action_dispatch/middleware/cookies'
-require 'action_dispatch/middleware/flash'
+# 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,
@@ -24,20 +46,5 @@ module ActionDispatch
def redirect_to_url
@response.redirect_url
end
-
- # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>:
- #
- # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png')
- #
- # To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
- # This will not affect other platforms:
- #
- # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary)
- def fixture_file_upload(path, mime_type = nil, binary = false)
- if self.class.respond_to?(:fixture_path) && self.class.fixture_path
- 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 9d4b73a43d..6f7c86fdcf 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
@@ -10,19 +14,12 @@ module ActionDispatch
new response.status, response.headers, response.body
end
- # Was the response successful?
- alias_method :success?, :successful?
-
- # Was the URL not found?
- alias_method :missing?, :not_found?
-
- # Was there a server-side error?
- alias_method :error?, :server_error?
-
- attr_writer :response_parser # :nodoc:
-
def parsed_body
- @parsed_body ||= @response_parser.call(body)
+ @parsed_body ||= response_parser.call(body)
+ end
+
+ def response_parser
+ @response_parser ||= RequestEncoder.parser(content_type)
end
end
end
diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb
index 941877d10d..36ee77c693 100644
--- a/actionpack/lib/action_pack.rb
+++ b/actionpack/lib/action_pack.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2019 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 157f401f54..3bbb1734d9 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 = "beta3"
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 edbb84d462..6db045fcd7 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
@@ -28,7 +30,7 @@ module AbstractController
end
test "register mime types on method missing" do
- AbstractController::Collector.send(:remove_method, :js)
+ AbstractController::Collector.remove_method :js
begin
collector = MyCollector.new
assert_not_respond_to collector, :js
@@ -55,7 +57,7 @@ module AbstractController
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[: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 1435928578..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,21 +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",
},
},
- },
- })
+ })
end
def test_action_controller_base_responds_to_translate
@@ -44,30 +45,31 @@ module AbstractController
def test_lazy_lookup
@controller.stub :action_name, :index do
- assert_equal 'bar', @controller.t('.foo')
+ assert_equal "bar", @controller.t(".foo")
end
end
def test_lazy_lookup_with_symbol
@controller.stub :action_name, :index do
- assert_equal 'bar', @controller.t(:'.foo')
+ assert_equal "bar", @controller.t(:'.foo')
end
end
def test_lazy_lookup_fallback
@controller.stub :action_name, :index do
- assert_equal 'no_action_tr', @controller.t(:'.no_action')
+ assert_equal "no_action_tr", @controller.t(:'.no_action')
end
end
def test_default_translation
@controller.stub :action_name, :index do
- assert_equal 'bar', @controller.t('one.two')
+ 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'
+ 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
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index fcbbfe8a18..f23151e518 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -1,42 +1,35 @@
-$:.unshift(File.dirname(__FILE__) + '/lib')
-$:.unshift(File.dirname(__FILE__) + '/fixtures/helpers')
-$:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers')
+# frozen_string_literal: true
-require 'active_support/core_ext/kernel/reporting'
+$:.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"
# 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"
-end
-
-require 'drb'
-begin
- require 'drb/unix'
-rescue LoadError
- puts "'drb/unix' is not available"
+ Encoding.default_internal = Encoding::UTF_8
+ Encoding.default_external = Encoding::UTF_8
end
-if ENV['TRAVIS']
+if ENV["TRAVIS"]
PROCESS_COUNT = 0
else
- PROCESS_COUNT = (ENV['N'] || 4).to_i
+ 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
@@ -44,7 +37,7 @@ module Rails
@_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test")
end
- def root; end;
+ def root; end
end
end
@@ -58,17 +51,13 @@ 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)'
+ get ":controller(/:action)"
end
end
@@ -84,7 +73,7 @@ end
module ActiveSupport
class TestCase
if RUBY_ENGINE == "ruby" && PROCESS_COUNT > 0
- parallelize_me!
+ parallelize(workers: PROCESS_COUNT)
end
end
end
@@ -110,6 +99,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
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
@@ -119,7 +109,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
app.routes.draw do
ActiveSupport::Deprecation.silence do
- get ':controller(/:action)'
+ get ":controller(/:action)"
end
end
@@ -127,27 +117,19 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
# Stub Rails dispatcher so it does not get controller references and
# simply return the controller#action as Rack::Body.
class NullController < ::ActionController::Metal
- def initialize(controller_name)
- @controller = controller_name
- end
-
- def make_response!(request)
- self.class.make_response! request
- end
-
- def dispatch(action, req, res)
- [200, {'Content-Type' => 'text/html'}, ["#{@controller}##{action}"]]
+ def self.dispatch(action, req, res)
+ [200, { "Content-Type" => "text/html" }, ["#{req.params[:controller]}##{action}"]]
end
end
- class NullControllerRequest < DelegateClass(ActionDispatch::Request)
+ class NullControllerRequest < ActionDispatch::Request
def controller_class
- NullController.new params[:controller]
+ NullController
end
end
- def make_request env
- NullControllerRequest.new super
+ def make_request(env)
+ NullControllerRequest.new env
end
end
@@ -164,12 +146,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
@@ -177,7 +159,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
@@ -188,7 +170,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
@@ -243,6 +225,7 @@ module ActionController
routes = ActionDispatch::Routing::RouteSet.new
routes.draw(&block)
include routes.url_helpers
+ routes
end
end
@@ -252,36 +235,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
@@ -291,16 +255,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}"
@@ -309,23 +273,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
@@ -333,7 +297,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)
@@ -371,125 +335,50 @@ module RoutingTestHelpers
private
- def make_request(env)
- Request.new super, url_helpers, @block, strict
- end
+ def make_request(env)
+ Request.new super, url_helpers, @block, strict
+ end
end
end
-class MetalRenderingController < ActionController::Metal
- include AbstractController::Rendering
- include ActionController::Rendering
- include ActionController::Renderers
-end
-
class ResourcesController < ActionController::Base
def index() head :ok end
alias_method :show, :index
end
-class ThreadsController < ResourcesController; end
-class MessagesController < ResourcesController; end
class CommentsController < ResourcesController; end
-class ReviewsController < ResourcesController; end
-
-class AccountsController < ResourcesController; end
-class AdminController < ResourcesController; end
-class ProductsController < ResourcesController; end
+class AccountsController < ResourcesController; end
class ImagesController < 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 "active_support/testing/method_call_assertions"
-require 'active_support/testing/method_call_assertions'
-
-class ForkingExecutor
- class Server
- include DRb::DRbUndumped
-
- def initialize
- @queue = Queue.new
- end
+class ActiveSupport::TestCase
+ include ActiveSupport::Testing::MethodCallAssertions
- def record reporter, result
- reporter.record result
+ private
+ # Skips the current run on Rubinius using Minitest::Assertions#skip
+ def rubinius_skip(message = "")
+ skip message if RUBY_ENGINE == "rbx"
end
- def << o
- o[2] = DRbObject.new(o[2]) if o
- @queue << o
+ # Skips the current run on JRuby using Minitest::Assertions#skip
+ def jruby_skip(message = "")
+ skip message if defined?(JRUBY_VERSION)
end
- def pop; @queue.pop; end
- end
-
- 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
- end
+end
- def << work; @queue << work; end
-
- def shutdown
- pool = @size.times.map {
- fork {
- DRb.stop_service
- 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
- if result.error?
- translate_exceptions result
- end
- queue.record reporter, result
- end
- }
- }
- @size.times { @queue << nil }
- pool.each { |pid| Process.waitpid pid }
- end
+class DrivenByRackTest < ActionDispatch::SystemTestCase
+ driven_by :rack_test
+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
+class DrivenBySeleniumWithChrome < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :chrome
end
-if RUBY_ENGINE == "ruby" && PROCESS_COUNT > 0
- # Use N processes (N defaults to 4)
- Minitest.parallel_executor = ForkingExecutor.new(PROCESS_COUNT)
+class DrivenBySeleniumWithHeadlessChrome < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :headless_chrome
end
-class ActiveSupport::TestCase
- include ActiveSupport::Testing::MethodCallAssertions
+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 579ce0ed29..261579dce5 100644
--- a/actionpack/test/assertions/response_assertions_test.rb
+++ b/actionpack/test/assertions/response_assertions_test.rb
@@ -1,15 +1,18 @@
-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, :location) do
+ 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|
@@ -26,7 +29,7 @@ module ActionDispatch
def test_assert_response_predicate_methods
[:success, :missing, :redirect, :error].each do |sym|
- @response = FakeResponse.new RESPONSE_PREDICATES[sym].to_s.sub(/\?/, '').to_sym
+ @response = FakeResponse.new RESPONSE_PREDICATES[sym].to_s.sub(/\?/, "").to_sym
assert_response sym
assert_raises(Minitest::Assertion) {
@@ -35,7 +38,7 @@ module ActionDispatch
end
end
- def test_assert_response_fixnum
+ def test_assert_response_integer
@response = FakeResponse.new 400
assert_response 400
@@ -92,7 +95,7 @@ module ActionDispatch
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'
+ @response.location = "http://test.host/posts/redirect/1"
error = assert_raises(Minitest::Assertion) { assert_response :success }
expected = "Expected response to be a <2XX: success>,"\
@@ -104,7 +107,7 @@ module ActionDispatch
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'
+ @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>,"\
@@ -112,6 +115,27 @@ module ActionDispatch
" 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 db71aa2160..51286155b9 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,14 +36,14 @@ 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...'
+ flash["hello"] = "my name is inigo montoya..."
render plain: "Inconceivable!"
end
@@ -53,7 +54,7 @@ class ActionPackAssertionsController < ActionController::Base
def assign_this
@howdy = "ho"
- render :inline => "Mr. Henke"
+ render inline: "Mr. Henke"
end
def render_based_on_parameters
@@ -69,7 +70,7 @@ class ActionPackAssertionsController < ActionController::Base
end
def session_stuffing
- session['xmas'] = 'turkey'
+ session["xmas"] = "turkey"
render plain: "ho ho ho"
end
@@ -84,11 +85,11 @@ class ActionPackAssertionsController < ActionController::Base
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,7 +98,7 @@ end
# is expecting something other than an error.
class AssertResponseWithUnexpectedErrorController < ActionController::Base
def index
- raise 'FAIL'
+ raise "FAIL"
end
def show
@@ -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,53 +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 "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'
+ 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
@@ -203,10 +227,10 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
with_routing do |set|
set.draw do
- get 'admin/inner_module', :to => 'admin/inner_module#index', :as => :admin_inner_module
+ get "admin/inner_module", to: "admin/inner_module#index", as: :admin_inner_module
ActiveSupport::Deprecation.silence do
- get ':controller/:action'
+ get ":controller/:action"
end
end
process :redirect_to_index
@@ -220,10 +244,10 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
with_routing do |set|
set.draw do
- get '/action_pack_assertions/:id', :to => 'action_pack_assertions#index', :as => :top_level
+ get "/action_pack_assertions/:id", to: "action_pack_assertions#index", as: :top_level
ActiveSupport::Deprecation.silence do
- get ':controller/:action'
+ get ":controller/:action"
end
end
process :redirect_to_top_level_named_route
@@ -239,77 +263,75 @@ 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 "/user/:id", to: "user#index", as: :top_level
ActiveSupport::Deprecation.silence do
- get ':controller/:action'
+ 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
def test_assert_redirect_failure_message_with_protocol_relative_url
- begin
- process :redirect_external_protocol_relative
- assert_redirected_to "/foo"
- rescue ActiveSupport::TestCase::Assertion => ex
- assert_no_match(
- /#{request.protocol}#{request.host}\/\/www.rubyonrails.org/,
- ex.message,
- 'protocol relative url was incorrectly normalized'
- )
- end
+ process :redirect_external_protocol_relative
+ assert_redirected_to "/foo"
+ rescue ActiveSupport::TestCase::Assertion => ex
+ assert_no_match(
+ /#{request.protocol}#{request.host}\/\/www.rubyonrails.org/,
+ ex.message,
+ "protocol relative URL was incorrectly normalized"
+ )
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
@@ -319,46 +341,46 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
def test_server_error_response_code
process :response500
- assert @response.server_error?
+ assert_predicate @response, :server_error?
process :response599
- assert @response.server_error?
+ assert_predicate @response, :server_error?
process :response404
- assert !@response.server_error?
+ assert_not_predicate @response, :server_error?
end
def test_missing_response_code
process :response404
- assert @response.not_found?
+ assert_predicate @response, :not_found?
end
def test_client_error_response_code
process :response404
- assert @response.client_error?
+ assert_predicate @response, :client_error?
end
def test_redirect_url_match
process :redirect_external
- assert @response.redirect?
+ assert_predicate @response, :redirect?
assert_match(/rubyonrails/, @response.redirect_url)
- assert !/perloffrails/.match(@response.redirect_url)
+ assert_no_match(/perloffrails/, @response.redirect_url)
end
def test_redirection
process :redirect_internal
- assert @response.redirect?
+ assert_predicate @response, :redirect?
process :redirect_external
- assert @response.redirect?
+ assert_predicate @response, :redirect?
process :nothing
- assert !@response.redirect?
+ assert_not_predicate @response, :redirect?
end
def test_successful_response_code
process :nothing
- assert @response.successful?
+ assert_predicate @response, :successful?
end
def test_response_object
@@ -376,24 +398,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
@@ -404,43 +426,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
@@ -449,21 +471,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 b4f1673be0..e366ce9532 100644
--- a/actionpack/test/controller/api/conditional_get_test.rb
+++ b/actionpack/test/controller/api/conditional_get_test.rb
@@ -1,6 +1,8 @@
-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
@@ -17,9 +19,9 @@ class ConditionalGetApiController < ActionController::API
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 53b3a0c3cc..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
@@ -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 911a8144b2..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
@@ -21,11 +23,7 @@ class RenderersApiController < ActionController::API
end
def plain
- render plain: 'Hi from plain', status: 500
- end
-
- def text
- render text: 'Hi from text', status: 500
+ render plain: "Hi from plain", status: 500
end
end
@@ -35,26 +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
-
- def test_render_text
- assert_deprecated do
- get :text
- end
- assert_response :internal_server_error
- assert_equal('Hi from text', @response.body)
+ 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 577a3d5800..d8cea10153 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
@@ -58,17 +66,16 @@ class ActionMissingController < ActionController::Base
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,13 +87,13 @@ 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
@@ -100,9 +107,9 @@ class ControllerInstanceTests < ActiveSupport::TestCase
end
def test_performed?
- assert !@empty.performed?
+ assert_not_predicate @empty, :performed?
@empty.response_body = ["sweet"]
- assert @empty.performed?
+ assert_predicate @empty, :performed?
end
def test_action_methods
@@ -112,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
@@ -152,40 +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,
+ params: { "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 "from_view", to: "url_options#from_view", as: :from_view
ActiveSupport::Deprecation.silence do
- get ':controller/:action'
+ 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
@@ -195,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
@@ -205,24 +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 "from_view", to: "default_url_options#from_view", as: :from_view
ActiveSupport::Deprecation.silence do
- get ':controller/:action'
+ 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
@@ -234,23 +262,23 @@ class DefaultUrlOptionsTest < ActionController::TestCase
end
ActiveSupport::Deprecation.silence do
- get ':controller/:action'
+ 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
@@ -259,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
@@ -272,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
@@ -289,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 754ac144cc..f09e812147 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,117 @@ 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')
+ 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 +165,9 @@ class FunctionalCachingController < CachingController
end
end
+ def xml_fragment_cached_with_html_partial
+ end
+
def formatted_fragment_cached
respond_to do |format|
format.html
@@ -184,6 +198,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
@@ -197,7 +212,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", "html")}/fragment")
end
def test_fragment_caching_in_partials
@@ -206,7 +221,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", "html")}/test.host/functional_caching/html_fragment_cached_with_partial"))
end
def test_skipping_fragment_cache_digesting
@@ -219,12 +234,15 @@ CACHED
end
def test_fragment_caching_with_options
+ time = Time.now
get :fragment_cached_with_options
assert_response :success
expected_body = "<body>\n<p>ERB</p>\n</body>\n"
assert_equal expected_body, @response.body
- assert_equal "<p>ERB</p>", @store.read("views/with_options")
+ Time.stub(:now, time + 11) do
+ assert_nil @store.read("views/with_options")
+ end
end
def test_render_inline_before_fragment_caching
@@ -233,7 +251,7 @@ CACHED
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", "html")}/test.host/functional_caching/inline_fragment_cached"))
end
def test_fragment_cache_instrumentation
@@ -253,50 +271,56 @@ CACHED
end
def test_html_formatted_fragment_caching
- get :formatted_fragment_cached, format: "html"
+ format = "html"
+ get :formatted_fragment_cached, format: format
assert_response :success
expected_body = "<body>\n<p>ERB</p>\n</body>\n"
assert_equal expected_body, @response.body
assert_equal "<p>ERB</p>",
- @store.read("views/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", format)}/fragment")
end
def test_xml_formatted_fragment_caching
- get :formatted_fragment_cached, format: "xml"
+ format = "xml"
+ get :formatted_fragment_cached, format: format
assert_response :success
expected_body = "<body>\n <p>Builder</p>\n</body>\n"
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", format)}/fragment")
end
-
def test_fragment_caching_with_variant
- get :formatted_fragment_cached_with_variant, format: "html", params: { v: :phone }
+ format = "html"
+ get :formatted_fragment_cached_with_variant, format: format, params: { v: :phone }
assert_response :success
expected_body = "<body>\n<p>PHONE</p>\n</body>\n"
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", format)}/fragment")
+ end
+
+ def test_fragment_caching_with_html_partials_in_xml
+ get :xml_fragment_cached_with_html_partial, format: "*/*"
+ assert_response :success
end
private
- def template_digest(name)
- ActionView::Digestor.digest(name: name, finder: @controller.lookup_context)
+ def template_digest(name, format)
+ ActionView::Digestor.digest(name: name, format: format, finder: @controller.lookup_context)
end
end
class CacheHelperOutputBufferTest < ActionController::TestCase
-
class MockController
def read_fragment(name, options)
- return false
+ false
end
def write_fragment(name, fragment, options)
@@ -312,9 +336,9 @@ class CacheHelperOutputBufferTest < ActionController::TestCase
output_buffer = ActionView::OutputBuffer.new
controller = MockController.new
cache_helper = Class.new do
- def self.controller; end;
- def self.output_buffer; end;
- def self.output_buffer=; end;
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
end
cache_helper.extend(ActionView::Helpers::CacheHelper)
@@ -322,7 +346,7 @@ class CacheHelperOutputBufferTest < ActionController::TestCase
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 }
+ cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil }
end
end
end
@@ -333,9 +357,9 @@ class CacheHelperOutputBufferTest < ActionController::TestCase
output_buffer = ActiveSupport::SafeBuffer.new
controller = MockController.new
cache_helper = Class.new do
- def self.controller; end;
- def self.output_buffer; end;
- def self.output_buffer=; end;
+ def self.controller; end
+ def self.output_buffer; end
+ def self.output_buffer=; end
end
cache_helper.extend(ActionView::Helpers::CacheHelper)
@@ -343,7 +367,7 @@ class CacheHelperOutputBufferTest < ActionController::TestCase
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 }
+ cache_helper.send :fragment_for, "Test fragment name", "Test fragment", &Proc.new { nil }
end
end
end
@@ -361,7 +385,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
@@ -373,26 +397,31 @@ 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_in_controller
- @customers = [Customer.new('david', 1)]
- render partial: 'customers/customer', collection: @customers, cached: true
+ @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, cached: true
+ @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
@@ -405,7 +434,7 @@ class AutomaticCollectionCacheTest < ActionController::TestCase
def test_collection_fetches_cached_views
get :index
assert_equal 1, @controller.partial_rendered_times
- assert_customer_cached 'david/1', 'david, 1'
+ assert_match "david, 1", ActionView::PartialRenderer.collection_cache.read("views/customers/_customer:7c228ab609f0baf0b1f2367469210937/david/1")
get :index
assert_equal 1, @controller.partial_rendered_times
@@ -414,17 +443,17 @@ class AutomaticCollectionCacheTest < ActionController::TestCase
def test_preserves_order_when_reading_from_cache_plus_rendering
get :index, params: { id: 2 }
assert_equal 1, @controller.partial_rendered_times
- assert_select ':root', 'david, 2'
+ assert_select ":root", "david, 2"
get :index_ordered
assert_equal 3, @controller.partial_rendered_times
- assert_select ':root', "david, 1\n david, 2\n david, 3"
+ assert_select ":root", "david, 1\n david, 2\n david, 3"
end
def test_explicit_render_call_with_options
get :index_explicit_render_in_controller
- assert_select ':root', "david, 1"
+ assert_select ":root", "david, 1"
end
def test_caching_works_with_beginning_comment
@@ -435,11 +464,10 @@ class AutomaticCollectionCacheTest < ActionController::TestCase
assert_equal 1, @controller.partial_rendered_times
end
- private
- def assert_customer_cached(key, content)
- assert_match content,
- ActionView::PartialRenderer.collection_cache.read("views/#{key}/7c228ab609f0baf0b1f2367469210937")
- end
+ 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
@@ -458,11 +486,21 @@ class FragmentCacheKeyTest < ActionController::TestCase
@controller.cache_store = @store
end
- def test_fragment_cache_key
+ def test_combined_fragment_cache_key
@controller.account_id = "123"
- assert_equal 'views/v1/123/what a key', @controller.fragment_cache_key('what a key')
+ 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.fragment_cache_key('what a key')
+ 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 c02607b55e..636b025f2c 100644
--- a/actionpack/test/controller/content_type_test.rb
+++ b/actionpack/test/controller/content_type_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class OldContentTypeController < ActionController::Base
# :ported:
@@ -14,7 +16,7 @@ class OldContentTypeController < ActionController::Base
# :ported:
def render_content_type_from_render
- render body: "hello world!", :content_type => Mime[:rss]
+ render body: "hello world!", content_type: Mime[:rss]
end
# :ported:
@@ -37,7 +39,7 @@ class OldContentTypeController < ActionController::Base
def render_change_for_builder
response.content_type = Mime[:html]
- render :action => "render_default_for_builder"
+ render action: "render_default_for_builder"
end
def render_default_content_types_for_respond_to
@@ -131,13 +133,13 @@ class ContentTypeTest < ActionController::TestCase
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
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 12fbe0424e..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,7 +1,8 @@
-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" }
@@ -10,16 +11,15 @@ class ControllerWithBeforeActionAndDefaultUrlOptions < ActionController::Base
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 08271012e9..fcee812ee4 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,7 +39,7 @@ 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 plain: "gobble"
@@ -55,15 +57,15 @@ class FilterTest < ActionController::TestCase
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 plain: '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 plain: '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 plain: 'ok'
+ render plain: "ok"
end
def public
- render plain: 'ok'
+ render plain: "ok"
end
end
@@ -272,7 +274,7 @@ class FilterTest < ActionController::TestCase
before_action :ensure_login
def index
- render plain: '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
@@ -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 plain: 'foo'
+ render plain: "foo"
end
def bar
- render plain: 'bar'
+ render plain: "bar"
end
- protected
+ private
def first
@first = true
end
@@ -455,23 +457,25 @@ class FilterTest < ActionController::TestCase
prepend_before_action :before_all
prepend_after_action :after_all
before_action :between_before_all_and_after_all
+ after_action :between_before_all_and_after_all
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 plain: 'hello'
+ render plain: "hello"
end
end
@@ -494,50 +498,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 +588,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 +613,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 +708,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 +747,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
@@ -765,7 +767,7 @@ class FilterTest < ActionController::TestCase
def test_running_prepended_before_and_after_action
test_process(PrependingBeforeAndAfterController)
- assert_equal %w( before_all between_before_all_and_after_all after_all ), @controller.instance_variable_get(:@ran_filter)
+ assert_equal %w( before_all between_before_all_and_after_all between_before_all_and_after_all after_all ), @controller.instance_variable_get(:@ran_filter)
end
def test_skipping_and_limiting_controller
@@ -787,7 +789,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 +797,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 +821,20 @@ class FilterTest < ActionController::TestCase
response = test_process(RescuedController)
end
- assert response.successful?
+ assert_predicate response, :successful?
assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body)
end
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 +861,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
@@ -886,7 +888,7 @@ class ControllerWithSymbolAsFilter < PostsController
yield
# Do stuff...
- wtf += 1
+ wtf + 1
end
end
@@ -898,7 +900,7 @@ class ControllerWithFilterClass < PostsController
end
end
- around_action YieldingFilter, :only => :raises_after
+ around_action YieldingFilter, only: :raises_after
end
class ControllerWithFilterInstance < PostsController
@@ -909,11 +911,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 +923,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 +933,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 +960,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
@@ -1005,72 +1000,47 @@ class YieldingAroundFiltersTest < ActionController::TestCase
def test_nested_actions
controller = ControllerWithNestedFilters
assert_nothing_raised do
- begin
- test_process(controller,'raises_both')
- rescue Before, After
- end
+ test_process(controller, "raises_both")
+ rescue Before, After
end
assert_raise Before do
- begin
- test_process(controller,'raises_both')
- rescue After
- end
+ test_process(controller, "raises_both")
+ rescue After
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 eef48e8480..bf95c633e5 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,12 @@ class FlashTest < ActionController::TestCase
end
class FlashIntegrationTest < ActionDispatch::IntegrationTest
- SessionKey = '_myapp_session'
- Generator = ActiveSupport::LegacyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33')
+ SessionKey = "_myapp_session"
+ Generator = ActiveSupport::CachingKeyGenerator.new(
+ ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33", iterations: 1000)
+ )
+ Rotations = ActiveSupport::Messages::RotationConfiguration.new
+ SIGNED_COOKIE_SALT = "signed cookie"
class TestController < ActionController::Base
add_flash_types :bar
@@ -256,22 +262,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 +292,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 +301,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,6 +367,8 @@ 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
+ args[0][:env]["action_dispatch.signed_cookie_salt"] = SIGNED_COOKIE_SALT
super(path, *args)
end
@@ -324,12 +376,12 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- get ':action', :to => FlashIntegrationTest::TestController
+ 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
end
diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb
index 03a9c9ae78..7f59f6acaf 100644
--- a/actionpack/test/controller/force_ssl_test.rb
+++ b/actionpack/test/controller/force_ssl_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class ForceSSLController < ActionController::Base
def banana
@@ -11,19 +13,23 @@ class ForceSSLController < ActionController::Base
end
class ForceSSLControllerLevel < ForceSSLController
- force_ssl
+ ActiveSupport::Deprecation.silence do
+ force_ssl
+ end
end
class ForceSSLCustomOptions < ForceSSLController
- force_ssl :host => "secure.example.com", :only => :redirect_host
- force_ssl :port => 8443, :only => :redirect_port
- force_ssl :subdomain => 'secure', :only => :redirect_subdomain
- force_ssl :domain => 'secure.com', :only => :redirect_domain
- force_ssl :path => '/foo', :only => :redirect_path
- force_ssl :status => :found, :only => :redirect_status
- force_ssl :flash => { :message => 'Foo, Bar!' }, :only => :redirect_flash
- force_ssl :alert => 'Foo, Bar!', :only => :redirect_alert
- force_ssl :notice => 'Foo, Bar!', :only => :redirect_notice
+ ActiveSupport::Deprecation.silence do
+ force_ssl host: "secure.example.com", only: :redirect_host
+ force_ssl port: 8443, only: :redirect_port
+ force_ssl subdomain: "secure", only: :redirect_subdomain
+ force_ssl domain: "secure.com", only: :redirect_domain
+ force_ssl path: "/foo", only: :redirect_path
+ force_ssl status: :found, only: :redirect_status
+ force_ssl flash: { message: "Foo, Bar!" }, only: :redirect_flash
+ force_ssl alert: "Foo, Bar!", only: :redirect_alert
+ force_ssl notice: "Foo, Bar!", only: :redirect_notice
+ end
def force_ssl_action
render plain: action_name
@@ -53,42 +59,50 @@ class ForceSSLCustomOptions < ForceSSLController
end
class ForceSSLOnlyAction < ForceSSLController
- force_ssl :only => :cheeseburger
+ ActiveSupport::Deprecation.silence do
+ force_ssl only: :cheeseburger
+ end
end
class ForceSSLExceptAction < ForceSSLController
- force_ssl :except => :banana
+ ActiveSupport::Deprecation.silence do
+ force_ssl except: :banana
+ end
end
class ForceSSLIfCondition < ForceSSLController
- force_ssl :if => :use_force_ssl?
+ ActiveSupport::Deprecation.silence do
+ force_ssl if: :use_force_ssl?
+ end
def use_force_ssl?
- action_name == 'cheeseburger'
+ 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(plain: 'monkey')
+ force_ssl_redirect || render(plain: "monkey")
end
def cheeseburger
- force_ssl_redirect('secure.cheeseburger.host') || render(plain: '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,17 +328,17 @@ 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
+ 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'
+ request.env["rack.url_scheme"] = "wss"
+ request.env["Upgrade"] = "websocket"
get :cheeseburger
assert_response 200
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 feb882a2b3..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,7 +141,7 @@ 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
end
@@ -168,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
@@ -227,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
@@ -261,7 +278,7 @@ class IsolatedHelpersTest < ActionController::TestCase
def setup
super
- @request.action = 'index'
+ @request.action = "index"
end
def test_helper_in_a
@@ -269,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 adcf259317..1544a627ee 100644
--- a/actionpack/test/controller/http_basic_authentication_test.rb
+++ b/actionpack/test/controller/http_basic_authentication_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class HttpBasicAuthenticationTest < ActionController::TestCase
class DummyController < ActionController::Base
@@ -7,88 +9,88 @@ class HttpBasicAuthenticationTest < ActionController::TestCase
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 plain: "Hello Secret"
end
def display
- render plain: 'Definitely Maybe' if @logged_in
+ render plain: "Definitely Maybe" if @logged_in
end
def show
- render plain: 'Only for loooooong credentials'
+ render plain: "Only for loooooong credentials"
end
def special_creds
- render plain: 'Only for special credentials'
+ render plain: "Only for special credentials"
end
def search
- render plain: '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 auth_with_special_chars
- authenticate_or_request_with_http_basic do |username, password|
- username == 'login!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t' && password == 'pwd:!@#$%^&*()_+{}[];"\',./<>?`~ \n\r\t'
+ 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
- end
- def authenticate_long_credentials
- authenticate_or_request_with_http_basic do |username, password|
- username == '1234567890123456789012345678901234567890' && password == '1234567890123456789012345678901234567890'
+ 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
@@ -104,19 +106,19 @@ 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")}"
+ @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'
+ assert_equal "Hello Secret", @response.body, "Authentication failed when authorization scheme BASIC"
end
test "authentication request without credential" do
@@ -124,54 +126,54 @@ class HttpBasicAuthenticationTest < ActionController::TestCase
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')
+ @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
+ 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 f06912bd5a..dd4ff85d11 100644
--- a/actionpack/test/controller/http_digest_authentication_test.rb
+++ b/actionpack/test/controller/http_digest_authentication_test.rb
@@ -1,48 +1,53 @@
-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 plain: "Hello Secret"
end
def display
- render plain: '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
setup do
# Used as secret in generating nonce to prevent tampering of timestamp
@secret = "4fb45da9e4ab4ddeb7580d6a35503d99"
- @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(@secret)
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::CachingKeyGenerator.new(
+ ActiveSupport::KeyGenerator.new(@secret)
+ )
+ @request.env["action_dispatch.http_auth_salt"] = "http authentication"
end
teardown do
@@ -51,17 +56,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 +79,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 +101,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 +109,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 +117,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 +125,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 +255,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 98e3c891a7..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
@@ -11,67 +13,67 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
end
def display
- render plain: 'Definitely Maybe'
+ render plain: "Definitely Maybe"
end
def show
- render plain: '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,18 +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\""
+ @request.env["HTTP_AUTHORIZATION"] = "Token\ttoken=\"lifo\""
get :index
assert_response :success
- assert_equal 'Hello Secret', @response.body
+ assert_equal "Hello Secret", @response.body
end
test "authentication request without credential" do
@@ -107,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
@@ -127,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)
@@ -148,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)
@@ -156,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
@@ -190,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 ad7166bafa..4dddd98f9f 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!
@@ -31,95 +33,8 @@ class SessionTest < ActiveSupport::TestCase
end
end
- def test_request_via_redirect_uses_given_method
- path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue"}
- assert_called_with @session, :process, [:put, path, params: args, headers: headers] do
- @session.stub :redirect?, false do
- @session.request_via_redirect(:put, path, params: args, headers: headers)
- end
- end
- end
-
- def test_deprecated_request_via_redirect_uses_given_method
- path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" }
- assert_called_with @session, :process, [:put, path, params: args, headers: headers] do
- @session.stub :redirect?, false do
- assert_deprecated { @session.request_via_redirect(:put, path, args, headers) }
- end
- end
- end
-
- def test_request_via_redirect_follows_redirects
- path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue"}
- value_series = [true, true, false]
- assert_called @session, :follow_redirect!, times: 2 do
- @session.stub :redirect?, ->{ value_series.shift } do
- @session.request_via_redirect(:get, path, params: args, headers: headers)
- end
- end
- end
-
- def test_request_via_redirect_returns_status
- path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue"}
- @session.stub :redirect?, false do
- @session.stub :status, 200 do
- assert_equal 200, @session.request_via_redirect(:get, path, params: args, headers: headers)
- end
- end
- end
-
- def test_deprecated_get_via_redirect
- path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" }
-
- assert_called_with @session, :request_via_redirect, [:get, path, args, headers] do
- assert_deprecated do
- @session.get_via_redirect(path, args, headers)
- end
- end
- end
-
- def test_deprecated_post_via_redirect
- path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" }
-
- assert_called_with @session, :request_via_redirect, [:post, path, args, headers] do
- assert_deprecated do
- @session.post_via_redirect(path, args, headers)
- end
- end
- end
-
- def test_deprecated_patch_via_redirect
- path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" }
-
- assert_called_with @session, :request_via_redirect, [:patch, path, args, headers] do
- assert_deprecated do
- @session.patch_via_redirect(path, args, headers)
- end
- end
- end
-
- def test_deprecated_put_via_redirect
- path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" }
-
- assert_called_with @session, :request_via_redirect, [:put, path, args, headers] do
- assert_deprecated do
- @session.put_via_redirect(path, args, headers)
- end
- end
- end
-
- def test_deprecated_delete_via_redirect
- path = "/somepath"; args = { id: '1' }; headers = { "X-Test-Header" => "testvalue" }
-
- assert_called_with @session, :request_via_redirect, [:delete, path, args, headers] do
- assert_deprecated do
- @session.delete_via_redirect(path, args, headers)
- end
- end
- end
-
def test_get
- path = "/index"; params = "blah"; headers = { location: 'blah' }
+ path = "/index"; params = "blah"; headers = { location: "blah" }
assert_called_with @session, :process, [:get, path, params: params, headers: headers] do
@session.get(path, params: params, headers: headers)
@@ -127,229 +42,88 @@ class SessionTest < ActiveSupport::TestCase
end
def test_get_with_env_and_headers
- path = "/index"; params = "blah"; headers = { location: 'blah' }; env = { 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest' }
+ 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_deprecated_get
- path = "/index"; params = "blah"; headers = { location: 'blah' }
-
- assert_called_with @session, :process, [:get, path, params: params, headers: headers] do
- assert_deprecated {
- @session.get(path, params, headers)
- }
- end
- end
-
def test_post
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- assert_called_with @session, :process, [:post, path, params: params, headers: headers] do
- assert_deprecated {
- @session.post(path, params, headers)
- }
- end
- end
-
- def test_deprecated_post
- path = "/index"; params = "blah"; headers = { location: 'blah' }
+ 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' }
+ 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_deprecated_patch
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- assert_called_with @session, :process, [:patch, path, params: params, headers: headers] do
- assert_deprecated {
- @session.patch(path, params, headers)
- }
- end
- end
-
def test_put
- path = "/index"; params = "blah"; headers = { location: 'blah' }
+ 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_deprecated_put
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- assert_called_with @session, :process, [:put, path, params: params, headers: headers] do
- assert_deprecated {
- @session.put(path, params, headers)
- }
- end
- end
-
def test_delete
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- assert_called_with @session, :process, [:delete, path, params: params, headers: headers] do
- assert_deprecated {
- @session.delete(path,params,headers)
- }
- end
- end
-
- def test_deprecated_delete
- path = "/index"; params = "blah"; headers = { location: 'blah' }
+ 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' }
+ 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 deprecated_test_head
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- assert_called_with @session, :process, [:head, path, params: params, headers: headers] do
- assert_deprecated {
- @session.head(path, params, headers)
- }
- end
- end
-
def test_xml_http_request_get
- 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_deprecated_xml_http_request_get
- path = "/index"; params = "blah"; headers = { location: 'blah' }
+ 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_deprecated_args_xml_http_request_get
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- assert_called_with @session, :process, [:get, path, params: params, headers: headers, xhr: true] do
- assert_deprecated(/xml_http_request/) {
- @session.xml_http_request(:get, path, params, headers)
- }
- end
- end
-
def test_xml_http_request_post
- path = "/index"; params = "blah"; headers = { location: 'blah' }
+ 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_deprecated_xml_http_request_post
- 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_deprecated_args_xml_http_request_post
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- assert_called_with @session, :process, [:post, path, params: params, headers: headers, xhr: true] do
- assert_deprecated(/xml_http_request/) { @session.xml_http_request(:post,path,params,headers) }
- end
- end
-
def test_xml_http_request_patch
- 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_deprecated_xml_http_request_patch
- path = "/index"; params = "blah"; headers = { location: 'blah' }
+ 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_deprecated_args_xml_http_request_patch
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- assert_called_with @session, :process, [:patch, path, params: params, headers: headers, xhr: true] do
- assert_deprecated(/xml_http_request/) { @session.xml_http_request(:patch,path,params,headers) }
- end
- end
-
def test_xml_http_request_put
- 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_deprecated_xml_http_request_put
- path = "/index"; params = "blah"; headers = { location: 'blah' }
+ 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_deprecated_args_xml_http_request_put
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- assert_called_with @session, :process, [:put, path, params: params, headers: headers, xhr: true] do
- assert_deprecated(/xml_http_request/) { @session.xml_http_request(:put, path, params, headers) }
- end
- end
-
def test_xml_http_request_delete
- path = "/index"; params = "blah"; headers = { location: 'blah' }
+ 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_deprecated_xml_http_request_delete
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- assert_called_with @session, :process, [:delete, path, params: params, headers: headers, xhr: true] do
- assert_deprecated { @session.xml_http_request(:delete, path, params: params, headers: headers) }
- end
- end
-
- def test_deprecated_args_xml_http_request_delete
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- assert_called_with @session, :process, [:delete, path, params: params, headers: headers, xhr: true] do
- assert_deprecated(/xml_http_request/) { @session.xml_http_request(:delete, path, params, headers) }
- end
- end
-
def test_xml_http_request_head
- path = "/index"; params = "blah"; headers = { location: 'blah' }
+ 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
-
- def test_deprecated_xml_http_request_head
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- assert_called_with @session, :process, [:head, path, params: params, headers: headers, xhr: true] do
- assert_deprecated(/xml_http_request/) { @session.xml_http_request(:head, path, params: params, headers: headers) }
- end
- end
-
- def test_deprecated_args_xml_http_request_head
- path = "/index"; params = "blah"; headers = { location: 'blah' }
- assert_called_with @session, :process, [:head, path, params: params, headers: headers, xhr: true] do
- assert_deprecated { @session.xml_http_request(:head, path, params, headers) }
- end
- end
end
class IntegrationTestTest < ActiveSupport::TestCase
@@ -361,7 +135,7 @@ class IntegrationTestTest < ActiveSupport::TestCase
session1 = @test.open_session { |sess| }
session2 = @test.open_session # implicit session
- assert !session1.equal?(session2)
+ assert_not session1.equal?(session2)
end
# RSpec mixes Matchers (which has a #method_missing) into
@@ -370,15 +144,15 @@ 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)
+ mixin.remove_method :method_missing
end
end
end
@@ -390,7 +164,7 @@ class IntegrationTestUsesCorrectClass < ActionDispatch::IntegrationTest
reset!
%w( get post head patch put delete ).each do |verb|
- assert_nothing_raised { __send__(verb, '/') }
+ assert_nothing_raised { __send__(verb, "/") }
end
end
end
@@ -401,9 +175,10 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
respond_to do |format|
format.html { render plain: "OK", status: 200 }
format.js { render plain: "JS 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 }
+ 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
@@ -426,7 +201,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
def set_cookie
- cookies["foo"] = 'bar'
+ cookies["foo"] = "bar"
head :ok
end
@@ -435,18 +210,18 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
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
@@ -463,7 +238,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
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}
+ get "/get", headers: { "HTTP_ACCEPT" => mime_string }
assert_equal 200, status
assert_equal "OK", status_message
assert_response 200
@@ -480,7 +255,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
def test_post
with_test_route_set do
- post '/post'
+ post "/post"
assert_equal 201, status
assert_equal "Created", status_message
assert_response 201
@@ -494,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
@@ -556,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
@@ -588,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
@@ -611,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
@@ -689,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
@@ -701,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
@@ -728,15 +547,15 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
set.draw do
- get 'moved' => redirect('/method')
+ get "moved" => redirect("/method")
ActiveSupport::Deprecation.silence do
- match ':action', :to => controller, :via => [:get, :post], :as => :action
- get 'get/:action', :to => controller, :as => :get_action
+ 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
@@ -749,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
@@ -772,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"]
@@ -819,7 +638,6 @@ class MetalIntegrationTest < ActionDispatch::IntegrationTest
get "https://test.com:80"
assert_equal "test.com:80", @request.env["HTTP_HOST"]
end
-
end
class ApplicationIntegrationTest < ActionDispatch::IntegrationTest
@@ -843,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(*)
@@ -851,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
@@ -866,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
@@ -897,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
@@ -926,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
@@ -934,11 +752,11 @@ 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
@@ -959,7 +777,7 @@ class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest
class BarController < ActionController::Base
def default_url_options
- { :host => "bar.com" }
+ { host: "bar.com" }
end
def index
@@ -980,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
@@ -990,17 +808,17 @@ class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest
end
end
- test "session uses default url options from routes" do
+ test "session uses default URL options from routes" do
assert_equal "http://foo.com/foo", foos_url
end
- test "current host overrides default url options from routes" do
+ test "current host overrides default URL options from routes" do
get "/foo"
assert_response :success
assert_equal "http://www.example.com/foo", foos_url
end
- test "controller can override default url options from request" do
+ test "controller can override default URL options from request" do
get "/bar"
assert_response :success
assert_equal "http://bar.com/foo", foos_url
@@ -1022,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
@@ -1046,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
@@ -1060,7 +878,7 @@ end
class IntegrationWithRoutingTest < ActionDispatch::IntegrationTest
class FooController < ActionController::Base
def index
- render plain: 'ok'
+ render plain: "ok"
end
end
@@ -1070,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
@@ -1100,8 +918,8 @@ class IntegrationRequestsWithoutSetup < ActionDispatch::IntegrationTest
class FooController < ActionController::Base
def ok
- cookies[:key] = 'ok'
- render plain: 'ok'
+ cookies[:key] = "ok"
+ render plain: "ok"
end
end
@@ -1109,15 +927,15 @@ class IntegrationRequestsWithoutSetup < ActionDispatch::IntegrationTest
with_routing do |routes|
routes.draw do
ActiveSupport::Deprecation.silence do
- get ':action' => FooController
+ get ":action" => FooController
end
end
- get '/ok'
+ 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
@@ -1125,33 +943,70 @@ 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'
+ 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_match 'foos_json.json', request.path
- assert_equal 'application/json', request.content_type
- assert_equal({ 'foo' => 'fighters' }, request.request_parameters)
- assert_equal({ 'foo' => 'fighters' }, response.parsed_body)
+ 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
@@ -1162,17 +1017,19 @@ class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest
end
def test_registering_custom_encoder
- Mime::Type.register 'text/wibble', :wibble
+ 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_match 'foos_wibble.wibble', request.path
- assert_equal 'text/wibble', request.content_type
+ 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
+ assert_equal "ok", response.parsed_body
end
ensure
Mime::Type.unregister :wibble
@@ -1182,13 +1039,57 @@ class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest
with_routing do |routes|
routes.draw do
ActiveSupport::Deprecation.silence do
- get ':action' => FooController
+ 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.json', params: { foo: 'heyo' }
+ get "/foos_json", as: :json
- assert_equal({ 'foo' => 'heyo' }, response.parsed_body)
+ assert_equal "http://www.example.com/foos_json", request.url
end
end
@@ -1197,13 +1098,49 @@ class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest
with_routing do |routes|
routes.draw do
ActiveSupport::Deprecation.silence do
- post ':action' => FooController
+ post ":action" => FooController
end
end
- post "/foos_#{as}", params: { foo: 'fighters' }, as: as
+ 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 0c3884cd38..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 'concurrent/atomic/count_down_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
@@ -115,7 +118,7 @@ module ActionController
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 plain: '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,7 +145,7 @@ 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.wait
@@ -150,10 +153,26 @@ module ActionController
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_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
@@ -161,20 +180,20 @@ module ActionController
end
def with_stale
- render plain: '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)
@@ -182,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
@@ -194,50 +213,50 @@ 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'
+ 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.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'
+ logger.info "Work complete"
latch.count_down
end
end
@@ -245,8 +264,9 @@ module ActionController
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
@@ -270,21 +290,29 @@ module ActionController
def test_set_cookie
get :set_cookie
- assert_equal({'hello' => 'world'}, @response.cookies)
+ assert_equal({ "hello" => "world" }, @response.cookies)
assert_equal "hello world", @response.body
end
def test_write_to_stream
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 = Concurrent::CountDownLatch.new
- parts = ['hello', 'world']
+ parts = ["hello", "world"]
get :blocking_stream
@@ -298,7 +326,7 @@ module ActionController
end
}
- 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
@@ -306,7 +334,7 @@ module ActionController
@controller.error_latch = Concurrent::CountDownLatch.new
capture_log_output do |output|
- get :overfill_buffer_and_die, :format => 'plain'
+ get :overfill_buffer_and_die, format: "plain"
t = Thread.new(response) { |resp|
resp.await_commit
@@ -320,7 +348,7 @@ module ActionController
t.join
@controller.error_latch.wait
- assert_match 'Error while streaming', output.rewind && output.read
+ assert_match "Error while streaming", output.rewind && output.read
end
end
@@ -343,26 +371,26 @@ module ActionController
Timeout.timeout(3) do
@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
get :default_header
- assert response.headers['Content-Type']
+ 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
@@ -374,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
@@ -388,8 +416,8 @@ 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
@@ -398,34 +426,34 @@ module ActionController
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
@@ -436,7 +464,7 @@ module ActionController
end
def test_stale_with_etag
- @request.if_none_match = %(W/"#{Digest::MD5.hexdigest('123')}")
+ @request.if_none_match = %(W/"#{ActiveSupport::Digest.hexdigest('123')}")
get :with_stale
assert_equal 304, response.status.to_i
end
@@ -455,10 +483,10 @@ class LiveStreamRouterTest < ActionDispatch::IntegrationTest
include ActionController::Live
def index
- 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
@@ -473,7 +501,7 @@ class LiveStreamRouterTest < ActionDispatch::IntegrationTest
end
routes.draw do
- get '/test' => 'live_stream_router_test/test#index'
+ get "/test" => "live_stream_router_test/test#index"
end
def app
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 57cf2dafdf..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
@@ -186,14 +188,14 @@ class ACLogSubscriberTest < ActionController::TestCase
def test_process_action_headers
get :show
wait
- assert_equal "Rails Testing", @controller.last_payload[:headers]['User-Agent']
+ 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
@@ -212,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
@@ -221,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
@@ -258,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
index 007866a559..5f0d125128 100644
--- a/actionpack/test/controller/metal/renderers_test.rb
+++ b/actionpack/test/controller/metal/renderers_test.rb
@@ -1,14 +1,22 @@
-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 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)
+ { a: "b" }.to_json(options)
end
def to_xml(options = {})
- { a: 'b' }.to_xml(options)
+ { a: "b" }.to_xml(options)
end
end
@@ -29,14 +37,14 @@ class RenderersMetalTest < ActionController::TestCase
def test_render_json
get :one
assert_response :success
- assert_equal({ a: 'b' }.to_json, @response.body)
- assert_equal 'application/json', @response.content_type
+ 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
+ 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 993f4001de..2f8f191828 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
@@ -45,13 +47,12 @@ class RespondToController < ActionController::Base
def json_xml_or_html
respond_to do |type|
- type.json { render body: 'JSON' }
- type.xml { render :xml => 'XML' }
- type.html { render body: 'HTML' }
+ type.json { render body: "JSON" }
+ type.xml { render xml: "XML" }
+ type.html { render body: "HTML" }
end
end
-
def forced_xml
request.format = :xml
@@ -101,15 +102,34 @@ class RespondToController < ActionController::Base
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 body: "HTML" }
- type.custom("application/crazy-xml") { render body: "Crazy XML" }
+ type.custom("application/fancy-xml") { render body: "Fancy XML" }
type.all { render body: "Nothing" }
end
end
-
def custom_constant_handling
respond_to do |type|
type.html { render body: "HTML" }
@@ -133,8 +153,14 @@ class RespondToController < ActionController::Base
def handle_any_any
respond_to do |type|
- type.html { render body: 'HTML' }
- type.any { render body: '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
+
+ def handle_any_with_template
+ respond_to do |type|
+ type.any { render "test/hello_world" }
end
end
@@ -146,7 +172,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
@@ -163,8 +189,8 @@ 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
@@ -223,7 +249,7 @@ class RespondToController < ActionController::Base
def variant_any
respond_to do |format|
format.html do |variant|
- variant.any(:tablet, :phablet){ render body: "any" }
+ variant.any(:tablet, :phablet) { render body: "any" }
variant.phone { render body: "phone" }
end
end
@@ -240,7 +266,7 @@ class RespondToController < ActionController::Base
def variant_inline_any
respond_to do |format|
- format.html.any(:tablet, :phablet){ render body: "any" }
+ format.html.any(:tablet, :phablet) { render body: "any" }
format.html.phone { render body: "phone" }
end
end
@@ -261,7 +287,7 @@ class RespondToController < ActionController::Base
def variant_any_with_none
respond_to do |format|
- format.html.any(:none, :phone){ render body: "none or phone" }
+ format.html.any(:none, :phone) { render body: "none or phone" }
end
end
@@ -269,19 +295,19 @@ class RespondToController < ActionController::Base
respond_to do |format|
format.html { render body: "HTML" }
format.any(:js, :xml) do |variant|
- variant.phone{ render body: "phone" }
- variant.any(:tablet, :phablet){ render body: "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
@@ -294,21 +320,23 @@ class RespondToControllerTest < ActionController::TestCase
@request.host = "www.example.com"
Mime::Type.register_alias("text/html", :iphone)
Mime::Type.register("text/x-mobile", :mobile)
+ Mime::Type.register("application/fancy-xml", :fancy_xml)
end
def teardown
super
Mime::Type.unregister(:iphone)
Mime::Type.unregister(:mobile)
+ Mime::Type.unregister(:fancy_xml)
end
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
@@ -318,29 +346,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"
@@ -352,25 +380,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
@@ -383,20 +411,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
@@ -422,7 +450,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
@@ -430,6 +458,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"
@@ -447,7 +489,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
@@ -455,85 +497,92 @@ class RespondToControllerTest < ActionController::TestCase
end
def test_custom_types
- @request.accept = "application/crazy-xml"
+ @request.accept = "application/fancy-xml"
get :custom_type_handling
- assert_equal "application/crazy-xml", @response.content_type
- assert_equal 'Crazy XML', @response.body
+ assert_equal "application/fancy-xml", @response.content_type
+ assert_equal "Fancy 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_handle_any_with_template
+ @request.accept = "*/*"
+
+ get :handle_any_with_template
+ assert_equal "Hello world!", @response.body
end
def test_html_type_with_layout
@@ -543,15 +592,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
@@ -658,13 +707,13 @@ class RespondToControllerTest < ActionController::TestCase
end
def test_variant_without_implicit_rendering_from_browser
- assert_raises(ActionController::UnknownFormat) do
+ assert_raises(ActionController::MissingExactTemplate) do
get :variant_without_implicit_template_rendering, params: { v: :does_not_matter }
end
end
def test_variant_variant_not_set_and_without_implicit_rendering_from_browser
- assert_raises(ActionController::UnknownFormat) do
+ assert_raises(ActionController::MissingExactTemplate) do
get :variant_without_implicit_template_rendering
end
end
@@ -676,7 +725,7 @@ class RespondToControllerTest < ActionController::TestCase
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_CONTENT_WARNING }.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
@@ -685,10 +734,10 @@ class RespondToControllerTest < ActionController::TestCase
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 }
+ 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"
+ 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
diff --git a/actionpack/test/controller/new_base/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb
index ee3c498b1c..7572d514fb 100644
--- a/actionpack/test/controller/new_base/bare_metal_test.rb
+++ b/actionpack/test/controller/new_base/bare_metal_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "abstract_unit"
module BareMetalTest
@@ -11,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"
@@ -52,7 +54,7 @@ module BareMetalTest
controller.set_request!(ActionDispatch::Request.empty)
controller.set_response!(BareController.make_response!(controller.request))
controller.index
- assert_equal nil, controller.response_body
+ assert_nil controller.response_body
end
end
@@ -102,38 +104,38 @@ 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
diff --git a/actionpack/test/controller/new_base/base_test.rb b/actionpack/test/controller/new_base/base_test.rb
index 0755dafe93..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
@@ -25,7 +27,7 @@ module Dispatching
render body: "actions: #{action_methods.to_a.sort.join(', ')}"
end
- protected
+ private
def authenticate
end
end
@@ -45,7 +47,6 @@ module Dispatching
end
class BaseTest < Rack::TestCase
- # :api: plugin
test "simple dispatching" do
get "/dispatching/simple/index"
@@ -54,14 +55,12 @@ module Dispatching
assert_content_type "text/plain; charset=utf-8"
end
- # :api: plugin
test "directly modifying response body" do
get "/dispatching/simple/modify_response_body"
assert_body "success"
end
- # :api: plugin
test "directly modifying response body twice" do
get "/dispatching/simple/modify_response_body_twice"
@@ -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 c0e92b3b05..548fa4300d 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 plain: self.formats.inspect
+ render plain: formats.inspect
end
end
@@ -19,9 +20,19 @@ module ContentNegotiation
assert_body "Hello world */*!"
end
- test "Not all mimes are converted to symbol" do
+ test "A js or */* Accept header will return HTML" do
+ get "/content_negotiation/basic/hello", headers: { "HTTP_ACCEPT" => "text/javascript, */*" }
+ assert_body "Hello world text/html!"
+ end
+
+ test "A js or */* Accept header on xhr will return JavaScript" do
+ get "/content_negotiation/basic/hello", headers: { "HTTP_ACCEPT" => "text/javascript, */*" }, xhr: true
+ assert_body "Hello world text/javascript!"
+ end
+
+ test "Unregistered mimes are ignored" do
get "/content_negotiation/basic/all", headers: { "HTTP_ACCEPT" => "text/plain, mime/another" }
- assert_body '[:text, "mime/another"]'
+ assert_body "[:text]"
end
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 0b3a26807d..d3ee4a8a6f 100644
--- a/actionpack/test/controller/new_base/content_type_test.rb
+++ b/actionpack/test/controller/new_base/content_type_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ContentType
class BaseController < ActionController::Base
@@ -44,7 +46,7 @@ module ContentType
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- get ':controller', :action => 'index'
+ get ":controller", action: "index"
end
end
diff --git a/actionpack/test/controller/new_base/middleware_test.rb b/actionpack/test/controller/new_base/middleware_test.rb
index 85a1f351f0..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"
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 c65c245773..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 { ActiveSupport::Deprecation.silence { 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 { ActiveSupport::Deprecation.silence { 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
deleted file mode 100644
index 177a1c088d..0000000000
--- a/actionpack/test/controller/new_base/render_context_test.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-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
-# similar to the way sinatra renders templates.
-module RenderContext
- class BasicController < ActionController::Base
- self.view_paths = [ActionView::FixtureResolver.new(
- "render_context/basic/hello_world.html.erb" => "<%= @value %> from <%= self.__controller_method__ %>",
- "layouts/basic.html.erb" => "?<%= yield %>?"
- )]
-
- # 1) Include ActionView::Context to bring the required dependencies
- include ActionView::Context
-
- # 2) Call _prepare_context that will do the required initialization
- before_action :_prepare_context
-
- def hello_world
- @value = "Hello"
- render :action => "hello_world", :layout => false
- end
-
- def with_layout
- @value = "Hello"
- render :action => "hello_world", :layout => "basic"
- end
-
- protected
-
- # 3) Set view_context to self
- def view_context
- self
- end
-
- def __controller_method__
- "controller context!"
- end
- end
-
- class RenderContextTest < Rack::TestCase
- test "rendering using the controller as context" do
- get "/render_context/basic/hello_world"
- assert_body "Hello from controller context!"
- assert_status 200
- end
-
- test "rendering using the controller as context with layout" do
- get "/render_context/basic/with_layout"
- assert_body "?Hello from controller context!?"
- assert_status 200
- end
- end
-end
diff --git a/actionpack/test/controller/new_base/render_file_test.rb b/actionpack/test/controller/new_base/render_file_test.rb
index 0c21bb0719..82325c5bb2 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
@@ -38,32 +40,44 @@ module RenderFile
testing RenderFile::BasicController
test "rendering simple template" do
- get :index
+ assert_deprecated do
+ get :index
+ end
assert_response "Hello world!"
end
test "rendering template with ivar" do
- get :with_instance_variables
+ assert_deprecated do
+ get :with_instance_variables
+ end
assert_response "The secret is in the sauce\n"
end
test "rendering a relative path" do
- get :relative_path
+ assert_deprecated do
+ get :relative_path
+ end
assert_response "The secret is in the sauce\n"
end
test "rendering a relative path with dot" do
- get :relative_path_with_dot
+ assert_deprecated do
+ get :relative_path_with_dot
+ end
assert_response "The secret is in the sauce\n"
end
test "rendering a Pathname" do
- get :pathname
+ assert_deprecated do
+ get :pathname
+ end
assert_response "The secret is in the sauce\n"
end
test "rendering file with locals" do
- get :with_locals
+ assert_deprecated do
+ get :with_locals
+ end
assert_response "The secret is in the sauce\n"
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 bfed136496..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 { ActiveSupport::Deprecation.silence { 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 { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } }
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
get "/render_html/with_layout"
diff --git a/actionpack/test/controller/new_base/render_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 94afe7bcfe..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 { ActiveSupport::Deprecation.silence { 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 { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } }
+ set.draw { ActiveSupport::Deprecation.silence { get ":controller", action: "index" } }
get "/render_plain/with_layout"
diff --git a/actionpack/test/controller/new_base/render_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 0d4c7cdb0a..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 { ActiveSupport::Deprecation.silence { 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 1fb852a2c4..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,9 +38,9 @@ module Render
private
- def secretz
- render plain: "FAIL WHALE!"
- end
+ def secretz
+ render plain: "FAIL WHALE!"
+ end
end
class DoubleRenderController < ActionController::Base
@@ -58,7 +60,7 @@ module Render
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- get ":controller", :action => 'index'
+ get ":controller", action: "index"
end
end
@@ -73,7 +75,7 @@ module Render
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- get ":controller", :action => 'index'
+ get ":controller", action: "index"
end
end
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 d4111d432c..0000000000
--- a/actionpack/test/controller/new_base/render_text_test.rb
+++ /dev/null
@@ -1,188 +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
- ActiveSupport::Deprecation.silence do
- get "/render_text/minimal/index"
- end
-
- 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 { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } }
-
- ActiveSupport::Deprecation.silence do
- get "/render_text/simple"
- end
-
- 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 { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } }
-
- ActiveSupport::Deprecation.silence do
- get "/render_text/with_layout"
- end
-
- assert_body "hello david"
- assert_status 200
- end
- end
-
- test "rendering text, while also providing a custom status code" do
- ActiveSupport::Deprecation.silence do
- get "/render_text/with_layout/custom_code"
- end
-
- assert_body "hello world"
- assert_status 404
- end
-
- test "rendering text with nil returns an empty body" do
- ActiveSupport::Deprecation.silence do
- get "/render_text/with_layout/with_nil"
- end
-
- assert_body ""
- assert_status 200
- end
-
- test "Rendering text with nil and custom status code returns an empty body and the status" do
- ActiveSupport::Deprecation.silence do
- get "/render_text/with_layout/with_nil_and_status"
- end
-
- assert_body ""
- assert_status 403
- end
-
- test "rendering text with false returns the string 'false'" do
- ActiveSupport::Deprecation.silence do
- get "/render_text/with_layout/with_false"
- end
-
- assert_body "false"
- assert_status 200
- end
-
- test "rendering text with layout: true" do
- ActiveSupport::Deprecation.silence do
- get "/render_text/with_layout/with_layout_true"
- end
-
- assert_body "hello world, I'm here!"
- assert_status 200
- end
-
- test "rendering text with layout: 'greetings'" do
- ActiveSupport::Deprecation.silence do
- get "/render_text/with_layout/with_custom_layout"
- end
-
- assert_body "hello world, I wish thee well."
- assert_status 200
- end
-
- test "rendering text with layout: false" do
- ActiveSupport::Deprecation.silence do
- get "/render_text/with_layout/with_layout_false"
- end
-
- assert_body "hello world"
- assert_status 200
- end
-
- test "rendering text with layout: nil" do
- ActiveSupport::Deprecation.silence do
- get "/render_text/with_layout/with_layout_nil"
- end
-
- assert_body "hello world"
- assert_status 200
- end
-
- test "rendering text displays deprecation warning" do
- assert_deprecated do
- get "/render_text/with_layout/with_layout_nil"
- end
- 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 17c62dc3fe..7789e654d5 100644
--- a/actionpack/test/controller/parameters/accessors_test.rb
+++ b/actionpack/test/controller/parameters/accessors_test.rb
@@ -1,6 +1,7 @@
-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
@@ -8,25 +9,25 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
@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
@@ -35,6 +36,11 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
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
@params.permit!
@params.each { |key, value| assert(value.permitted?) if key == "person" }
@@ -44,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" }
@@ -53,92 +67,198 @@ 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 "values_at retains permitted status" do
- @params.permit!
- assert @params.values_at(:person).first.permitted?
- assert @params[:person].values_at(:name).first.permitted?
+ test "transform_values converts hashes to parameters" do
+ @params.transform_values do |value|
+ assert_kind_of ActionController::Parameters, value
+ value
+ end
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?
+ test "transform_values without block yieds an enumerator" do
+ assert_kind_of Enumerator, @params.transform_values
end
- test "equality with a hash is deprecated" do
- hash1 = { foo: :bar }
- params1 = ActionController::Parameters.new(hash1)
- assert_deprecated("will be removed in Rails 5.1") do
- assert(params1 == hash1)
+ 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_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_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)
@@ -158,7 +278,7 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
assert(params2 == params1)
end
- test 'is not equal to an unpermitted Parameters instance with same params' do
+ 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)
@@ -173,7 +293,7 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
end
test "equality with simple types works" do
- assert(@params != 'Hello')
+ assert(@params != "Hello")
assert(@params != 42)
assert(@params != false)
end
@@ -195,23 +315,24 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
assert_match(/permitted: true/, @params.inspect)
end
- if Hash.method_defined?(:dig)
- test "#dig delegates the dig method to its values" do
- assert_equal "David", @params.dig(:person, :name, :first)
- assert_equal "Chicago", @params.dig(:person, :addresses, 0, :city)
- end
+ test "#dig delegates the dig method to its values" do
+ assert_equal "David", @params.dig(:person, :name, :first)
+ assert_equal "Chicago", @params.dig(:person, :addresses, 0, :city)
+ end
- test "#dig converts hashes to parameters" do
- assert_kind_of ActionController::Parameters, @params.dig(:person)
- assert_kind_of ActionController::Parameters, @params.dig(:person, :addresses, 0)
- assert @params.dig(:person, :addresses).all? do |value|
- value.is_a?(ActionController::Parameters)
- end
- end
- else
- test "ActionController::Parameters does not respond to #dig on Ruby 2.2" do
- assert_not ActionController::Parameters.method_defined?(:dig)
- assert_not @params.respond_to?(:dig)
+ test "#dig converts hashes to parameters" do
+ assert_kind_of ActionController::Parameters, @params.dig(:person)
+ assert_kind_of ActionController::Parameters, @params.dig(:person, :addresses, 0)
+ assert @params.dig(:person, :addresses).all? do |value|
+ value.is_a?(ActionController::Parameters)
end
end
+
+ test "mutating #dig return value mutates underlying parameters" do
+ @params.dig(:person, :name)[:first] = "Bill"
+ assert_equal "Bill", @params.dig(:person, :name, :first)
+
+ @params.dig(:person, :addresses)[0] = { city: "Boston", state: "Massachusetts" }
+ assert_equal "Boston", @params.dig(:person, :addresses, 0, :city)
+ end
end
diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
index c5bfb10b53..974612fb7b 100644
--- a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
+++ b/actionpack/test/controller/parameters/always_permitted_parameters_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 AlwaysPermittedParametersTest < ActiveSupport::TestCase
def setup
@@ -18,12 +20,11 @@ class AlwaysPermittedParametersTest < ActiveSupport::TestCase
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 7151a8567c..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 96048e2868..fbfe24059b 100644
--- a/actionpack/test/controller/parameters/parameters_permit_test.rb
+++ b/actionpack/test/controller/parameters/parameters_permit_test.rb
@@ -1,34 +1,36 @@
-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
- def walk_permitted params
- params.each do |k,v|
+ def walk_permitted(params)
+ params.each do |k, v|
case v
when ActionController::Parameters
walk_permitted v
@@ -38,26 +40,26 @@ class ParametersPermitTest < ActiveSupport::TestCase
end
end
- test 'iteration should not impact permit' do
- hash = {"foo"=>{"bar"=>{"0"=>{"baz"=>"hello", "zot"=>"1"}}}}
+ 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)
+ 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')
+ 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__),
@@ -66,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
@@ -97,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
@@ -111,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
@@ -123,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
@@ -178,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
@@ -202,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
@@ -216,11 +259,11 @@ class ParametersPermitTest < ActiveSupport::TestCase
end
test "fetch doesnt raise ParameterMissing exception if there is a default that is nil" do
- assert_equal nil, @params.fetch(:foo, nil)
- assert_equal nil, @params.fetch(:foo) { nil }
+ assert_nil @params.fetch(:foo, nil)
+ assert_nil @params.fetch(:foo) { nil }
end
- test 'KeyError in fetch block should not be covered up' do
+ 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) }
@@ -229,12 +272,77 @@ class ParametersPermitTest < ActiveSupport::TestCase
end
test "not permitted is sticky beyond merges" do
- assert !@params.merge(a: "b").permitted?
+ assert_not_predicate @params.merge(a: "b"), :permitted?
end
test "permitted is sticky beyond merges" do
@params.permit!
- assert @params.merge(a: "b").permitted?
+ assert_predicate @params.merge(a: "b"), :permitted?
+ end
+
+ test "merge with parameters" do
+ 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
@@ -245,88 +353,112 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert_equal "Jonas", @params[:person][:family][:brother]
end
- test "permit state is kept on a dup" do
+ test "permit! is recursive" do
+ @params[:nested_array] = [[{ x: 2, y: 3 }, { x: 21, y: 42 }]]
@params.permit!
- assert_equal @params.permitted?, @params.dup.permitted?
- end
-
- test "permit is recursive" do
- @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: {
- age: "32", name: { first: "David", last: "Heinemeier Hansson" }
- }})
-
- assert params.slice(:person).permitted?
- assert params[:person][:name].permitted?
- ensure
- ActionController::Parameters.permit_all_parameters = false
- end
+ ActionController::Parameters.permit_all_parameters = true
+ params = ActionController::Parameters.new(person: {
+ age: "32", name: { first: "David", last: "Heinemeier Hansson" }
+ })
+
+ assert_predicate params.slice(:person), :permitted?
+ assert_predicate params[:person][:name], :permitted?
+ ensure
+ ActionController::Parameters.permit_all_parameters = false
end
test "permitting parameters as an array" do
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? ActiveSupport::HashWithIndifferentAccess
- 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? ActiveSupport::HashWithIndifferentAccess
- 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
end
test "to_h 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 params.to_h.is_a? ActiveSupport::HashWithIndifferentAccess
- assert_not @params.to_h.is_a? ActionController::Parameters
- assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_h)
- ensure
- ActionController::Parameters.permit_all_parameters = false
+ ActionController::Parameters.permit_all_parameters = true
+ params = ActionController::Parameters.new(crab: "Senjougahara Hitagi")
+
+ 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
+
+ test "to_hash raises UnfilteredParameters on unfiltered params" do
+ assert_raises(ActionController::UnfilteredParameters) do
+ @params.to_hash
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 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({ "controller" => "users", "action" => "create" }, params.to_h)
+ assert_equal({ a: 1 }, { a: 1 }.merge!(params))
+ end
+
+ test "to_hash returns converted hash when .permit_all_parameters is set" do
+ 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
test "to_unsafe_h returns unfiltered params" do
- assert @params.to_unsafe_h.is_a? ActiveSupport::HashWithIndifferentAccess
- assert_not @params.to_unsafe_h.is_a? ActionController::Parameters
+ 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"]}}
+ params = ActionController::Parameters.new("f" => { "language_facet" => ["Tibetan"] })
+ expected = { "f" => { "language_facet" => ["Tibetan"] } }
- assert params['f'].is_a? ActionController::Parameters
+ 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
@@ -334,10 +466,10 @@ class ParametersPermitTest < ActiveSupport::TestCase
end.new
params = ActionController::Parameters.new(prem: { likes: %i( dancing ) })
- assert_equal({ 'prem' => { 'likes' => %i( dancing ) } }, params.permit!.to_h)
+ 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_equal({ "companies" => [ company, :acme ] }, params.permit!.to_h)
assert_not company.dupped
end
@@ -348,16 +480,31 @@ class ParametersPermitTest < ActiveSupport::TestCase
end.new
params = ActionController::Parameters.new(prem: { likes: %i( dancing ) })
- assert_equal({ 'prem' => { 'likes' => %i( dancing ) } }, params.to_unsafe_h)
+ 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_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 @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 7226beed26..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
@@ -32,6 +34,10 @@ class ParamsWrapperTest < ActionController::TestCase
def self.attribute_names
[]
end
+
+ def self.stored_attributes
+ { settings: [:color, :size] }
+ end
end
class Person
@@ -48,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
@@ -66,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
@@ -76,99 +93,107 @@ 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
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' }})
+ @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
@@ -178,40 +203,72 @@ class ParamsWrapperTest < ActionController::TestCase
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
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
@@ -219,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
@@ -252,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
@@ -262,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
@@ -275,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
@@ -302,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
@@ -323,7 +379,7 @@ class IrregularInflectionParamsWrapperTest < ActionController::TestCase
class ParamswrappernewsItem
def self.attribute_names
- ['test_attr']
+ ["test_attr"]
end
end
@@ -343,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 7c753a45a5..caac88ffb2 100644
--- a/actionpack/test/controller/permitted_params_test.rb
+++ b/actionpack/test/controller/permitted_params_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class PeopleController < ActionController::Base
def create
diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb
index e10d4449f3..7f1c41787a 100644
--- a/actionpack/test/controller/redirect_test.rb
+++ b/actionpack/test/controller/redirect_test.rb
@@ -1,57 +1,91 @@
-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 plain: 'called status'; end
- def location; render plain: 'called location'; end
+ def status; raise "Should not be called!"; end
+ def location; raise "Should not be called!"; end
def simple_redirect
- redirect_to :action => "hello_world"
+ 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})
- end
-
- def redirect_to_back_with_status
- redirect_to :back, :status => 307
+ redirect_to("/things/stuff", status: 301)
end
def redirect_back_with_status
redirect_back(fallback_location: "/things/stuff", status: 307)
end
+ def redirect_back_with_status_and_fallback_location_to_another_host
+ redirect_back(fallback_location: "http://www.rubyonrails.org/", status: 307)
+ end
+
+ def safe_redirect_back_with_status
+ redirect_back(fallback_location: "/things/stuff", status: 307, allow_other_host: false)
+ end
+
+ def safe_redirect_back_with_status_and_fallback_location_to_another_host
+ redirect_back(fallback_location: "http://www.rubyonrails.org/", status: 307, allow_other_host: false)
+ end
+
def host_redirect
- redirect_to :action => "other_host", :only_path => false, :host => 'other.test.host'
+ 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
@@ -70,10 +104,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
@@ -87,7 +117,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
@@ -100,7 +130,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
@@ -113,9 +147,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
@@ -176,7 +210,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
@@ -189,21 +222,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"
-
- assert_deprecated do
- get :redirect_to_back_with_status
- end
-
- 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
@@ -215,7 +244,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
@@ -242,53 +271,68 @@ 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"
+ def test_redirect_back
+ referer = "http://www.example.com/coming/from"
+ @request.env["HTTP_REFERER"] = referer
- assert_deprecated do
- get :redirect_to_back
- end
+ get :redirect_back_with_status
- assert_response :redirect
- assert_equal "http://www.example.com/coming/from", redirect_to_url
+ 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
+ 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
- assert_deprecated do
- get :redirect_to_back
- end
+ def test_redirect_back_with_no_referer_redirects_to_another_host
+ get :redirect_back_with_status_and_fallback_location_to_another_host
- get :redirect_to_back
- }
+ assert_response 307
+ assert_equal "http://www.rubyonrails.org/", redirect_to_url
end
- def test_redirect_back
- referer = "http://www.example.com/coming/from"
- @request.env["HTTP_REFERER"] = referer
+ def test_safe_redirect_back_from_other_host
+ @request.env["HTTP_REFERER"] = "http://another.host/coming/from"
+ get :safe_redirect_back_with_status
- 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_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_back_with_no_referer
- get :redirect_back_with_status
+ def test_safe_redirect_back_with_no_referer
+ get :safe_redirect_back_with_status
assert_response 307
assert_equal "http://test.host/things/stuff", redirect_to_url
end
+ def test_safe_redirect_back_with_no_referer_redirects_to_another_host
+ get :safe_redirect_back_with_status_and_fallback_location_to_another_host
+
+ assert_response 307
+ assert_equal "http://www.rubyonrails.org/", redirect_to_url
+ end
+
def test_redirect_to_record
with_routing do |set|
set.draw do
resources :workshops
ActiveSupport::Deprecation.silence do
- get ':controller/:action'
+ get ":controller/:action"
end
end
@@ -310,10 +354,10 @@ class RedirectTest < ActionController::TestCase
end
def test_redirect_to_params
- error = assert_raise(ArgumentError) do
+ error = assert_raise(ActionController::UnfilteredParameters) do
get :redirect_to_params
end
- assert_equal "Generating a URL from non sanitized request parameters is insecure!", error.message
+ assert_equal "unable to convert unpermitted parameters to hash", error.message
end
def test_redirect_to_with_block
@@ -328,11 +372,17 @@ 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
ActiveSupport::Deprecation.silence do
- get ':controller/:action'
+ get ":controller/:action"
end
end
@@ -347,7 +397,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
@@ -363,7 +413,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
@@ -375,7 +425,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 3773900cc4..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 plain: 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_test.rb b/actionpack/test/controller/render_test.rb
index f42efd35af..6d198ca42f 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -1,15 +1,20 @@
-require 'abstract_unit'
-require 'controller/fake_models'
+# 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 plain: "stale" if stale?(etag: '123', template: false)
+ render plain: "stale" if stale?(etag: "123", template: false)
end
def array
@@ -17,14 +22,18 @@ class TestControllerWithExtraEtags < ActionController::Base
end
def strong
- render plain: "stale" if stale?(strong_etag: 'strong')
+ render plain: "stale" if stale?(strong_etag: "strong", template: false)
end
def with_template
- if stale? template: 'test/hello_world'
- render plain: '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
@@ -35,6 +44,14 @@ class ImplicitRenderTestController < ActionController::Base
end
end
+module Namespaced
+ class ImplicitRenderTestController < ActionController::Base
+ def hello_world
+ fresh_when(etag: "abc")
+ end
+ end
+end
+
class TestController < ActionController::Base
protect_from_forgery
@@ -56,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
@@ -65,7 +82,7 @@ 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
@@ -100,70 +117,84 @@ 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'
+ render action: "hello_world"
end
- def respond_with_empty_body
- render nothing: true
+ 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
- def conditional_hello_with_bangs
- render :action => 'hello_world'
+ def conditional_hello_without_expires_and_confliciting_cache_control_headers
+ response.headers["Cache-Control"] = "no-cache, must-revalidate"
+ 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_public_header
+ response.headers["Cache-Control"] = "public, no-cache"
+ 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
@@ -171,19 +202,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
@@ -199,20 +230,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
@@ -224,6 +255,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
@@ -232,7 +272,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",
@@ -242,22 +282,35 @@ 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
def accessing_logger_in_template
- render :inline => "<%= logger.class %>"
+ render inline: "<%= logger.class %>"
end
end
@@ -271,14 +324,14 @@ class ExpiresInRenderTest < ActionController::TestCase
def test_dynamic_render_with_file
# This is extremely bad, but should be possible to do.
- assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb'))
- response = get :dynamic_render_with_file, params: { id: '../\\../test/abstract_unit.rb' }
- assert_equal File.read(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')),
+ assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__))
+ response = assert_deprecated { 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 = Tempfile.new("name")
file.write "secrets!"
file.flush
assert_raises ActionView::MissingTemplate do
@@ -290,17 +343,16 @@ class ExpiresInRenderTest < ActionController::TestCase
end
def test_dynamic_render
- assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb'))
+ 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
- skip "FIXME: this test passes on 4-2-stable but not master. Why?"
- assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb'))
- response = get :dynamic_render_permit, params: { id: { file: '../\\../test/abstract_unit.rb' } }
- assert_equal File.read(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb')),
+ assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__))
+ response = assert_deprecated { 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
@@ -330,6 +382,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"]
@@ -351,14 +413,23 @@ 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_no_expires_now_with_public
+ get :conditional_hello_without_expires_and_public_header
+ assert_equal "public, no-cache", @response.headers["Cache-Control"]
end
def test_date_header_when_expires_in
- time = Time.mktime(2011,10,30)
+ 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"]
@@ -376,15 +447,15 @@ 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
@@ -395,25 +466,25 @@ class LastModifiedRenderTest < ActionController::TestCase
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
@@ -424,24 +495,24 @@ class LastModifiedRenderTest < ActionController::TestCase
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
@@ -452,16 +523,16 @@ class LastModifiedRenderTest < ActionController::TestCase
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
@@ -480,13 +551,14 @@ end
class EtagRenderTest < ActionController::TestCase
tests TestControllerWithExtraEtags
+ include TemplateModificationHelper
def test_strong_etag
- @request.if_none_match = strong_etag(['strong', 'ab', :cde, [:f]])
+ @request.if_none_match = strong_etag(["strong", "ab", :cde, [:f]])
get :strong
assert_response :not_modified
- @request.if_none_match = '*'
+ @request.if_none_match = "*"
get :strong
assert_response :not_modified
@@ -494,13 +566,13 @@ class EtagRenderTest < ActionController::TestCase
get :strong
assert_response :ok
- @request.if_none_match = weak_etag(['strong', 'ab', :cde, [:f]])
+ @request.if_none_match = weak_etag(["strong", "ab", :cde, [:f]])
get :strong
assert_response :ok
end
def test_multiple_etags
- @request.if_none_match = weak_etag(["123", 'ab', :cde, [:f]])
+ @request.if_none_match = weak_etag(["123", "ab", :cde, [:f]])
get :fresh
assert_response :not_modified
@@ -510,7 +582,7 @@ class EtagRenderTest < ActionController::TestCase
end
def test_array
- @request.if_none_match = weak_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
@@ -528,20 +600,28 @@ 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::LookupContext::DetailsKey.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
@@ -551,8 +631,30 @@ class EtagRenderTest < ActionController::TestCase
end
def strong_etag(record)
- %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}")
+ %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}")
+ end
+end
+
+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
class MetalRenderTest < ActionController::TestCase
@@ -564,11 +666,30 @@ 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_as_browser
- assert_raises(ActionController::UnknownFormat) do
+ assert_raises(ActionController::MissingExactTemplate) do
get :empty_action
end
end
@@ -586,7 +707,7 @@ class ImplicitRenderTest < ActionController::TestCase
def test_implicit_unknown_format_response
assert_raises(ActionController::UnknownFormat) do
- get :empty_action_with_template, format: 'json'
+ get :empty_action_with_template, format: "json"
end
end
end
@@ -600,40 +721,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
@@ -644,12 +752,12 @@ class HeadRenderTest < ActionController::TestCase
resources :customers
ActiveSupport::Deprecation.silence do
- get ':controller/:action'
+ 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
@@ -657,14 +765,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
@@ -680,7 +788,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|
@@ -725,13 +833,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]) do
- render plain: 'hello'
+ render plain: "hello"
end
end
end
@@ -739,11 +852,11 @@ class HttpCacheForeverTest < ActionController::TestCase
tests HttpCacheForeverController
def test_cache_with_public
- get :cache_me_forever, params: {public: true}
+ get :cache_me_forever, params: { public: true }
assert_response :ok
assert_equal "max-age=#{100.years}, public", @response.headers["Cache-Control"]
assert_not_nil @response.etag
- assert @response.weak_etag?
+ assert_predicate @response, :weak_etag?
end
def test_cache_with_private
@@ -751,14 +864,14 @@ class HttpCacheForeverTest < ActionController::TestCase
assert_response :ok
assert_equal "max-age=#{100.years}, private", @response.headers["Cache-Control"]
assert_not_nil @response.etag
- assert @response.weak_etag?
+ assert_predicate @response, :weak_etag?
end
def test_cache_response_code_with_if_modified_since
get :cache_me_forever
assert_response :ok
- @request.if_modified_since = @response.headers['Last-Modified']
+ @request.if_modified_since = @response.headers["Last-Modified"]
get :cache_me_forever
assert_response :not_modified
end
diff --git a/actionpack/test/controller/render_xml_test.rb b/actionpack/test/controller/render_xml_test.rb
index 137236c496..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
@@ -74,7 +76,7 @@ class RenderXmlTest < ActionController::TestCase
resources :customers
ActiveSupport::Deprecation.silence do
- get ':controller/:action'
+ get ":controller/:action"
end
end
@@ -85,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
@@ -94,7 +96,7 @@ class RenderXmlTest < ActionController::TestCase
end
def test_should_use_implicit_content_type
- get :implicit_content_type, format: 'atom'
+ 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 16d24fa82a..ea79f4de85 100644
--- a/actionpack/test/controller/renderer_test.rb
+++ b/actionpack/test/controller/renderer_test.rb
@@ -1,92 +1,134 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class RendererTest < ActiveSupport::TestCase
- test 'action controller base has a renderer' do
+ test "action controller base has a renderer" do
assert ActionController::Base.renderer
end
- test 'creating with a controller' do
+ 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
+
+ new_defaults = { https: true }
+ new_renderer = renderer.with_defaults(new_defaults).new
+ content = new_renderer.render(inline: "<%= request.ssl? %>")
+
+ assert_equal "true", content
+ end
+
+ test "rendering with a class renderer" do
renderer = ApplicationController.renderer
- content = renderer.render template: 'ruby_template'
+ content = renderer.render template: "ruby_template"
- assert_equal 'Hello from Ruby code', content
+ assert_equal "Hello from Ruby code", content
end
- test 'rendering with an instance renderer' do
+ test "rendering with an instance renderer" do
renderer = ApplicationController.renderer.new
- content = renderer.render file: 'test/hello_world'
+ content = assert_deprecated { 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
+ 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
+
+ test "rendering with defaults" do
renderer = ApplicationController.renderer.new https: true
- content = renderer.render inline: '<%= request.ssl? %>'
+ content = renderer.render inline: "<%= request.ssl? %>"
- assert_equal 'true', content
+ 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_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 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
def render
@render ||= ApplicationController.renderer.method(:render)
diff --git a/actionpack/test/controller/renderers_test.rb b/actionpack/test/controller/renderers_test.rb
index e6c2e4636e..d92de6f5d5 100644
--- a/actionpack/test/controller/renderers_test.rb
+++ b/actionpack/test/controller/renderers_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'controller/fake_models'
-require 'active_support/logger'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "controller/fake_models"
+require "active_support/logger"
class RenderersTest < ActionController::TestCase
class XmlRenderable
@@ -10,14 +12,14 @@ class RenderersTest < ActionController::TestCase
end
end
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
class CsvRenderable
@@ -26,9 +28,8 @@ class RenderersTest < ActionController::TestCase
end
end
class TestController < ActionController::Base
-
def render_simon_says
- render :simon => "foo"
+ render simon: "foo"
end
def respond_to_mime
@@ -36,7 +37,7 @@ class RenderersTest < ActionController::TestCase
type.json do
render json: JsonRenderable.new
end
- type.js { render json: 'JS', callback: 'alert' }
+ type.js { render json: "JS", callback: "alert" }
type.csv { render csv: CsvRenderable.new }
type.xml { render xml: XmlRenderable.new }
type.html { render body: "HTML" }
@@ -70,7 +71,7 @@ class RenderersTest < ActionController::TestCase
def test_raises_missing_template_no_renderer
assert_raise ActionView::MissingTemplate do
- get :respond_to_mime, format: 'csv'
+ get :respond_to_mime, format: "csv"
end
assert_equal Mime[:csv], @response.content_type
assert_equal "", @response.body
@@ -81,7 +82,7 @@ class RenderersTest < ActionController::TestCase
send_data value.to_csv, type: Mime[:csv]
end
@request.accept = "text/csv"
- get :respond_to_mime, format: 'csv'
+ get :respond_to_mime, format: "csv"
assert_equal Mime[:csv], @response.content_type
assert_equal "c,s,v", @response.body
ensure
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 f7dcbc1984..ea94a3e048 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -1,42 +1,61 @@
-require 'abstract_unit'
+# 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 plain: '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
@@ -52,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
@@ -90,46 +108,45 @@ 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
+ protect_from_forgery with: :exception
self.per_form_csrf_tokens = true
def index
@@ -141,18 +158,25 @@ class PerFormTokensController < ActionController::Base
end
def post_one
- render plain: ''
+ render plain: ""
end
def post_two
- render plain: ''
+ 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 = Base64.strict_encode64('railstestrailstestrailstestrails')
+ @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
@@ -166,7 +190,7 @@ module RequestForgeryProtectionTests
assert_not_blocked do
get :index
end
- assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
end
end
@@ -175,7 +199,7 @@ module RequestForgeryProtectionTests
assert_not_blocked do
get :show_button
end
- assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
end
end
@@ -206,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
@@ -216,7 +240,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"
end
def test_should_render_form_with_token_tag_if_remote_and_authenticity_token_requested
@@ -224,7 +248,7 @@ module RequestForgeryProtectionTests
assert_not_blocked do
get :form_for_remote_with_token
end
- assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
+ assert_select "form>input[name=?][value=?]", "custom_authenticity_token", @token
end
end
@@ -233,7 +257,81 @@ module RequestForgeryProtectionTests
assert_not_blocked do
get :form_for_with_token
end
- assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
+ 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_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
end
@@ -254,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
@@ -303,25 +401,25 @@ module RequestForgeryProtectionTests
def test_should_allow_post_with_token_in_header
session[:_csrf_token] = @token
- @request.env['HTTP_X_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
session[:_csrf_token] = @token
- @request.env['HTTP_X_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
session[:_csrf_token] = @token
- @request.env['HTTP_X_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
session[:_csrf_token] = @token
- @request.env['HTTP_X_CSRF_TOKEN'] = @token
+ @request.env["HTTP_X_CSRF_TOKEN"] = @token
assert_not_blocked { put :index }
end
@@ -330,7 +428,7 @@ module RequestForgeryProtectionTests
session[:_csrf_token] = @token
@controller.stub :form_authenticity_token, @token do
assert_not_blocked do
- @request.set_header 'HTTP_ORIGIN', 'http://test.host'
+ @request.set_header "HTTP_ORIGIN", "http://test.host"
post :index, params: { custom_authenticity_token: @token }
end
end
@@ -348,16 +446,40 @@ module RequestForgeryProtectionTests
end
end
+ def test_should_raise_for_post_with_null_origin
+ forgery_protection_origin_check do
+ session[:_csrf_token] = @token
+ @controller.stub :form_authenticity_token, @token do
+ exception = assert_raises(ActionController::InvalidAuthenticityToken) do
+ @request.set_header "HTTP_ORIGIN", "null"
+ post :index, params: { custom_authenticity_token: @token }
+ end
+ assert_match "The browser returned a 'null' origin for a request", exception.message
+ end
+ end
+ end
+
def test_should_block_post_with_origin_checking_and_wrong_origin
+ old_logger = ActionController::Base.logger
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ 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'
+ @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
@@ -393,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'
+ @request.accept = "text/javascript"
+ get :negotiate_same_origin
+ end
+
+ assert_cross_origin_blocked do
+ @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
assert_blocked do
- patch :index, params: { custom_authenticity_token: { foo: 'bar' } }
+ patch :index, params: { custom_authenticity_token: { foo: "bar" } }
end
end
@@ -478,20 +636,11 @@ 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.stub :form_authenticity_token, @token + '<=?' do
+ 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'
+ 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
@@ -500,26 +649,27 @@ 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
@@ -579,29 +729,29 @@ class FreeCookieControllerTest < ActionController::TestCase
def test_should_not_render_form_with_token_tag
SecureRandom.stub :base64, @token do
get :index
- assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false
+ assert_select "form>div>input[name=?][value=?]", "authenticity_token", @token, false
end
end
def test_should_not_render_button_to_with_token_tag
SecureRandom.stub :base64, @token do
get :show_button
- assert_select 'form>div>input[name=?][value=?]', 'authenticity_token', @token, false
+ assert_select "form>div>input[name=?][value=?]", "authenticity_token", @token, false
end
end
def test_should_allow_all_methods_without_token
SecureRandom.stub :base64, @token do
[:post, :patch, :put, :delete].each do |method|
- assert_nothing_raised { send(method, :index)}
+ assert_nothing_raised { send(method, :index) }
end
end
end
- test 'should not emit a csrf-token meta tag' do
+ test "should not emit a csrf-token meta tag" do
SecureRandom.stub :base64, @token do
get :meta
- assert @response.body.blank?
+ assert_predicate @response.body, :blank?
end
end
end
@@ -625,7 +775,7 @@ class CustomAuthenticityParamControllerTest < ActionController::TestCase
ActionController::Base.logger = @logger
begin
@controller.stub :valid_authenticity_token?, :true do
- post :index, params: { custom_token_name: 'foobar' }
+ post :index, params: { custom_token_name: "foobar" }
assert_equal 0, @logger.logged(:warn).size
end
ensure
@@ -637,7 +787,7 @@ 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
@@ -646,10 +796,19 @@ class CustomAuthenticityParamControllerTest < ActionController::TestCase
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
+ actual = @controller.send(:per_form_csrf_token, session, "/path", "post").size
assert_equal expected, actual
end
@@ -661,9 +820,9 @@ class PerFormTokensControllerTest < ActionController::TestCase
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'
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
assert_nothing_raised do
- post :post_one, params: {custom_authenticity_token: form_token}
+ post :post_one, params: { custom_authenticity_token: form_token }
end
assert_response :success
end
@@ -676,9 +835,9 @@ class PerFormTokensControllerTest < ActionController::TestCase
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'
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_two"
assert_raises(ActionController::InvalidAuthenticityToken) do
- post :post_two, params: {custom_authenticity_token: form_token}
+ post :post_two, params: { custom_authenticity_token: form_token }
end
end
@@ -690,21 +849,21 @@ class PerFormTokensControllerTest < ActionController::TestCase
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'
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
assert_raises(ActionController::InvalidAuthenticityToken) do
- patch :post_one, params: {custom_authenticity_token: form_token}
+ 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' }
+ 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'
+ 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'
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
assert_raises(ActionController::InvalidAuthenticityToken) do
patch :post_one, params: { custom_authenticity_token: form_token }
end
@@ -715,10 +874,10 @@ class PerFormTokensControllerTest < ActionController::TestCase
form_token = assert_presence_and_fetch_form_csrf_token
- assert_matches_session_token_on_server form_token, 'post'
+ 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'
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
assert_nothing_raised do
post :post_one, params: { custom_authenticity_token: form_token }
end
@@ -733,7 +892,7 @@ class PerFormTokensControllerTest < ActionController::TestCase
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'
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
assert_nothing_raised do
send verb, :post_one, params: { custom_authenticity_token: form_token }
end
@@ -746,37 +905,50 @@ class PerFormTokensControllerTest < ActionController::TestCase
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'
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
assert_nothing_raised do
- post :post_one, params: {custom_authenticity_token: token}
+ 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'}
+ 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'
+ @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'}
+ 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/'}
+ 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'
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one"
assert_nothing_raised do
- post :post_one, params: {custom_authenticity_token: form_token}
+ 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
@@ -787,21 +959,21 @@ class PerFormTokensControllerTest < ActionController::TestCase
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/'
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one/"
assert_nothing_raised do
- post :post_one, params: {custom_authenticity_token: form_token}
+ 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"}
+ 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/'
+ @request.env["PATH_INFO"] = "/per_form_tokens/post_one/"
assert_nothing_raised do
- post :post_one, params: {custom_authenticity_token: form_token}
+ post :post_one, params: { custom_authenticity_token: form_token }
end
assert_response :success
end
@@ -809,15 +981,38 @@ class PerFormTokensControllerTest < ActionController::TestCase
private
def assert_presence_and_fetch_form_csrf_token
assert_select 'input[name="custom_authenticity_token"]' do |input|
- form_csrf_token = input.first['value']
+ 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')
+ 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)
+ 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 b6efcd6f9a..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
@@ -50,17 +51,17 @@ class ParametersRequireTest < ActiveSupport::TestCase
end
test "require array when all required params are present" do
- safe_params = ActionController::Parameters.new(person: {first_name: 'Gaurish', title: 'Mjallo', city: 'Barcelona'})
+ 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
+ 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})
+ ActionController::Parameters.new(person: { first_name: "Gaurish", title: nil })
.require(:person)
.require([:first_name, :title])
end
@@ -73,9 +74,27 @@ class ParametersRequireTest < ActiveSupport::TestCase
assert params.value?("cinco")
end
- test "Deprecated methods are deprecated" do
- assert_deprecated do
- ActionController::Parameters.new(foo: "bar").merge!({bar: "foo"})
+ 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 ed78f859ce..089b0b94d4 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,61 +33,44 @@ 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 plain: exception.message }
- rescue_from 'InvalidRequestToRescueAsString', 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 plain: exception.message
end
- rescue_from 'ResourceUnavailableToRescueAsString' do |exception|
+ rescue_from "ResourceUnavailableToRescueAsString" do |exception|
render plain: exception.message
end
- rescue_from ActionView::TemplateError do
- render plain: 'action_view templater error'
- end
-
- rescue_from IOError do
- render plain: 'io error'
+ 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 plain: 'already rendered'
- raise "don't panic!"
- end
-
- def method_not_allowed
- raise ActionController::MethodNotAllowed.new(:get, :head, :put)
- end
-
- def not_implemented
- raise ActionController::NotImplemented.new(:get, :put)
- end
-
def not_authorized
raise NotAuthorized
end
@@ -128,23 +113,12 @@ class RescueController < ActionController::Base
raise ResourceUnavailableToRescueAsString
end
- def missing_template
+ def arbitrary_action
+ params
+ render plain: "arbitrary action"
end
- def io_error_in_view
- begin
- raise IOError.new('this is io error')
- rescue
- raise ActionView::TemplateError.new(nil)
- end
- end
-
- def zero_division_error_in_view
- begin
- raise ZeroDivisionError.new('this is zero division error')
- rescue
- raise ActionView::TemplateError.new(nil)
- end
+ def missing_template
end
def exception_with_more_specific_handler_for_wrapper
@@ -165,7 +139,7 @@ class RescueController < ActionController::Base
raise RangeError
end
- protected
+ private
def deny_access
head :forbidden
end
@@ -176,7 +150,6 @@ class RescueController < ActionController::Base
end
class ExceptionInheritanceRescueController < ActionController::Base
-
class ParentException < StandardError
end
@@ -186,9 +159,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
@@ -222,7 +195,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
@@ -251,17 +224,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
@@ -276,7 +238,6 @@ class RescueControllerTest < ActionController::TestCase
get :record_invalid
end
end
-
def test_rescue_handler_with_argument_as_string
assert_called_with @controller, :show_errors, [Exception] do
get :record_invalid_raise_as_string
@@ -314,36 +275,52 @@ 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
+ test "rescue when wrapper has more specific handler than cause" do
get :exception_with_more_specific_handler_for_wrapper
- assert_response :unprocessable_entity
+ assert_response :forbidden
end
- test 'rescue when cause has more specific handler than wrapper' do
+ 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
+ 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 plain: "foo"
@@ -353,27 +330,23 @@ class RescueTest < ActionDispatch::IntegrationTest
raise RecordInvalid
end
- def b00m
- raise 'b00m'
- end
-
- protected
+ private
def show_errors(exception)
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
@@ -382,9 +355,8 @@ 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)
end
yield
end
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
index 4490abf7b2..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
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
+ 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
+ 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
+ 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,11 +261,11 @@ 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
@@ -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
+ assert_restful_named_routes_for :messages, path_names: { new: "nuevo" } do
assert_named_route mark_path, :mark_message_path, mark_options
end
end
@@ -291,17 +306,17 @@ 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
@@ -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,14 +356,14 @@ 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
@@ -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
+ 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
+ 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
@@ -528,13 +543,13 @@ class ResourcesTest < ActionController::TestCase
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,14 +558,14 @@ 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
@@ -565,16 +580,16 @@ 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
@@ -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,107 +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)
+ 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
+ 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])
+ 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
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", {}, :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
@@ -854,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
@@ -867,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
@@ -880,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
@@ -893,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
@@ -906,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
@@ -919,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
@@ -1042,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
@@ -1075,7 +1112,7 @@ class ResourcesTest < ActionController::TestCase
end
end
- protected
+ private
def with_restful_routing(*args)
options = args.extract_options!
collection_methods = options.delete(:collection)
@@ -1085,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
@@ -1111,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
@@ -1155,34 +1192,34 @@ class ResourcesTest < ActionController::TestCase
formatted_edit_member_path = "#{member_path}/#{edit_action}.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'
+ 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'
- end
-
- 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(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)
+ 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(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(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 route_options if block_given?
end
@@ -1214,7 +1251,7 @@ class ResourcesTest < ActionController::TestCase
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"
@@ -1224,14 +1261,14 @@ class ResourcesTest < ActionController::TestCase
end
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}.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", 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')
+ 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 route_options if block_given?
end
@@ -1246,27 +1283,27 @@ class ResourcesTest < ActionController::TestCase
formatted_edit_path = "#{full_path}/edit.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(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(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)
+ 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(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(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 route_options if block_given?
end
@@ -1283,23 +1320,23 @@ class ResourcesTest < ActionController::TestCase
name_prefix = options[:name_prefix]
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}.xml", "#{name_prefix}#{singleton_name}_path", route_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}/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')
+ 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
@@ -1307,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 c477b4156c..b378bb80b8 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
@@ -16,43 +18,41 @@ class UriReservedCharactersRoutingTest < ActiveSupport::TestCase
@set = ActionDispatch::Routing::RouteSet.new
@set.draw do
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:variable/*additional'
+ 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
@@ -86,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]]
}
@@ -193,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
@@ -259,174 +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)
+ 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 { 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"))
+ 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 })
+ 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
ActiveSupport::Deprecation.silence do
- get '/:controller/:action/:id', action: 'index', id: nil
+ get "/:controller/:action/:id", action: "index", id: nil
end
- get ':url', controller: 'content', action: 'translate'
+ get ":url", controller: "content", action: "translate"
end
- assert_equal({controller: 'content', action: 'translate', url: 'example'}, rs.recognize_path('/example'))
+ 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[-|_].+/ } }
+ 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({ action: "auth_google", controller: "content" }, rs.recognize_path("/content/auth_google"))
+ assert_equal({ action: "auth-twitter", controller: "content" }, rs.recognize_path("/content/auth-twitter"))
- 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" })
+ assert_equal "/content/auth_google", url_for(rs, controller: "content", action: "auth_google")
+ assert_equal "/content/auth-twitter", url_for(rs, controller: "content", action: "auth-twitter")
end
def test_route_with_regexp_for_controller
rs.draw do
ActiveSupport::Deprecation.silence do
- get ':controller/:admintoken(/:action(/:id))', :controller => /admin\/.+/
- get '/:controller(/:action(/:id))'
+ 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
ActiveSupport::Deprecation.silence do
- get '/:controller(/:action(/:id))', :controller => /admin\/(accounts|users)/
+ 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
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:file',
- :controller => /admin|user/,
- :action => /upload|download/,
- :defaults => {:file => nil},
- :constraints => {:file => %r{[^/]+(\.[^/]+)?}}
+ 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
@@ -437,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
@@ -447,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",
@@ -456,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
@@ -469,14 +485,14 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_named_route_without_hash
rs.draw do
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id', :as => 'normal'
+ 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))
@@ -520,62 +536,62 @@ 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+/
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id'
+ 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 { ActiveSupport::Deprecation.silence { 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 "file/*path" => "content#show_file", :as => "path"
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id'
+ 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
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id'
+ get ":controller/:action/:id"
end
end
assert_raise(ActionController::RoutingError) { rs.recognize_path("/not_a/show/10") }
@@ -583,186 +599,186 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
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
ActiveSupport::Deprecation.silence do
- get 'page/:id(/:action)' => 'pages#show'
- get ':controller(/:action(/:id))'
+ 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 "page(/:id)" => "content#show_page", :id => 1
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id'
+ 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 "page/:id" => "content#show_page", :id => 1
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id'
+ 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 { 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)
+ 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}/ }
)
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id'
+ 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 "pages(/:year(/:month(/:day)))" => "content#list_pages", :month => nil, :day => nil
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id'
+ 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 "categories" => "content#categories", :as => "categories"
ActiveSupport::Deprecation.silence do
- get ':controller(/:action(/:id))'
+ 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
@@ -772,56 +788,55 @@ 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
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id'
+ 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
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'
+ 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
@@ -845,20 +860,20 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_subpath_generated
rs.draw do
ActiveSupport::Deprecation.silence do
- get '/books/:id/edit' => 'subpath_books#edit'
- get '/items/:id/:action' => 'subpath_books'
- get '/posts/new/:action' => 'subpath_books'
+ 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
@@ -869,13 +884,13 @@ 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 "ca" => "ca#aa"
+ get "cb" => "cb#ab"
+ get "cc" => "cc#ac"
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id'
- get ':controller/:action/:id.:format'
+ get ":controller/:action/:id"
+ get ":controller/:action/:id.:format"
end
end
@@ -885,12 +900,12 @@ 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 "cb" => "cb#ab"
+ get "cc" => "cc#ac"
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id'
- get ':controller/:action/:id.:format'
+ get ":controller/:action/:id"
+ get ":controller/:action/:id.:format"
end
end
@@ -922,9 +937,8 @@ class RouteSetTest < ActiveSupport::TestCase
@default_route_set ||= begin
set = ActionDispatch::Routing::RouteSet.new
set.draw do
-
ActiveSupport::Deprecation.silence do
- get '/:controller(/:action(/:id))'
+ get "/:controller(/:action(/:id))"
end
end
set
@@ -932,26 +946,26 @@ class RouteSetTest < ActiveSupport::TestCase
end
def test_generate_extras
- set.draw { ActiveSupport::Deprecation.silence { 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 { ActiveSupport::Deprecation.silence { 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
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id.:format'
- get ':controller/:action/:id'
+ 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
@@ -959,29 +973,29 @@ class RouteSetTest < ActiveSupport::TestCase
def test_generate_not_first
set.draw do
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id.:format'
- get ':controller/:action/:id'
+ 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
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id.:format'
- get ':controller/:action/:id'
+ 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
@@ -989,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]
@@ -1007,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)
@@ -1065,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
@@ -1087,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
@@ -1099,46 +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
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id'
+ 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 "page/:id" => "pages#show", :id => /\d+/
ActiveSupport::Deprecation.silence do
- get '/:controller(/:action(/:id))'
+ 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
@@ -1146,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
@@ -1174,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
@@ -1184,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
@@ -1206,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])
@@ -1273,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
@@ -1291,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])
@@ -1309,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
@@ -1380,37 +1392,37 @@ class RouteSetTest < ActiveSupport::TestCase
set.draw do
ActiveSupport::Deprecation.silence do
- get ':controller/:id/:action'
+ 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 "about" => "welcome#about"
ActiveSupport::Deprecation.silence do
- get ':controller/:id/:action'
+ 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 { ActiveSupport::Deprecation.silence { 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)
@@ -1420,12 +1432,12 @@ class RouteSetTest < ActiveSupport::TestCase
set.draw do
scope "my" do
ActiveSupport::Deprecation.silence do
- get ':controller(/:action(/:id))'
+ 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
@@ -1433,12 +1445,12 @@ class RouteSetTest < ActiveSupport::TestCase
set.draw do
scope "" do
ActiveSupport::Deprecation.silence do
- get ':controller(/:action(/:id))'
+ 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
@@ -1447,20 +1459,20 @@ class RouteSetTest < ActiveSupport::TestCase
set.draw do
ActiveSupport::Deprecation.silence do
- get "/connection/manage(/:action)" => 'connection/manage#index'
+ get "/connection/manage(/:action)" => "connection/manage#index"
get "/connection/connection" => "connection/connection#index"
- get '/connection' => 'connection#index', :as => 'family_connection'
+ 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
@@ -1469,75 +1481,75 @@ class RouteSetTest < ActiveSupport::TestCase
set.draw do
ActiveSupport::Deprecation.silence do
- get ':controller(/:action(/:id))'
+ 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 "show_weblog/:parameter" => "weblog#show"
ActiveSupport::Deprecation.silence do
- get ':controller(/:action(/:id))'
+ 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 { ActiveSupport::Deprecation.silence { 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
@@ -1546,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
@@ -1555,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
@@ -1565,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
@@ -1574,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
@@ -1594,243 +1606,243 @@ 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
ActiveSupport::Deprecation.silence do
- get '/cars/:action/:person/:car/', :controller => 'cars'
+ 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
ActiveSupport::Deprecation.silence do
- get '/books/:action.rss', :controller => 'books'
+ 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
ActiveSupport::Deprecation.silence do
- get '/books(/:action(.:format))', :controller => 'books'
+ 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 { 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 "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'
+ 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
@@ -1841,21 +1853,21 @@ class RouteSetTest < ActiveSupport::TestCase
@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+/
+ get "blog/show/:id", controller: "blog", action: "show", id: /\d+/
ActiveSupport::Deprecation.silence do
get "blog/:controller/:action(/:id)"
end
- get "*anything", :controller => "blog", :action => "unknown_request"
+ get "*anything", controller: "blog", action: "unknown_request"
end
recognize_path = ->(path) {
@@ -1863,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
@@ -1889,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
@@ -1905,59 +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'
+ 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'
+ get "ã“ã‚“ã«ã¡ã¯/世界", controller: "news", action: "index"
ActiveSupport::Deprecation.silence do
- match ':controller(/:action(/:id))(.:format)', :via => :all
+ match ":controller(/:action(/:id))(.:format)", via: :all
end
- root :to => "news#index"
+ root to: "news#index"
}
attr_reader :routes
@@ -1969,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 2820425c31..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,11 +24,15 @@ 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'
+ type: Mime[:png],
+ disposition: "disposition",
+ filename: "filename"
}
send_data "foo", options
@@ -32,32 +40,32 @@ class SendFileController < ActionController::Base
def test_send_file_headers_with_disposition_as_a_symbol
options = {
- :type => Mime[:png],
- :disposition => :disposition,
- :filename => 'filename'
+ 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 }
+ options = { type: :png }
send_data "foo", options
end
def test_send_file_headers_with_bad_symbol
- options = { :type => :this_type_is_not_registered }
+ options = { type: :this_type_is_not_registered }
send_data "foo", options
end
def test_send_file_headers_with_nil_content_type
- options = { :type => nil }
+ options = { type: nil }
send_data "foo", options
end
def test_send_file_headers_guess_type_from_extension
- options = { :filename => params[:filename] }
+ options = { filename: params[:filename] }
send_data "foo", options
end
@@ -78,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
@@ -89,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)
@@ -103,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
@@ -120,10 +128,10 @@ 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
@@ -135,25 +143,24 @@ class SendFileTest < ActionController::TestCase
5.times do
get :test_send_file_headers_bang
- assert_equal 'image/png', response.content_type
- assert_equal 'disposition; filename="filename"', response.get_header('Content-Disposition')
- assert_equal 'binary', response.get_header('Content-Transfer-Encoding')
- assert_equal 'private', response.get_header('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
get :test_send_file_headers_with_disposition_as_a_symbol
- assert_equal 'disposition; filename="filename"', response.get_header('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
get __method__
- assert_equal 'image/png', response.content_type
+ assert_equal "image/png", response.content_type
end
-
def test_send_file_headers_with_bad_symbol
error = assert_raise(ArgumentError) { get __method__ }
assert_equal "Unknown MIME type this_type_is_not_registered", error.message
@@ -166,47 +173,56 @@ class SendFileTest < ActionController::TestCase
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|
+ "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 }
+ @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
@@ -214,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..8724f9bcdb 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,40 +74,41 @@ 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
class ShowFailsafeExceptionsTest < ActionDispatch::IntegrationTest
def test_render_failsafe_exception
@app = ShowExceptionsOverriddenController.action(:boom)
- @exceptions_app = @app.instance_variable_get(:@exceptions_app)
- @app.instance_variable_set(:@exceptions_app, nil)
+ middleware = @app.instance_variable_get(:@middleware)
+ @exceptions_app = middleware.instance_variable_get(:@exceptions_app)
+ middleware.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)
+ middleware.instance_variable_set(:@exceptions_app, @exceptions_app)
$stderr = STDERR
end
end
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 ebcdda6074..998a495d0d 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -1,40 +1,42 @@
-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;
+ def self.fixture_path; end
class TestController < ActionController::Base
def no_op
- render plain: 'dummy'
+ render plain: "dummy"
end
def set_flash
flash["test"] = ">#{flash["test"]}<"
- render plain: 'ignore me'
+ render plain: "ignore me"
end
def delete_flash
flash.delete("test")
- render plain: 'ignore me'
+ render plain: "ignore me"
end
def set_flash_now
flash.now["test_now"] = ">#{flash["test_now"]}<"
- render plain: 'ignore me'
+ render plain: "ignore me"
end
def set_session
- session['string'] = 'A wonder'
- session[:symbol] = 'it works'
- render plain: 'Success'
+ session["string"] = "A wonder"
+ session[:symbol] = "it works"
+ render plain: "Success"
end
def reset_the_session
reset_session
- render plain: 'ignore me'
+ render plain: "ignore me"
end
def render_raw_post
@@ -100,11 +102,11 @@ HTML
end
def test_xml_output
- response.content_type = "application/xml"
+ response.content_type = params[:response_as]
render plain: <<XML
<?xml version="1.0" encoding="UTF-8"?>
<root>
- <area>area is an empty tag in HTML, raising an error if not in xml mode</area>
+ <area><p>area is an empty tag in HTML, so it won't contain this content</p></area>
</root>
XML
end
@@ -122,19 +124,19 @@ XML
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
@@ -143,7 +145,7 @@ XML
def delete_cookie
cookies.delete("foo")
- render plain: 'ok'
+ render plain: "ok"
end
def test_without_body
@@ -154,6 +156,14 @@ XML
render html: '<body class="foo"></body>'.html_safe
end
+ def render_json
+ render json: request.raw_post
+ end
+
+ def boom
+ raise "boom!"
+ end
+
private
def generate_url(opts)
@@ -164,18 +174,18 @@ XML
def setup
super
@controller = TestController.new
- @request.delete_header 'PATH_INFO'
+ @request.delete_header "PATH_INFO"
@routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
r.draw do
ActiveSupport::Deprecation.silence do
- get ':controller(/:action(/:id))'
+ 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 plain: url_for
@@ -193,49 +203,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
@@ -243,21 +266,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
@@ -265,31 +278,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
@@ -301,64 +304,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
@@ -389,11 +366,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",
@@ -402,14 +374,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
@@ -417,13 +383,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
@@ -435,36 +401,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
+
+ def test_should_not_impose_childless_html_tags_in_xml
+ process :test_xml_output, params: { response_as: "application/xml" }
- assert err.empty?
+ # <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
@@ -472,125 +444,113 @@ 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' }
+ def test_params_passing
+ 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
- get :test_params, params: {
- page: {
- name: "Page name",
- month: '4',
- year: '2004',
- day: '6'
- }
- }
- parsed_params = ::JSON.parse(@response.body)
+ def test_nil_params
+ get :test_params, params: nil
+ 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' }
+ "action" => "test_params",
+ "controller" => "test_case_test/test"
},
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 }
- 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 }
+ 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' },
+ { "controller" => "test_case_test/test", "action" => "test_params",
+ "format" => "json", "id" => "1" },
parsed_params
)
end
@@ -598,13 +558,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
@@ -616,8 +576,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"]
@@ -625,36 +585,50 @@ 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' }
+ @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]
+ 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' }
+ @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]
+ 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' }
+ @request.headers["Content-Type"] = "type/fake"
+ post :render_body, params: { name: "foo.fake" }
end
end
@@ -665,24 +639,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 "file/*path", to: "test_case_test/test#test_params"
ActiveSupport::Deprecation.silence do
- get ':controller/:action'
+ 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
@@ -700,8 +669,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
@@ -720,46 +689,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
@@ -767,7 +713,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
@@ -778,6 +724,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]
@@ -800,54 +767,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'] = '+'
+ cookies["foo"] = "+"
get :render_cookie
- assert_equal '+', @response.body
+ 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|
@@ -866,15 +849,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)
@@ -887,20 +870,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.stub :fixture_path, FILES_DIR do
- 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
+ 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
@@ -910,9 +892,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
@@ -922,72 +904,100 @@ 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.stub :fixture_path, FILES_DIR do
- 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
+ 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
- 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
get :create
assert_response :created
- # Redirect url doesn't care that it wasn't a :redirect response.
- assert_equal 'created resource', @response.redirect_url
+ # Redirect URL doesn't care that it wasn't a :redirect response.
+ 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
+
+ def test_parsed_body_without_as_option
+ post :render_json, body: { foo: "heyo" }
+ assert_equal({ "foo" => "heyo" }, response.parsed_body)
+ end
+
+ def test_parsed_body_with_as_option
+ post :render_json, body: { foo: "heyo" }.to_json, as: :json
+ assert_equal({ "foo" => "heyo" }, response.parsed_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
@@ -998,7 +1008,7 @@ class ResponseDefaultHeadersTest < ActionController::TestCase
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
@@ -1010,11 +1020,11 @@ 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
ActiveSupport::Deprecation.silence do
- get ':controller(/:action(/:id))'
+ get ":controller(/:action(/:id))"
end
end
end
@@ -1024,18 +1034,18 @@ class ResponseDefaultHeadersTest < ActionController::TestCase
get :leave_alone
# Response headers start out with the defaults
- assert_equal @defaults.merge('Content-Type' => 'text/html'), response.headers
+ assert_equal @defaults.merge("Content-Type" => "text/html"), response.headers
end
test "response deletes a default header" do
- get :remove_header, params: { header: 'A' }
+ 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
@@ -1044,13 +1054,13 @@ module EngineControllerTests
isolate_namespace EngineControllerTests
routes.draw do
- get '/' => 'bar#index'
+ get "/" => "bar#index"
end
end
class BarController < ActionController::Base
def index
- render plain: 'bar'
+ render plain: "bar"
end
end
@@ -1059,7 +1069,7 @@ module EngineControllerTests
def test_engine_controller_route
get :index
- assert_equal @response.body, 'bar'
+ assert_equal @response.body, "bar"
end
end
@@ -1072,7 +1082,7 @@ module EngineControllerTests
def test_engine_controller_route
get :index
- assert_equal @response.body, 'bar'
+ assert_equal @response.body, "bar"
end
end
end
@@ -1113,7 +1123,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
@@ -1126,8 +1136,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
@@ -1143,7 +1153,7 @@ class AnonymousControllerTest < ActionController::TestCase
@routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
r.draw do
ActiveSupport::Deprecation.silence do
- get ':controller(/:action(/:id))'
+ get ":controller(/:action(/:id))"
end
end
end
@@ -1151,7 +1161,7 @@ class AnonymousControllerTest < ActionController::TestCase
def test_controller_name
get :index
- assert_equal 'anonymous', @response.body
+ assert_equal "anonymous", @response.body
end
end
@@ -1169,19 +1179,19 @@ class RoutingDefaultsTest < ActionController::TestCase
@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 a6ca5fc868..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,54 +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
@@ -73,111 +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/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' }]],
+ ["/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 b4d2088c0a..9222250b9c 100644
--- a/actionpack/test/controller/url_for_test.rb
+++ b/actionpack/test/controller/url_for_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module AbstractController
module Testing
@@ -7,7 +9,7 @@ module AbstractController
include ActionDispatch::Routing::RouteSet.new.tap { |r|
r.draw {
ActiveSupport::Deprecation.silence {
- get ':controller(/:action(/:id(.:format)))'
+ get ":controller(/:action(/:id(.:format)))"
}
}
}.url_helpers
@@ -21,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!
@@ -45,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
@@ -212,118 +214,118 @@ 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 { ActiveSupport::Deprecation.silence { 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 "home/sweet/home/:user", to: "home#index", as: :home
ActiveSupport::Deprecation.silence do
- get ':controller/:action/:id'
+ get ":controller/:action/:id"
end
end
@@ -331,83 +333,91 @@ module AbstractController
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_params_option
+ url = W.new.url_for(only_path: true, controller: "c", action: "a", params: { domain: "foo", id: "1" })
+ params = extract_params(url)
+ assert_equal("/c/a?domain=foo&id=1", url)
+ assert_equal({ domain: "foo" }.to_query, params[0])
+ assert_equal({ id: "1" }.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(ArgumentError) do
- W.new.url_for(ActionController::Parameters.new(:controller => 'c', :action => 'a', protocol: 'javascript', f: '%0Aeval(name)'))
+ 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))
@@ -418,35 +428,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
@@ -458,11 +468,11 @@ 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
@@ -476,9 +486,9 @@ 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"
- original_components = [:new, :admin, :post, { param: 'value' }]
+ original_components = [:new, :admin, :post, { param: "value" }]
components = original_components.dup
kls.new.url_for(components)
@@ -487,9 +497,30 @@ module AbstractController
end
end
+ def test_default_params_first_empty
+ with_routing do |set|
+ set.draw do
+ get "(:param1)/test(/:param2)" => "index#index",
+ defaults: {
+ param1: 1,
+ param2: 2
+ },
+ constraints: {
+ param1: /\d*/,
+ param2: /\d+/
+ }
+ end
+
+ kls = Class.new { include set.url_helpers }
+ kls.default_url_options[:host] = "www.basecamphq.com"
+
+ assert_equal "http://www.basecamphq.com/test", kls.new.url_for(controller: "index", param1: "1")
+ end
+ end
+
private
def extract_params(url)
- url.split('?', 2).last.split('&').sort
+ 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 bc0d215530..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,75 +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
ActiveSupport::Deprecation.silence do
- get ':controller(/:action(/:id))'
+ 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 daf17558aa..23a46df5cd 100644
--- a/actionpack/test/controller/webservice_test.rb
+++ b/actionpack/test/controller/webservice_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"
class WebServiceTest < ActionDispatch::IntegrationTest
class TestController < ActionController::Base
@@ -7,12 +9,12 @@ class WebServiceTest < ActionDispatch::IntegrationTest
if params[:full]
render plain: dump_params_keys
else
- render plain: (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|
+ hash.keys.sort.each_with_object(+"") do |k, s|
value = hash[k]
if value.is_a?(Hash) || value.is_a?(ActionController::Parameters)
@@ -35,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
@@ -43,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
@@ -55,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
@@ -90,9 +92,9 @@ 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
@@ -102,8 +104,8 @@ class WebServiceTest < ActionDispatch::IntegrationTest
{ 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' })
+ 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
@@ -125,7 +127,7 @@ class WebServiceTest < ActionDispatch::IntegrationTest
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 7b707df7f6..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
@@ -35,30 +37,11 @@ class DispatcherTest < ActiveSupport::TestCase
assert_equal 6, Foo.b
end
- def test_to_prepare_and_cleanup_delegation
- prepared = cleaned = false
- assert_deprecated do
- ActionDispatch::Callbacks.to_prepare { prepared = true }
- ActionDispatch::Callbacks.to_prepare { cleaned = true }
- end
-
- assert_deprecated do
- ActionDispatch::Reloader.prepare!
- end
- assert prepared
-
- assert_deprecated do
- ActionDispatch::Reloader.cleanup!
- end
- assert cleaned
- end
-
private
def dispatch(&block)
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 dfcef14344..d129fa717d 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -1,7 +1,9 @@
-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
@@ -12,26 +14,32 @@ class CookieJarTest < ActiveSupport::TestCase
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')
+ 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)
+ 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 }
+ 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)
+ 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
@@ -41,24 +49,24 @@ class CookieJarTest < ActiveSupport::TestCase
end
def test_each
- request.cookie_jar['foo'] = :bar
+ request.cookie_jar["foo"] = :bar
list = []
- request.cookie_jar.each do |k,v|
+ request.cookie_jar.each do |k, v|
list << [k, v]
end
- assert_equal [['foo', :bar]], list
+ 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
+ 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 !request.cookie_jar.key?(:foo)
- assert !request.cookie_jar.has_key?("foo")
+ assert_not request.cookie_jar.key?(:foo)
+ assert_not request.cookie_jar.has_key?("foo")
request.cookie_jar[:foo] = :bar
assert request.cookie_jar.key?(:foo)
@@ -68,7 +76,7 @@ class CookieJarTest < ActiveSupport::TestCase
def test_write_doesnt_set_a_nil_header
headers = {}
request.cookie_jar.write(headers)
- assert !headers.include?('Set-Cookie')
+ assert_not_includes headers, "Set-Cookie"
end
end
@@ -95,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
@@ -115,6 +123,11 @@ class CookiesTest < ActionController::TestCase
head :ok
end
+ def set_cookie_if_not_present
+ cookies["user_name"] = "alice" unless cookies["user_name"].present?
+ head :ok
+ end
+
def logout
cookies.delete("user_name")
head :ok
@@ -123,17 +136,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
@@ -153,7 +166,7 @@ class CookiesTest < ActionController::TestCase
end
def set_encrypted_cookie
- cookies.encrypted[:foo] = 'bar'
+ cookies.encrypted[:foo] = "bar"
head :ok
end
@@ -173,7 +186,7 @@ class CookiesTest < ActionController::TestCase
end
def set_wrapped_encrypted_cookie
- cookies.encrypted[:foo] = JSONWrapper.new('bar')
+ cookies.encrypted[:foo] = JSONWrapper.new("bar")
head :ok
end
@@ -183,12 +196,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
@@ -205,47 +218,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
@@ -255,7 +268,7 @@ class CookiesTest < ActionController::TestCase
end
def string_key
- cookies['user_name'] = "dhh"
+ cookies["user_name"] = "dhh"
head :ok
end
@@ -265,27 +278,85 @@ 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
+
+ def encrypted_cookie
+ cookies.encrypted["foo"]
+ end
+
+ def cookie_expires_in_two_hours
+ cookies[:user_name] = { value: "assain", expires: 2.hours }
+ head :ok
+ end
+
+ def encrypted_discount_and_user_id_cookie
+ cookies.encrypted[:user_id] = { value: 50, expires: 1.hour }
+ cookies.encrypted[:discount_percentage] = 10
+
+ head :ok
+ end
+
+ def signed_discount_and_user_id_cookie
+ cookies.signed[:user_id] = { value: 50, expires: 1.hour }
+ cookies.signed[:discount_percentage] = 10
+
+ head :ok
+ end
+
+ 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
+
+ def rails_5_2_stable_encrypted_cookie_with_authenticated_encryption_flag_off
+ cookies[:favorite] = "rTG4zs5UufEFAr+ppKwh+MDMymKyAUMOSaWyYa3uUVmD8sMQqyiyQBxgYeAncDHVZIlo4y+kDVSzp66u1/7BNYpnmFe8ES/YT2m8ckNA23jBDmnRZ9CTNfMIRXjFtfxO9YxEOzzhn0ZiA0/zFtr5wkluXtxplOz959Q7MgLOyvTze2h9p8A=--QHOS3rAEGq/HCxXs--xQNra8dk24Idc2qBtpMLpg=="
+
+ head :ok
+ 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"
+
+ head :ok
+ end
+
+ def rails_5_2_stable_signed_cookie_with_authenticated_encryption_flag_off
+ cookies[:favorite] = "BAhJIiE1LTItU3RhYmxlIENob2NvIENoaXAgQ29va2llBjoGRVQ=--50bbdbf8d64f5a3ec3e54878f54d4f55b6cb3aff"
+
+ head :ok
+ end
end
tests TestController
- SALT = 'b3c631c314c0bbca50c1b2843150fe33'
+ SECRET_KEY_BASE = "b3c631c314c0bbca50c1b2843150fe33"
+ SIGNED_COOKIE_SALT = "signed cookie"
+ ENCRYPTED_COOKIE_SALT = "encrypted cookie"
+ ENCRYPTED_SIGNED_COOKIE_SALT = "signed encrypted cookie"
+ AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "authenticated encrypted cookie"
def setup
super
- @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SALT, iterations: 2)
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SECRET_KEY_BASE, iterations: 2)
+ @request.env["action_dispatch.cookies_rotations"] = ActiveSupport::Messages::RotationConfiguration.new
+
+ @request.env["action_dispatch.secret_key_base"] = SECRET_KEY_BASE
+ @request.env["action_dispatch.use_authenticated_cookie_encryption"] = true
- @request.env["action_dispatch.signed_cookie_salt"] =
- @request.env["action_dispatch.encrypted_cookie_salt"] =
- @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT
+ @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
@@ -293,57 +364,57 @@ class CookiesTest < ActionController::TestCase
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
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
@@ -351,14 +422,14 @@ class CookiesTest < ActionController::TestCase
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
@@ -366,14 +437,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
@@ -381,20 +452,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
@@ -410,7 +481,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
@@ -420,28 +491,57 @@ 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_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
@@ -469,23 +569,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}"
@@ -497,15 +596,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}"
@@ -520,91 +619,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
@@ -612,20 +680,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
@@ -638,7 +706,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
@@ -653,10 +721,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
@@ -681,265 +749,148 @@ class CookiesTest < ActionController::TestCase
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) {
- @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("")
- get :set_signed_cookie
- }
- end
-
- def test_raises_argument_error_if_secret_is_probably_insecure
- assert_raise(ArgumentError, "password".inspect) {
- @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("password")
- get :set_signed_cookie
- }
-
- assert_raise(ArgumentError, "secret".inspect) {
- @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("secret")
- get :set_signed_cookie
- }
-
- assert_raise(ArgumentError, "12345678901234567890123456789".inspect) {
- @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("12345678901234567890123456789")
- get :set_signed_cookie
- }
- end
+ def test_legacy_signed_cookie_is_treated_as_nil_by_signed_cookie_jar_if_tampered
+ @request.headers["Cookie"] = "user_id=45"
+ get :get_signed_cookie
- 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
+ assert_nil @controller.send(:cookies).signed[:user_id]
+ assert_nil @response.cookies["user_id"]
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_legacy_signed_cookie_is_treated_as_nil_by_encrypted_cookie_jar_if_tampered
+ @request.headers["Cookie"] = "foo=baz"
+ get :get_encrypted_cookie
- 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
+ assert_nil @controller.send(:cookies).encrypted[:foo]
+ assert_nil @response.cookies["foo"]
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_use_authenticated_cookie_encryption_uses_legacy_hmac_aes_cbc_encryption_when_not_enabled
+ @request.env["action_dispatch.use_authenticated_cookie_encryption"] = nil
- 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
+ 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)
- 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
+ get :set_encrypted_cookie
- 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
+ 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_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_rotating_signed_cookies_digest
+ @request.env["action_dispatch.signed_cookie_digest"] = "SHA256"
+ @request.env["action_dispatch.cookies_rotations"].rotate :signed, digest: "SHA1"
- 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"
+ key_generator = @request.env["action_dispatch.key_generator"]
- legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45)
+ 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=#{legacy_value}"
+ @request.headers["Cookie"] = "user_id=#{old_value}"
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)
+ verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA256")
assert_equal 45, verifier.verify(@response.cookies["user_id"])
end
- 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')
-
- @request.headers["Cookie"] = "foo=#{legacy_value}"
- get :get_encrypted_cookie
-
- assert_equal 'bar', @controller.send(:cookies).encrypted[:foo]
-
+ def test_legacy_hmac_aes_cbc_encrypted_marshal_cookie_is_upgraded_to_authenticated_encrypted_cookie
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"])
- end
+ 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")
- 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"
+ @request.headers["Cookie"] = "foo=#{marshal_value}"
- legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45)
+ get :get_encrypted_cookie
- @request.headers["Cookie"] = "user_id=#{legacy_value}"
- get :get_signed_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal "bar", cookies[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
- assert_equal 45, @controller.send(:cookies).signed[:user_id]
+ 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)
- 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: JSON)
- assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"])
end
- 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
+ def test_legacy_hmac_aes_cbc_encrypted_json_cookie_is_upgraded_to_authenticated_encrypted_cookie
@request.env["action_dispatch.cookies_serializer"] = :json
- @request.env["action_dispatch.secret_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')
-
- @request.headers["Cookie"] = "foo=#{legacy_value}"
- get :get_encrypted_cookie
-
- 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"])
- end
+ 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")
- 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"
+ @request.headers["Cookie"] = "foo=#{marshal_value}"
- legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45)
+ get :get_encrypted_cookie
- @request.headers["Cookie"] = "user_id=#{legacy_value}"
- get :get_signed_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal "bar", cookies[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
- assert_equal 45, @controller.send(:cookies).signed[:user_id]
+ 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)
- 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: JSON)
- assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ assert_equal "bar", aead_encryptor.decrypt_and_verify(@response.cookies["foo"])
end
- 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"
+ 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"] = "4433796b79d99a7735553e316522acee"
- @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
+ @request.env["action_dispatch.encrypted_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
- legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate('bar')
+ # Cookie generated with 64 bytes secret
+ message = ["566d4e75536d686e633246564e6b493062557079626c566d51574d30515430394c53315665564a694e4563786555744f57537454576b396a5a31566a626e52525054303d2d2d34663234333330623130623261306163363562316266323335396164666364613564643134623131"].pack("H*")
+ @request.headers["Cookie"] = "foo=#{message}"
- @request.headers["Cookie"] = "foo=#{legacy_value}"
get :get_encrypted_cookie
- 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"])
- 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)
-
- @request.headers["Cookie"] = "user_id=#{legacy_value}"
- get :get_signed_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal "bar", cookies[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
- assert_equal 45, @controller.send(:cookies).signed[:user_id]
+ 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)
- 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: JSON)
- assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
end
- 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.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"
+ def test_encrypted_cookie_rotating_secret
+ secret = "b3c631c314c0bbca50c1b2843150fe33"
- legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate('bar')
+ @request.env["action_dispatch.encrypted_cookie_cipher"] = "aes-256-gcm"
+ @request.env["action_dispatch.cookies_rotations"].rotate :encrypted, secret
- @request.headers["Cookie"] = "foo=#{legacy_value}"
- get :get_encrypted_cookie
+ key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")
- assert_equal 'bar', @controller.send(:cookies).encrypted[:foo]
+ old_message = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal).encrypt_and_sign(45)
- 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"])
- end
+ @request.headers["Cookie"] = "foo=#{::Rack::Utils.escape old_message}"
- 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"]
- 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 45, @controller.send(:cookies).encrypted[:foo]
- assert_equal nil, @controller.send(:cookies).encrypted[:foo]
- assert_equal nil, @response.cookies["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
@@ -998,7 +949,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"
@@ -1032,7 +983,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"
@@ -1061,7 +1012,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"
@@ -1069,7 +1020,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"
@@ -1078,20 +1029,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
@@ -1102,11 +1053,11 @@ class CookiesTest < ActionController::TestCase
assert_equal "david", cookies[:user_name]
get :noop
- assert !@response.headers.include?("Set-Cookie")
+ assert_not_includes @response.headers, "Set-Cookie"
assert_equal "david", cookies[:user_name]
get :noop
- assert !@response.headers.include?("Set-Cookie")
+ assert_not_includes @response.headers, "Set-Cookie"
assert_equal "david", cookies[:user_name]
end
@@ -1123,57 +1074,213 @@ 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]
+ end
+ def test_cookies_are_not_cleared
+ cookies.encrypted["foo"] = "bar"
get :noop
- assert_equal 'david', cookies['user_name']
- assert_equal 'david', cookies[:user_name]
+ assert_equal "bar", @controller.encrypted_cookie
+ end
+
+ def test_cookie_override
+ get :set_cookie_if_not_present
+ assert_equal "alice", cookies["user_name"]
+ cookies["user_name"] = "bob"
+ get :set_cookie_if_not_present
+ assert_equal "bob", cookies["user_name"]
+ 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_nil cookies.signed[:user_id]
+ 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_nil cookies.signed[:user_id]
+ 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 159bf10545..2812b1b614 100644
--- a/actionpack/test/dispatch/debug_exceptions_test.rb
+++ b/actionpack/test/dispatch/debug_exceptions_test.rb
@@ -1,11 +1,14 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class DebugExceptionsTest < ActionDispatch::IntegrationTest
+ InterceptedErrorInstance = StandardError.new
class Boomer
attr_accessor :closed
- def initialize(detailed = false)
+ def initialize(detailed = false)
@detailed = detailed
@closed = false
end
@@ -20,12 +23,24 @@ 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
+ raise "First error"
+ rescue
+ begin
+ raise "Second error"
+ rescue
+ raise "Third error"
+ end
end
def call(env)
- env['action_dispatch.show_detailed_exceptions'] = @detailed
+ env["action_dispatch.show_detailed_exceptions"] = @detailed
req = ActionDispatch::Request.new(env)
+ template = ActionView::Template.new(File.read(__FILE__), __FILE__, ActionView::Template::Handlers::Raw.new, format: :html, locals: [])
+
case req.path
when "/pass"
[404, { "X-Cascade" => "pass" }, self]
@@ -35,20 +50,30 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
raise RuntimeError
when "/method_not_allowed"
raise ActionController::MethodNotAllowed
+ when "/intercepted_error"
+ raise InterceptedErrorInstance
when "/unknown_http_method"
raise ActionController::UnknownHttpMethod
when "/not_implemented"
raise ActionController::NotImplemented
when "/unprocessable_entity"
raise ActionController::InvalidAuthenticityToken
+ when "/invalid_mimetype"
+ raise Mime::Type::InvalidMimeType
when "/not_found_original_exception"
begin
raise AbstractController::ActionNotFound.new
rescue
- raise ActionView::Template::Error.new('template')
+ raise ActionView::Template::Error.new(template)
+ end
+ when "/cause_mapped_to_rescue_responses"
+ begin
+ raise ActionController::ParameterMissing, :missing_param_key
+ rescue
+ raise NameError.new("uninitialized constant Userr")
end
when "/missing_template"
- raise ActionView::MissingTemplate.new(%w(foo), 'foo/index', %w(foo), false, 'mailer')
+ raise ActionView::MissingTemplate.new(%w(foo), "foo/index", %w(foo), false, "mailer")
when "/bad_request"
raise ActionController::BadRequest
when "/missing_keys"
@@ -56,115 +81,114 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
when "/parameter_missing"
raise ActionController::ParameterMissing, :missing_param_key
when "/original_syntax_error"
- eval 'broke_syntax =' # `eval` need for raise native SyntaxError at runtime
+ eval "broke_syntax =" # `eval` need for raise native SyntaxError at runtime
when "/syntax_error_into_view"
begin
- eval 'broke_syntax ='
+ eval "broke_syntax ="
rescue Exception
- template = ActionView::Template.new(File.read(__FILE__),
- __FILE__,
- ActionView::Template::Handlers::Raw.new,
- {})
raise ActionView::Template::Error.new(template)
end
when "/framework_raises"
method_that_raises
+ when "/nested_exceptions"
+ raise_nested_exceptions
else
raise "puke!"
end
end
end
- class BoomerAPI < Boomer
- def call(env)
- env['action_dispatch.show_detailed_exceptions'] = @detailed
- raise "puke!"
- end
- end
-
+ Interceptor = proc { |request, exception| request.set_header("int", exception) }
+ BadInterceptor = proc { |request, exception| raise "bad" }
RoutesApp = Struct.new(:routes).new(SharedTestRoutes)
ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false), RoutesApp)
DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp)
+ InterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [Interceptor])
+ BadInterceptedApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :default, [BadInterceptor])
- test 'skip diagnosis if not showing detailed exceptions' do
+ 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)
+
+ get "/invalid_mimetype", headers: { "Accept" => "text/html,*", "action_dispatch.show_exceptions" => true }
+ assert_response 406
+ assert_match(/Mime::Type::InvalidMimeType/, 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
@@ -173,12 +197,12 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
assert_equal "text/plain", response.content_type
assert_match(/RuntimeError\npuke/, body)
- Rails.stub :root, Pathname.new('.') do
+ 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
+ assert_match "Extracted source (around line #", body
+ assert_select "pre", { count: 0 }, body
end
get "/not_found", headers: xhr_request_env
@@ -212,89 +236,115 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
assert_match(/ActionController::ParameterMissing/, body)
end
- test "rescue with json error for API request" do
+ 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 }
+ 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 }
+ 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 }
+ 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 }
+ 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 }
+ 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 }
+ 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 json on API request returns only allowed formats or json as a fallback" do
+ test "rescue with HTML format for HTML API request" do
@app = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp, :api)
- get "/index.json", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/index.html", headers: { "action_dispatch.show_exceptions" => true }
assert_response 500
- assert_equal "application/json", response.content_type
- assert_match(/RuntimeError: puke/, body)
+ 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.html", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/index.xml", headers: { "action_dispatch.show_exceptions" => true }
assert_response 500
- assert_no_match(/<header>/, body)
- assert_no_match(/<body>/, body)
- assert_equal "application/json", response.content_type
+ assert_equal "application/xml", response.content_type
assert_match(/RuntimeError: puke/, body)
+ end
- get "/index.xml", headers: { 'action_dispatch.show_exceptions' => true }
+ test "rescue with JSON format as fallback if API request format is not supported" do
+ 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/xml", response.content_type
+ assert_equal "application/json", response.content_type
assert_match(/RuntimeError: puke/, body)
+
+ ensure
+ Mime::Type.unregister :wibble
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
- test "show registered original exception for wrapped exceptions" do
+ test "show registered original exception if the last exception is TemplateError" do
@app = DevelopmentApp
- get "/not_found_original_exception", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/not_found_original_exception", headers: { "action_dispatch.show_exceptions" => true }
assert_response 404
- assert_match(/AbstractController::ActionNotFound/, body)
+ assert_match %r{AbstractController::ActionNotFound}, body
+ assert_match %r{Showing <i>.*test/dispatch/debug_exceptions_test.rb</i>}, body
+ end
+
+ test "show the last exception and cause even when the cause is mapped to resque_responses" do
+ @app = DevelopmentApp
+
+ get "/cause_mapped_to_rescue_responses", headers: { "action_dispatch.show_exceptions" => true }
+ assert_response 500
+ assert_match %r{ActionController::ParameterMissing}, body
+ assert_match %r{NameError}, body
end
- test "named urls missing keys raise 500 level error" do
+ 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)
@@ -303,11 +353,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
@@ -318,74 +368,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
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 }
+ 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"
@@ -394,56 +484,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
+ assert_match %r{Showing <i>.*test/dispatch/debug_exceptions_test.rb</i>}, body
end
- test 'debug exceptions app shows user code that caused the error in source view' do
+ test "debug exceptions app shows user code that caused the error in source view" do
@app = DevelopmentApp
- Rails.stub :root, Pathname.new('.') do
+ 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 }
+ get "/framework_raises", headers: { "action_dispatch.backtrace_cleaner" => cleaner }
# Assert correct error
assert_response 500
- assert_select 'h2', /error in framework/
+ 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/
+ 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/
+ 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' do
- assert_select 'pre code a:first', %r{test/dispatch/debug_exceptions_test\.rb:\d+:in `call}
+ 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' do
- assert_select 'pre code a:first', /method_that_raises/
+ assert_select "#Framework-Trace-0" do
+ assert_select "code a:first", /method_that_raises/
+ end
+ end
+ end
+
+ test "invoke interceptors before rendering" do
+ @app = InterceptedApp
+ get "/intercepted_error", headers: { "action_dispatch.show_exceptions" => true }
+
+ assert_equal InterceptedErrorInstance, request.get_header("int")
+ end
+
+ test "bad interceptors doesn't debug exceptions" do
+ @app = BadInterceptedApp
+
+ get "/puke", headers: { "action_dispatch.show_exceptions" => true }
+
+ assert_response 500
+ assert_match(/puke/, body)
+ end
+
+ 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
+
+ get "/nested_exceptions", headers: { "action_dispatch.backtrace_cleaner" => cleaner }
+
+ # 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 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
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 dfbb91c0ca..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
@@ -18,46 +20,53 @@ module ActionDispatch
setup do
@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(nil, exception)
- assert_called_with(wrapper, :source_fragment, ['lib/file.rb', 42], returns: 'foo') do
- 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(nil, exc)
- 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
+ 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(nil, exc)
- assert_called_with(wrapper, :source_fragment, ['invalid', 0], returns: 'nothing') do
- 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(@cleaner, exception)
assert_equal [ "lib/file.rb:42:in `index'" ], wrapper.application_trace
end
- test '#application_trace cannot be nil' do
+ 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)
@@ -65,14 +74,14 @@ module ActionDispatch
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(@cleaner, exception)
assert_equal caller, wrapper.framework_trace
end
- test '#framework_trace cannot be nil' do
+ test "#framework_trace cannot be nil" do
nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new)
nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new)
@@ -80,14 +89,14 @@ module ActionDispatch
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(@cleaner, exception)
assert_equal exception.backtrace, wrapper.full_trace
end
- test '#full_trace cannot be nil' do
+ test "#full_trace cannot be nil" do
nil_backtrace_wrapper = ExceptionWrapper.new(@cleaner, BadlyDefinedError.new)
nil_cleaner_wrapper = ExceptionWrapper.new(nil, BadlyDefinedError.new)
@@ -95,16 +104,32 @@ module ActionDispatch
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(@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
index 28bb232ecd..5b8be39b6d 100644
--- a/actionpack/test/dispatch/executor_test.rb
+++ b/actionpack/test/dispatch/executor_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class ExecutorTest < ActiveSupport::TestCase
class MyBody < Array
@@ -79,7 +81,7 @@ class ExecutorTest < ActiveSupport::TestCase
running = false
body.close
- assert !running
+ assert_not running
end
def test_complete_callbacks_are_called_on_close
@@ -87,7 +89,7 @@ class ExecutorTest < ActiveSupport::TestCase
executor.to_complete { completed = true }
body = call_and_return_body
- assert !completed
+ assert_not completed
body.close
assert completed
@@ -114,13 +116,13 @@ class ExecutorTest < ActiveSupport::TestCase
call_and_return_body.close
assert result
- assert !defined?(@in_shared_context) # it's not in the test itself
+ assert_not defined?(@in_shared_context) # it's not in the test itself
end
private
def call_and_return_body(&block)
- app = middleware(block || proc { [200, {}, 'response'] })
- _, _, body = app.call({'rack.input' => StringIO.new('')})
+ app = middleware(block || proc { [200, {}, "response"] })
+ _, _, body = app.call("rack.input" => StringIO.new(""))
body
end
diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb
index 7f1ef121b7..bd2a5b35fb 100644
--- a/actionpack/test/dispatch/header_test.rb
+++ b/actionpack/test/dispatch/header_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "abstract_unit"
class HeaderTest < ActiveSupport::TestCase
@@ -18,14 +20,14 @@ class HeaderTest < ActiveSupport::TestCase
"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
@@ -44,20 +46,20 @@ class HeaderTest < ActiveSupport::TestCase
test "add to multivalued headers" do
# Sets header when not present
- @headers.add 'Foo', '1'
- assert_equal '1', @headers['Foo']
+ @headers.add "Foo", "1"
+ assert_equal "1", @headers["Foo"]
# Ignores nil values
- @headers.add 'Foo', nil
- assert_equal '1', @headers['Foo']
+ @headers.add "Foo", nil
+ assert_equal "1", @headers["Foo"]
# Converts value to string
- @headers.add 'Foo', 1
- assert_equal '1,1', @headers['Foo']
+ @headers.add "Foo", 1
+ assert_equal "1,1", @headers["Foo"]
# Case-insensitive
- @headers.add 'fOo', 2
- assert_equal '1,1,2', @headers['foO']
+ @headers.add "fOo", 2
+ assert_equal "1,1,2", @headers["foO"]
end
test "headers can contain numbers" do
@@ -76,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
@@ -105,28 +107,28 @@ 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
@@ -151,11 +153,17 @@ class HeaderTest < ActiveSupport::TestCase
end
test "headers directly modifies the passed environment" do
- env = {"HTTP_REFERER" => "/"}
+ env = { "HTTP_REFERER" => "/" }
headers = make_headers(env)
- headers['Referer'] = "http://example.com/"
- headers.merge! "CONTENT_TYPE" => "text/plain"
- assert_equal({"HTTP_REFERER"=>"http://example.com/",
- "CONTENT_TYPE"=>"text/plain"}, 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/host_authorization_test.rb b/actionpack/test/dispatch/host_authorization_test.rb
new file mode 100644
index 0000000000..5263dd2597
--- /dev/null
+++ b/actionpack/test/dispatch/host_authorization_test.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "ipaddr"
+
+class HostAuthorizationTest < ActionDispatch::IntegrationTest
+ App = -> env { [200, {}, %w(Success)] }
+
+ test "blocks requests to unallowed host" do
+ @app = ActionDispatch::HostAuthorization.new(App, %w(only.com))
+
+ get "/"
+
+ assert_response :forbidden
+ assert_match "Blocked host: www.example.com", response.body
+ end
+
+ test "allows all requests if hosts is empty" do
+ @app = ActionDispatch::HostAuthorization.new(App, nil)
+
+ get "/"
+
+ assert_response :ok
+ assert_equal "Success", body
+ end
+
+ test "hosts can be a single element array" do
+ @app = ActionDispatch::HostAuthorization.new(App, %w(www.example.com))
+
+ get "/"
+
+ assert_response :ok
+ assert_equal "Success", body
+ end
+
+ test "hosts can be a string" do
+ @app = ActionDispatch::HostAuthorization.new(App, "www.example.com")
+
+ get "/"
+
+ assert_response :ok
+ assert_equal "Success", body
+ end
+
+ test "passes requests to allowed hosts with domain name notation" do
+ @app = ActionDispatch::HostAuthorization.new(App, ".example.com")
+
+ get "/"
+
+ assert_response :ok
+ assert_equal "Success", body
+ end
+
+ test "does not allow domain name notation in the HOST header itself" do
+ @app = ActionDispatch::HostAuthorization.new(App, ".example.com")
+
+ get "/", env: {
+ "HOST" => ".example.com",
+ }
+
+ assert_response :forbidden
+ assert_match "Blocked host: .example.com", response.body
+ end
+
+ test "checks for requests with #=== to support wider range of host checks" do
+ @app = ActionDispatch::HostAuthorization.new(App, [-> input { input == "www.example.com" }])
+
+ get "/"
+
+ assert_response :ok
+ assert_equal "Success", body
+ end
+
+ test "mark the host when authorized" do
+ @app = ActionDispatch::HostAuthorization.new(App, ".example.com")
+
+ get "/"
+
+ assert_equal "www.example.com", request.get_header("action_dispatch.authorized_host")
+ end
+
+ test "sanitizes regular expressions to prevent accidental matches" do
+ @app = ActionDispatch::HostAuthorization.new(App, [/w.example.co/])
+
+ get "/"
+
+ assert_response :forbidden
+ assert_match "Blocked host: www.example.com", response.body
+ end
+
+ test "blocks requests to unallowed host supporting custom responses" do
+ @app = ActionDispatch::HostAuthorization.new(App, ["w.example.co"], -> env do
+ [401, {}, %w(Custom)]
+ end)
+
+ get "/"
+
+ assert_response :unauthorized
+ assert_equal "Custom", body
+ end
+
+ test "blocks requests with spoofed X-FORWARDED-HOST" do
+ @app = ActionDispatch::HostAuthorization.new(App, [IPAddr.new("127.0.0.1")])
+
+ get "/", env: {
+ "HTTP_X_FORWARDED_HOST" => "127.0.0.1",
+ "HOST" => "www.example.com",
+ }
+
+ assert_response :forbidden
+ assert_match "Blocked host: 127.0.0.1", response.body
+ end
+
+ test "does not consider IP addresses in X-FORWARDED-HOST spoofed when disabled" do
+ @app = ActionDispatch::HostAuthorization.new(App, nil)
+
+ get "/", env: {
+ "HTTP_X_FORWARDED_HOST" => "127.0.0.1",
+ "HOST" => "www.example.com",
+ }
+
+ assert_response :ok
+ assert_equal "Success", body
+ end
+
+ test "detects localhost domain spoofing" do
+ @app = ActionDispatch::HostAuthorization.new(App, "localhost")
+
+ get "/", env: {
+ "HTTP_X_FORWARDED_HOST" => "localhost",
+ "HOST" => "www.example.com",
+ }
+
+ assert_response :forbidden
+ assert_match "Blocked host: localhost", response.body
+ end
+
+ test "forwarded hosts should be permitted" do
+ @app = ActionDispatch::HostAuthorization.new(App, "domain.com")
+
+ get "/", env: {
+ "HTTP_X_FORWARDED_HOST" => "sub.domain.com",
+ "HOST" => "domain.com",
+ }
+
+ assert_response :forbidden
+ assert_match "Blocked host: sub.domain.com", response.body
+ end
+
+ test "forwarded hosts are allowed when permitted" do
+ @app = ActionDispatch::HostAuthorization.new(App, ".domain.com")
+
+ get "/", env: {
+ "HTTP_X_FORWARDED_HOST" => "sub.domain.com",
+ "HOST" => "domain.com",
+ }
+
+ assert_response :ok
+ assert_equal "Success", body
+ end
+end
diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb
index e4475f4233..f2459112b2 100644
--- a/actionpack/test/dispatch/live_response_test.rb
+++ b/actionpack/test/dispatch/live_response_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'concurrent/atomic/count_down_latch'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "concurrent/atomic/count_down_latch"
module ActionController
module Live
@@ -10,7 +12,7 @@ module ActionController
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
@@ -30,53 +32,59 @@ module ActionController
latch = Concurrent::CountDownLatch.new
t = Thread.new {
- @response.stream.write 'foo'
+ @response.stream.write "foo"
latch.wait
@response.stream.close
}
@response.await_commit
@response.each do |part|
- assert_equal 'foo', part
+ 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_by_default
+ @response.stream.write "omg"
+ assert_equal "no-cache", @response.headers["Cache-Control"]
end
- def test_cache_control_is_set
- @response.stream.write 'omg'
- assert_equal 'no-cache', @response.headers['Cache-Control']
+ def test_cache_control_is_set_manually
+ @response.set_header("Cache-Control", "public")
+ @response.stream.write "omg"
+ assert_equal "public", @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'
+ def test_headers_cannot_be_written_after_web_server_reads
+ @response.stream.write "omg"
latch = Concurrent::CountDownLatch.new
t = Thread.new {
- @response.stream.each do
+ @response.each do
latch.count_down
end
}
latch.wait
- assert @response.headers.frozen?
+ 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
@@ -87,9 +95,9 @@ module ActionController
@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 df27e41997..969a08efed 100644
--- a/actionpack/test/dispatch/mapper_test.rb
+++ b/actionpack/test/dispatch/mapper_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Routing
@@ -50,15 +52,15 @@ module ActionDispatch
fakeset = FakeSet.new
mapper = Mapper.new fakeset
assert_raises(ArgumentError) do
- mapper.match '/', :to => 'posts#index', :as => :main
+ 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"},
+ 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
@@ -67,9 +69,9 @@ module ActionDispatch
fakeset = FakeSet.new
mapper = Mapper.new fakeset
mapper.scope(format: true) do
- mapper.get '/foo', :to => 'posts#index', :as => :main
+ mapper.get "/foo", to: "posts#index", as: :main
end
- assert_equal({:controller=>"posts", :action=>"index"},
+ assert_equal({ controller: "posts", action: "index" },
fakeset.defaults.first)
assert_equal "/foo.:format", fakeset.asts.first.to_s
end
@@ -78,18 +80,18 @@ module ActionDispatch
fakeset = FakeSet.new
mapper = Mapper.new fakeset
mapper.scope(omg: :awesome) do
- mapper.get '/', :to => 'posts#index', :as => :main
+ mapper.get "/", to: "posts#index", as: :main
end
- assert_equal({:omg=>:awesome, :controller=>"posts", :action=>"index"},
+ assert_equal({ omg: :awesome, controller: "posts", action: "index" },
fakeset.defaults.first)
assert_equal("GET", fakeset.routes.first.verb)
end
def test_mapping_requirements
- options = { }
+ 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)
+ 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
@@ -97,16 +99,28 @@ module ActionDispatch
fakeset = FakeSet.new
mapper = Mapper.new fakeset
mapper.scope(via: :put) do
- mapper.match '/', :to => 'posts#index', :as => :main
+ 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.asts.first.to_s
+ mapper.get "/", to: "posts#index", as: :main
+ assert_equal "/", fakeset.asts.first.to_s
end
def test_map_more_slashes
@@ -114,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.asts.first.to_s
+ 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.asts.first.to_s
+ 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.asts.first.to_s
+ 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.asts.first.to_s
+ 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
@@ -146,16 +160,16 @@ 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.asts.first.to_s
+ 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.asts.first.to_s
+ mapper.get "/*path", to: "pages#show", format: true
+ assert_equal "/*path.:format", fakeset.asts.first.to_s
end
def test_raising_error_when_path_is_not_passed
@@ -178,6 +192,19 @@ module ActionDispatch
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_test.rb b/actionpack/test/dispatch/middleware_stack_test.rb
index a8c8e0784f..90f2eccd19 100644
--- a/actionpack/test/dispatch/middleware_stack_test.rb
+++ b/actionpack/test/dispatch/middleware_stack_test.rb
@@ -1,13 +1,26 @@
-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
+ class Base
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ @app.call(env)
+ end
+ end
+
+ class FooMiddleware < Base; end
+ class BarMiddleware < Base; end
+ class BazMiddleware < Base; end
+ class HiyaMiddleware < Base; end
+ class BlockMiddleware < Base
attr_reader :block
- def initialize(&block)
+ def initialize(app, &block)
+ super(app)
@block = block
end
end
@@ -18,14 +31,6 @@ class MiddlewareStackTest < ActiveSupport::TestCase
@stack.use BarMiddleware
end
- def test_delete_with_string_is_deprecated
- assert_deprecated do
- assert_difference "@stack.size", -1 do
- @stack.delete FooMiddleware.name
- end
- end
- end
-
def test_delete_works
assert_difference "@stack.size", -1 do
@stack.delete FooMiddleware
@@ -39,34 +44,16 @@ class MiddlewareStackTest < ActiveSupport::TestCase
assert_equal BazMiddleware, @stack.last.klass
end
- test "use should push middleware as a string onto the stack" do
- assert_deprecated do
- assert_difference "@stack.size" do
- @stack.use "MiddlewareStackTest::BazMiddleware"
- end
- assert_equal BazMiddleware, @stack.last.klass
- end
- end
-
- test "use should push middleware as a symbol onto the stack" do
- assert_deprecated do
- assert_difference "@stack.size" do
- @stack.use :"MiddlewareStackTest::BazMiddleware"
- end
- assert_equal BazMiddleware, @stack.last.klass
- end
- 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
@@ -102,15 +89,13 @@ 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
- assert_deprecated do
- @stack.unshift :"MiddlewareStackTest::BazMiddleware"
- assert_equal BazMiddleware, @stack.first.klass
- end
+ @stack.unshift MiddlewareStackTest::BazMiddleware
+ assert_equal BazMiddleware, @stack.first.klass
end
test "raise an error on invalid index" do
@@ -123,15 +108,6 @@ class MiddlewareStackTest < ActiveSupport::TestCase
end
end
- test "lazy evaluates middleware class" do
- assert_deprecated do
- assert_difference "@stack.size" do
- @stack.use "MiddlewareStackTest::BazMiddleware"
- end
- assert_equal BazMiddleware, @stack.last.klass
- end
- end
-
test "can check if Middleware are equal - Class" do
assert_equal @stack.last, BarMiddleware
end
@@ -144,6 +120,24 @@ class MiddlewareStackTest < ActiveSupport::TestCase
assert_equal @stack.last, @stack.last
end
+ test "instruments the execution of middlewares" do
+ app = @stack.build(proc { |env| [200, {}, []] })
+ env = {}
+
+ events = []
+
+ subscriber = proc do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ ActiveSupport::Notifications.subscribed(subscriber, "process_middleware.action_dispatch") do
+ app.call(env)
+ end
+
+ assert_equal 2, events.count
+ assert_equal ["MiddlewareStackTest::BarMiddleware", "MiddlewareStackTest::FooMiddleware"], events.map { |e| e.payload[:middleware] }
+ end
+
test "includes a middleware" do
assert_equal true, @stack.include?(ActionDispatch::MiddlewareStack::Middleware.new(BarMiddleware, nil, nil))
end
diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb
index 672b272590..50f6c06fee 100644
--- a/actionpack/test/dispatch/mime_type_test.rb
+++ b/actionpack/test/dispatch/mime_type_test.rb
@@ -1,9 +1,11 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+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
@@ -15,7 +17,7 @@ class MimeTypeTest < ActiveSupport::TestCase
begin
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("text/x-mobile")
assert_equal mime, Mime::Type.lookup_by_extension(:mobile)
Mime::Type.unregister(:mobile)
@@ -28,41 +30,41 @@ class MimeTypeTest < ActiveSupport::TestCase
test "parse text with trailing star at the beginning" do
accept = "text/*, text/html, application/json, multipart/form-data"
- expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]]
+ expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json], Mime[:multipart_form]]
parsed = Mime::Type.parse(accept)
- assert_equal expect, parsed
+ assert_equal expect.map(&:to_s), parsed.map(&:to_s)
end
test "parse text with trailing star in the end" do
accept = "text/html, application/json, multipart/form-data, text/*"
- expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml]]
+ expect = [Mime[:html], Mime[:json], Mime[:multipart_form], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml]]
parsed = Mime::Type.parse(accept)
- assert_equal expect, parsed
+ assert_equal expect.map(&:to_s), parsed.map(&:to_s)
end
test "parse text with trailing star" do
accept = "text/*"
- expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:xml], Mime[:yaml], Mime[:json]]
+ expect = [Mime[:html], Mime[:text], Mime[:js], Mime[:css], Mime[:ics], Mime[:csv], Mime[:vcf], Mime[:vtt], Mime[:xml], Mime[:yaml], Mime[:json]]
parsed = Mime::Type.parse(accept)
- assert_equal expect, 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], 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], '*/*']
+ 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], '*/*']
+ 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
@@ -81,7 +83,7 @@ class MimeTypeTest < ActiveSupport::TestCase
# 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], '*/*']
+ expect = [Mime[:html], Mime[:xml], "image/*", Mime[:text], "*/*"]
assert_equal expect.map(&:to_s), Mime::Type.parse(accept).map(&:to_s)
end
@@ -89,66 +91,56 @@ class MimeTypeTest < ActiveSupport::TestCase
# (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', '*/*']
+ 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
- type = Mime::Type.register("image/foo", :foo)
- assert_equal type, Mime[:foo]
- ensure
- Mime::Type.unregister(:foo)
- end
+ type = Mime::Type.register("image/foo", :foo)
+ assert_equal type, Mime[:foo]
+ ensure
+ Mime::Type.unregister(:foo)
end
test "custom type with type aliases" do
- 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
- end
- ensure
- Mime::Type.unregister(:foobar)
+ 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
end
+ ensure
+ Mime::Type.unregister(:foobar)
end
test "register callbacks" do
- begin
- registered_mimes = []
- Mime::Type.register_callback do |mime|
- registered_mimes << mime
- end
-
- mime = Mime::Type.register("text/foo", :foo)
- assert_equal [mime], registered_mimes
- ensure
- Mime::Type.unregister(:foo)
+ registered_mimes = []
+ Mime::Type.register_callback do |mime|
+ registered_mimes << mime
end
+
+ mime = Mime::Type.register("text/foo", :foo)
+ assert_equal [mime], registered_mimes
+ ensure
+ Mime::Type.unregister(:foo)
end
test "custom type with extension aliases" do
- begin
- Mime::Type.register "text/foobar", :foobar, [], [:foo, "bar"]
- %w[foobar foo bar].each do |extension|
- assert_equal Mime[:foobar], Mime::EXTENSION_LOOKUP[extension]
- end
- ensure
- Mime::Type.unregister(:foobar)
+ Mime::Type.register "text/foobar", :foobar, [], [:foo, "bar"]
+ %w[foobar foo bar].each do |extension|
+ assert_equal Mime[:foobar], Mime::EXTENSION_LOOKUP[extension]
end
+ ensure
+ Mime::Type.unregister(:foobar)
end
test "register alias" do
- begin
- Mime::Type.register_alias "application/xhtml+xml", :foobar
- assert_equal Mime[:html], Mime::EXTENSION_LOOKUP['foobar']
- ensure
- Mime::Type.unregister(:foobar)
- end
+ Mime::Type.register_alias "application/xhtml+xml", :foobar
+ assert_equal Mime[:html], Mime::EXTENSION_LOOKUP["foobar"]
+ ensure
+ Mime::Type.unregister(:foobar)
end
test "type should be equal to symbol" do
- assert_equal Mime[:html], 'application/xhtml+xml'
+ assert_equal Mime[:html], "application/xhtml+xml"
assert_equal Mime[:html], :html
end
@@ -157,7 +149,7 @@ class MimeTypeTest < ActiveSupport::TestCase
types.each do |type|
mime = Mime[type]
- assert mime.respond_to?("#{type}?"), "#{mime.inspect} does not respond to #{type}?"
+ assert_respond_to mime, "#{type}?"
assert_equal type, mime.symbol, "#{mime.inspect} is not #{type}?"
invalid_types = types - [type]
invalid_types.delete(:html)
@@ -167,18 +159,6 @@ class MimeTypeTest < ActiveSupport::TestCase
end
end
- test "deprecated lookup" do
- assert_deprecated do
- Mime::HTML
- end
- end
-
- test "deprecated const_defined?" do
- assert_deprecated do
- Mime.const_defined? :HTML
- end
- end
-
test "references gives preference to symbols before strings" do
assert_equal :html, Mime[:html].ref
another = Mime::Type.lookup("foo/bar")
@@ -190,8 +170,55 @@ class MimeTypeTest < ActiveSupport::TestCase
assert Mime[:js] =~ "text/javascript"
assert Mime[:js] =~ "application/javascript"
assert Mime[:js] !~ "text/html"
- assert !(Mime[:js] !~ "text/javascript")
- assert !(Mime[:js] !~ "application/javascript")
- assert Mime[:html] =~ 'application/xhtml+xml'
+ assert_not (Mime[:js] !~ "text/javascript")
+ assert_not (Mime[:js] !~ "application/javascript")
+ assert Mime[:html] =~ "application/xhtml+xml"
+ end
+
+ test "can be initialized with wildcards" do
+ assert_equal "*/*", Mime::Type.new("*/*").to_s
+ assert_equal "text/*", Mime::Type.new("text/*").to_s
+ assert_equal "video/*", Mime::Type.new("video/*").to_s
+ end
+
+ test "can be initialized with parameters" do
+ assert_equal "text/html; parameter", Mime::Type.new("text/html; parameter").to_s
+ assert_equal "text/html; parameter=abc", Mime::Type.new("text/html; parameter=abc").to_s
+ assert_equal 'text/html; parameter="abc"', Mime::Type.new('text/html; parameter="abc"').to_s
+ assert_equal 'text/html; parameter=abc; parameter2="xyz"', Mime::Type.new('text/html; parameter=abc; parameter2="xyz"').to_s
+ end
+
+ test "invalid mime types raise error" do
+ assert_raises Mime::Type::InvalidMimeType do
+ Mime::Type.new("too/many/slash")
+ end
+
+ assert_raises Mime::Type::InvalidMimeType do
+ Mime::Type.new("missingslash")
+ end
+
+ assert_raises Mime::Type::InvalidMimeType do
+ Mime::Type.new("improper/semicolon;")
+ end
+
+ assert_raises Mime::Type::InvalidMimeType do
+ Mime::Type.new('improper/semicolon; parameter=abc; parameter2="xyz";')
+ end
+
+ assert_raises Mime::Type::InvalidMimeType do
+ Mime::Type.new("text/html, text/plain")
+ end
+
+ assert_raises Mime::Type::InvalidMimeType do
+ Mime::Type.new("*/html")
+ end
+
+ assert_raises Mime::Type::InvalidMimeType do
+ Mime::Type.new("")
+ end
+
+ assert_raises Mime::Type::InvalidMimeType do
+ Mime::Type.new(nil)
+ end
end
end
diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb
index d027f09762..e42ea89f6f 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
@@ -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
@@ -78,6 +80,12 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
assert_equal "/shorthand -- /omg", response.body
end
+ def test_mounting_does_not_match_similar_paths
+ get "/shorthandomg"
+ assert_not_equal "/shorthand -- /omg", response.body
+ assert_equal " -- /shorthandomg", response.body
+ end
+
def test_mounting_works_with_via
get "/getfake"
assert_equal "OK", response.body
diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb
index d75e31db62..63c147cb1b 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
@@ -81,9 +81,9 @@ module TestGenerationPrefix
end
def url_to_application
- path = main_app.url_for(:controller => "outside_engine_generating",
- :action => "index",
- :only_path => true)
+ path = main_app.url_for(controller: "outside_engine_generating",
+ action: "index",
+ only_path: true)
render plain: path
end
@@ -151,116 +151,116 @@ module TestGenerationPrefix
include BlogEngine.routes.mounted_helpers
# Inside Engine
- test "[ENGINE] generating engine's url use SCRIPT_NAME from request" do
+ 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
+ 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
+ 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
+ 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
+ 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
@@ -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 fe8a4a3a17..edc4cd62a3 100644
--- a/actionpack/test/dispatch/reloader_test.rb
+++ b/actionpack/test/dispatch/reloader_test.rb
@@ -1,32 +1,13 @@
-require 'abstract_unit'
+# frozen_string_literal: true
-class ReloaderTest < ActiveSupport::TestCase
- Reloader = ActionDispatch::Reloader
+require "abstract_unit"
+class ReloaderTest < ActiveSupport::TestCase
teardown do
ActiveSupport::Reloader.reset_callbacks :prepare
ActiveSupport::Reloader.reset_callbacks :complete
end
- def test_prepare_callbacks
- a = b = c = nil
- assert_deprecated do
- Reloader.to_prepare { |*args| a = b = c = 1 }
- Reloader.to_prepare { |*args| b = c = 2 }
- Reloader.to_prepare { |*args| c = 3 }
- end
-
- # Ensure to_prepare callbacks are not run when defined
- assert_nil a || b || c
-
- # Run callbacks
- call_and_return_body
-
- assert_equal 1, a
- assert_equal 2, b
- assert_equal 3, c
- end
-
class MyBody < Array
def initialize(&block)
@on_close = block
@@ -45,6 +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
@@ -62,15 +60,12 @@ class ReloaderTest < ActiveSupport::TestCase
def test_condition_specifies_when_to_reload
i, j = 0, 0, 0, 0
- assert_deprecated do
- Reloader.to_prepare { |*args| i += 1 }
- Reloader.to_cleanup { |*args| j += 1 }
- end
- x = Class.new(ActiveSupport::Reloader)
- x.check = lambda { i < 3 }
+ reloader = reloader(lambda { i < 3 })
+ reloader.to_prepare { |*args| i += 1 }
+ reloader.to_complete { |*args| j += 1 }
- app = Reloader.new(lambda { |env| [200, {}, []] }, x)
+ app = middleware(lambda { |env| [200, {}, []] }, reloader)
5.times do
resp = app.call({})
resp[2].close
@@ -115,71 +110,31 @@ class ReloaderTest < ActiveSupport::TestCase
assert_respond_to body, :bar
end
- def test_cleanup_callbacks_are_called_when_body_is_closed
- cleaned = false
- assert_deprecated do
- Reloader.to_cleanup { cleaned = true }
- end
+ def test_complete_callbacks_are_called_when_body_is_closed
+ completed = false
+ reloader.to_complete { completed = true }
body = call_and_return_body
- assert !cleaned
+ assert_not completed
body.close
- assert cleaned
+ assert completed
end
def test_prepare_callbacks_arent_called_when_body_is_closed
prepared = false
- assert_deprecated do
- Reloader.to_prepare { prepared = true }
- end
+ reloader.to_prepare { prepared = true }
body = call_and_return_body
prepared = false
body.close
- assert !prepared
+ assert_not prepared
end
- def test_manual_reloading
- prepared = cleaned = false
- assert_deprecated do
- Reloader.to_prepare { prepared = true }
- Reloader.to_cleanup { cleaned = true }
- end
-
- assert_deprecated do
- Reloader.prepare!
- end
- assert prepared
- assert !cleaned
-
- prepared = cleaned = false
- assert_deprecated do
- Reloader.cleanup!
- end
- assert prepared
- assert cleaned
- end
-
- def test_prepend_prepare_callback
- i = 10
- assert_deprecated do
- Reloader.to_prepare { i += 1 }
- Reloader.to_prepare(:prepend => true) { i = 0 }
- end
-
- assert_deprecated do
- Reloader.prepare!
- end
- assert_equal 1, i
- end
-
- def test_cleanup_callbacks_are_called_on_exceptions
- cleaned = false
- assert_deprecated do
- Reloader.to_cleanup { cleaned = true }
- end
+ def test_complete_callbacks_are_called_on_exceptions
+ completed = false
+ reloader.to_complete { completed = true }
begin
call_and_return_body do
@@ -188,16 +143,25 @@ class ReloaderTest < ActiveSupport::TestCase
rescue
end
- assert cleaned
+ assert completed
end
private
def call_and_return_body(&block)
- x = Class.new(ActiveSupport::Reloader)
- x.check = lambda { true }
+ app = middleware(block || proc { [200, {}, "response"] })
+ _, _, body = app.call("rack.input" => StringIO.new(""))
+ body
+ end
+
+ def middleware(inner_app, reloader = reloader())
+ ActionDispatch::Reloader.new(inner_app, reloader)
+ end
- @response ||= 'response'
- @reloader ||= Reloader.new(block || proc {[200, {}, @response]}, x)
- @reloader.call({'rack.input' => StringIO.new('')})[2]
+ def reloader(check = lambda { true })
+ @reloader ||= begin
+ reloader = Class.new(ActiveSupport::Reloader)
+ reloader.check = check
+ reloader
+ end
end
end
diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb
index a07138b55e..2a48a12497 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,44 +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' }
+ "{\"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
@@ -63,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/
@@ -72,21 +74,21 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
test "occurring a parse error if parsing unsuccessful" do
with_test_routing do
- 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.cause.class
- assert_equal exception.cause.message, exception.message
- ensure
- $stderr = STDERR
+ $stderr = StringIO.new # suppress the log
+ json = "[\"person]\": {\"name\": \"David\"}}"
+ 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
- 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
@@ -104,7 +106,7 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- post ':action', :to => ::JsonParamsParsingTest::TestController
+ post ":action", to: ::JsonParamsParsingTest::TestController
end
end
yield
@@ -114,7 +116,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
@@ -133,51 +135,47 @@ 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.api+json)
- assert_parses(
- {"user" => {"username" => "meinac"}, "username" => "meinac"},
- "{\"username\": \"meinac\"}", { 'CONTENT_TYPE' => 'application/json' }
- )
- ensure
- Mime::Type.unregister :json
- Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
- end
+ 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
test "parses json params after custom json mime type registered with synonym" do
- begin
- Mime::Type.unregister :json
- Mime::Type.register "application/json", :json, %w(application/vnd.api+json)
- assert_parses(
- {"user" => {"username" => "meinac"}, "username" => "meinac"},
- "{\"username\": \"meinac\"}", { 'CONTENT_TYPE' => 'application/vnd.api+json' }
- )
- ensure
- Mime::Type.unregister :json
- Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
- end
+ 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
private
@@ -186,7 +184,7 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest
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
@@ -194,7 +192,7 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- post ':action', :to => controller
+ post ":action", to: controller
end
end
yield
diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
index bab4413b2a..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
@@ -21,136 +23,136 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
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')
+ params = parse_multipart("utf8_filename")
assert_equal %w(file foo), params.keys.sort
- assert_equal 'bar', params['foo']
+ assert_equal "bar", params["foo"]
- file = params['file']
- assert_equal 'ファイル%å.txt', file.original_filename
+ file = params["file"]
+ assert_equal "ファイル%å.txt", file.original_filename
assert_equal "text/plain", file.content_type
- assert_equal 'contents', file.read
+ 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
@@ -160,7 +162,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- get ':action', controller: 'multipart_params_parsing_test/test'
+ get ":action", controller: "multipart_params_parsing_test/test"
end
end
headers = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x" }
@@ -171,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 }
@@ -191,7 +193,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- post ':action', :controller => 'multipart_params_parsing_test/test'
+ post ":action", controller: "multipart_params_parsing_test/test"
end
end
yield
diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb
index f04022a544..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
@@ -145,7 +147,7 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- get ':action', :to => ::QueryStringParsingTest::TestController
+ get ":action", to: ::QueryStringParsingTest::TestController
end
end
@@ -159,7 +161,7 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- get ':action', :to => ::QueryStringParsingTest::TestController
+ get ":action", to: ::QueryStringParsingTest::TestController
end
end
@app = self.class.build_app(set) do |middleware|
diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb
index 7dcbcc5c21..74da2fe7d3 100644
--- a/actionpack/test/dispatch/request/session_test.rb
+++ b/actionpack/test/dispatch/request/session_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
class Request
@@ -17,18 +19,19 @@ module ActionDispatch
def test_to_hash
s = Session.create(store, req, {})
- s['foo'] = 'bar'
- assert_equal 'bar', s['foo']
- assert_equal({'foo' => 'bar'}, s.to_hash)
+ 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
s = Session.create(store, req, {})
- s['foo'] = 'bar'
+ s["foo"] = "bar"
s1 = Session.create(store, req, {})
assert_not_equal s, s1
- assert_equal 'bar', s1['foo']
+ assert_equal "bar", s1["foo"]
end
def test_find
@@ -40,7 +43,7 @@ module ActionDispatch
def test_destroy
s = Session.create(store, req, {})
- s['rails'] = 'ftw'
+ s["rails"] = "ftw"
s.destroy
@@ -49,22 +52,32 @@ module ActionDispatch
def test_keys
s = Session.create(store, req, {})
- s['rails'] = 'ftw'
- s['adequate'] = 'awesome'
+ 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, req, {})
- s['rails'] = 'ftw'
- s['adequate'] = 'awesome'
+ 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, req, {})
- s['rails'] = 'ftw'
- s['adequate'] = 'awesome'
+ s["rails"] = "ftw"
+ s["adequate"] = "awesome"
s.clear
assert_empty(s.values)
@@ -72,19 +85,19 @@ module ActionDispatch
def test_update
s = Session.create(store, req, {})
- s['rails'] = 'ftw'
+ 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, req, {})
- s['rails'] = 'ftw'
+ s["rails"] = "ftw"
- s.delete('rails')
+ s.delete("rails")
assert_empty(s.keys)
end
@@ -92,26 +105,72 @@ module ActionDispatch
def test_fetch
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 delete_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 b9f8c52378..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
@@ -141,7 +143,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- post ':action', to: ::UrlEncodedParamsParsingTest::TestController
+ post ":action", to: ::UrlEncodedParamsParsingTest::TestController
end
end
yield
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 a4cb8ce449..eb49396145 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class BaseRequestTest < ActiveSupport::TestCase
def setup
@@ -14,11 +16,11 @@ class BaseRequestTest < ActiveSupport::TestCase
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
@@ -34,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
}
@@ -178,139 +180,139 @@ 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' => "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
- request = stub_request 'HTTP_HOST' => 'www.rubyonrails.org'
+ request = stub_request "HTTP_HOST" => "www.rubyonrails.org"
assert_equal "rubyonrails.org", request.domain
- request = stub_request 'HTTP_HOST' => "www.rubyonrails.co.uk"
+ 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
+ request = stub_request "HTTP_HOST" => "www.rubyonrails.co.uk", :tld_length => 2
assert_equal "rubyonrails.co.uk", request.domain
end
test "subdomains" do
- request = stub_request 'HTTP_HOST' => "foobar.foobar.com"
+ 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"
+ 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"
+ 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"
+ 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
+ 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
@@ -321,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
+ 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 'HTTP_HOST' => 'www.example.org:8080'
- assert_equal ':8080', request.port_string
+ 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")
+ 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")
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
@@ -417,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
@@ -506,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
@@ -538,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
@@ -550,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
@@ -559,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.
@@ -593,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
@@ -650,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
@@ -658,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
@@ -699,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
@@ -709,28 +742,27 @@ 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
begin
ActiveSupport::Inflector.inflections do |inflect|
inflect.acronym "POS"
@@ -739,12 +771,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
@@ -752,108 +784,99 @@ end
class RequestFormat < BaseRequestTest
test "xml format" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: {format: :xml}) do
- assert_equal Mime[:xml], request.format
- end
+ request = stub_request "QUERY_STRING" => "format=xml"
+
+ assert_equal Mime[:xml], request.format
end
test "xhtml format" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: {format: :xhtml}) do
- assert_equal Mime[:html], request.format
- end
+ request = stub_request "QUERY_STRING" => "format=xhtml"
+
+ assert_equal Mime[:html], request.format
end
test "txt format" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: {format: :txt}) do
- assert_equal Mime[:text], request.format
- end
+ request = stub_request "QUERY_STRING" => "format=txt"
+
+ assert_equal Mime[:text], request.format
end
test "XMLHttpRequest" do
request = stub_request(
- 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest',
- 'HTTP_ACCEPT' => [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(",")
+ "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
+ "HTTP_ACCEPT" => [Mime[:js], Mime[:html], Mime[:xml], "text/xml", "*/*"].join(","),
+ "QUERY_STRING" => ""
)
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert request.xhr?
- assert_equal Mime[:js], request.format
- end
+ assert_predicate request, :xhr?
+ assert_equal Mime[:js], request.format
end
test "can override format with parameter negative" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: {format: :txt}) do
- assert !request.format.xml?
- end
+ request = stub_request("QUERY_STRING" => "format=txt")
+
+ assert_not_predicate request.format, :xml?
end
test "can override format with parameter positive" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: {format: :xml}) do
- assert request.format.xml?
- end
+ request = stub_request("QUERY_STRING" => "format=xml")
+
+ assert_predicate request.format, :xml?
end
test "formats text/html with accept header" do
- request = stub_request 'HTTP_ACCEPT' => 'text/html'
+ 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' => ''
+ 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"
+ 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")
+ 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
- assert_called(request, :parameters, times: 2, returns: {format: :txt}) do
- assert_equal [Mime[:text]], request.formats
- end
+ request = stub_request("QUERY_STRING" => "format=txt")
+
+ assert_equal [Mime[:text]], request.formats
end
test "formats format:unknown with accept header" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: {format: :unknown}) do
- assert_instance_of Mime::NullType, request.format
- end
+ request = stub_request("QUERY_STRING" => "format=unknown")
+
+ assert_instance_of Mime::NullType, request.format
end
test "format is not nil with unknown format" do
- request = stub_request
- assert_called(request, :parameters, times: 2, returns: {format: :hello}) do
- assert request.format.nil?
- assert_not request.format.html?
- assert_not request.format.xml?
- assert_not request.format.json?
- end
+ request = stub_request("QUERY_STRING" => "format=hello")
+
+ assert_nil request.format
+ assert_not_predicate request.format, :html?
+ assert_not_predicate request.format, :xml?
+ assert_not_predicate request.format, :json?
end
test "format does not throw exceptions when malformed parameters" do
request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2")
assert request.formats
- assert request.format.html?
+ assert_predicate request.format, :html?
end
test "formats with xhr request" do
- request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [Mime[:js]], request.formats
- end
+ request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
+ "QUERY_STRING" => ""
+
+ assert_equal [Mime[:js]], request.formats
end
test "ignore_accept_header" do
@@ -861,101 +884,97 @@ class RequestFormat < BaseRequestTest
ActionDispatch::Request.ignore_accept_header = true
begin
- request = stub_request 'HTTP_ACCEPT' => 'application/xml'
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime[:html] ], request.formats
- end
+ request = stub_request "HTTP_ACCEPT" => "application/xml",
+ "QUERY_STRING" => ""
- request = stub_request 'HTTP_ACCEPT' => 'koz-asked/something-crazy'
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime[:html] ], request.formats
- end
+ assert_equal [ Mime[:html] ], request.formats
- request = stub_request 'HTTP_ACCEPT' => '*/*;q=0.1'
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime[:html] ], request.formats
- end
+ request = stub_request "HTTP_ACCEPT" => "koz-asked/something-crazy",
+ "QUERY_STRING" => ""
- request = stub_request 'HTTP_ACCEPT' => 'application/jxw'
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime[:html] ], request.formats
- end
+ assert_equal [ Mime[:html] ], request.formats
- request = stub_request 'HTTP_ACCEPT' => 'application/xml',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
+ request = stub_request "HTTP_ACCEPT" => "*/*;q=0.1",
+ "QUERY_STRING" => ""
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime[:js] ], request.formats
- end
+ assert_equal [ Mime[:html] ], request.formats
- request = stub_request 'HTTP_ACCEPT' => 'application/xml',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- assert_called(request, :parameters, times: 2, returns: {format: :json}) do
- assert_equal [ Mime[:json] ], request.formats
- end
+ 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'
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [Mime[:xml]], request.formats
- end
+ request = stub_request "PATH_INFO" => "/foo.xml", "QUERY_STRING" => ""
- request = stub_request 'PATH_INFO' => '/foo.123'
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [Mime[:html]], request.formats
- end
+ assert_equal [Mime[:xml]], request.formats
+
+ request = stub_request "PATH_INFO" => "/foo.123", "QUERY_STRING" => ""
+
+ assert_equal [Mime[:html]], request.formats
end
test "formats from accept headers have higher precedence than path extension" do
- request = stub_request 'HTTP_ACCEPT' => 'application/json',
- 'PATH_INFO' => '/foo.xml'
+ request = stub_request "HTTP_ACCEPT" => "application/json",
+ "PATH_INFO" => "/foo.xml",
+ "QUERY_STRING" => ""
- assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [Mime[:json]], request.formats
- end
+ assert_equal [Mime[:json]], request.formats
end
end
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_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]])
@@ -964,15 +983,14 @@ end
class RequestParameters < BaseRequestTest
test "parameters" do
- request = stub_request
+ request = stub_request "CONTENT_TYPE" => "application/json",
+ "CONTENT_LENGTH" => 9,
+ "RAW_POST_DATA" => '{"foo":1}',
+ "QUERY_STRING" => "bar=2"
- assert_called(request, :request_parameters, times: 2, returns: {"foo" => 1}) do
- assert_called(request, :query_parameters, times: 2, returns: {"bar" => 2}) do
- assert_equal({"foo" => 1, "bar" => 2}, request.parameters)
- assert_equal({"foo" => 1}, request.request_parameters)
- assert_equal({"bar" => 2}, request.query_parameters)
- end
- end
+ assert_equal({ "foo" => 1, "bar" => "2" }, request.parameters)
+ assert_equal({ "foo" => 1 }, request.request_parameters)
+ assert_equal({ "bar" => "2" }, request.query_parameters)
end
test "parameters not accessible after rack parse error" do
@@ -987,17 +1005,14 @@ class RequestParameters < BaseRequestTest
end
test "path parameters with invalid UTF8 encoding" do
- request = stub_request(
- "action_dispatch.request.path_parameters" => { foo: "\xBE" }
- )
+ request = stub_request
err = assert_raises(ActionController::BadRequest) do
- request.check_path_parameters!
+ request.path_parameters = { foo: "\xBE" }
end
- assert_match "Invalid parameter encoding", err.message
- assert_match "foo", err.message
- assert_match "\\xBE", err.message
+ 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
@@ -1017,10 +1032,10 @@ class RequestParameters < BaseRequestTest
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
@@ -1042,44 +1057,21 @@ class RequestParameters < BaseRequestTest
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
@@ -1090,12 +1082,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)
@@ -1107,9 +1099,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
@@ -1119,9 +1111,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
@@ -1130,9 +1122,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
@@ -1141,9 +1133,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
@@ -1153,10 +1145,10 @@ end
class RequestEtag < BaseRequestTest
test "always matches *" do
- request = stub_request('HTTP_IF_NONE_MATCH' => '*')
+ request = stub_request("HTTP_IF_NONE_MATCH" => "*")
- assert_equal '*', request.if_none_match
- assert_equal ['*'], request.if_none_match_etags
+ assert_equal "*", request.if_none_match
+ assert_equal ["*"], request.if_none_match_etags
assert request.etag_matches?('"strong"')
assert request.etag_matches?('W/"weak"')
@@ -1166,7 +1158,7 @@ class RequestEtag < BaseRequestTest
test "doesn't match absent If-None-Match" do
request = stub_request
- assert_equal nil, request.if_none_match
+ assert_nil request.if_none_match
assert_equal [], request.if_none_match_etags
assert_not request.etag_matches?("foo")
@@ -1175,7 +1167,7 @@ class RequestEtag < BaseRequestTest
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
@@ -1186,8 +1178,8 @@ class RequestEtag < BaseRequestTest
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
@@ -1203,62 +1195,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 stub_request('CONTENT_TYPE' => 'application/x-www-form-urlencoded').form_data?
- assert stub_request('CONTENT_TYPE' => 'multipart/form-data').form_data?
+ 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
- test 'media_type is not from the FORM_DATA_MEDIA_TYPES array' do
- assert !stub_request('CONTENT_TYPE' => 'application/xml').form_data?
- assert !stub_request('CONTENT_TYPE' => 'multipart/related').form_data?
+class EarlyHintsRequestTest < BaseRequestTest
+ def setup
+ super
+ @env["rack.early_hints"] = lambda { |links| links }
+ @request = stub_request
end
- test 'no Content-Type header is provided and the request_method is POST' do
- request = stub_request('REQUEST_METHOD' => 'POST')
+ 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 '', request.media_type
- assert_equal 'POST', request.request_method
- assert !request.form_data?
+ 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 658e0d004b..7758b0406a 100644
--- a/actionpack/test/dispatch/response_test.rb
+++ b/actionpack/test/dispatch/response_test.rb
@@ -1,6 +1,8 @@
-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
@@ -13,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
@@ -37,11 +39,44 @@ 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.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_equal nil, @response.content_type
+ assert_nil @response.content_type
# after the action reads back @response.body,
assert_equal "Hello, World!", @response.body
@@ -66,15 +101,20 @@ class ResponseTest < ActiveSupport::TestCase
end
def test_response_charset_writer
- @response.charset = 'utf-16'
- assert_equal 'utf-16', @response.charset
+ @response.charset = "utf-16"
+ assert_equal "utf-16", @response.charset
@response.charset = nil
- assert_equal 'utf-8', @response.charset
+ 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
+ @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
@@ -92,14 +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']
+ _, headers, _ = response.to_a
+ assert_equal "text/html; charset=utf-16", headers["Content-Type"]
end
test "utf8 output" do
@@ -112,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|
@@ -131,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
@@ -168,110 +228,111 @@ class ResponseTest < ActiveSupport::TestCase
end
test "cookies" do
- @response.set_cookie("user_name", :value => "david", :path => "/")
+ @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
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))
+ @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.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")
- assert_equal({"user_name" => "david", "login" => nil}, @response.cookies)
+ assert_equal({ "user_name" => "david", "login" => nil }, @response.cookies)
end
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 resp.etag?
- assert resp.weak_etag?
- assert_not resp.strong_etag?
+ assert_predicate resp, :etag?
+ assert_predicate resp, :weak_etag?
+ assert_not_predicate resp, :strong_etag?
assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.etag)
- assert_equal({:public => true}, resp.cache_control)
+ assert_equal({ public: true }, resp.cache_control)
- assert_equal('public', resp.headers['Cache-Control'])
- assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.headers['ETag'])
+ 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'
+ response.strong_etag = "123"
+ response.body = "Hello"
}
resp.to_a
- assert resp.etag?
- assert_not resp.weak_etag?
- assert resp.strong_etag?
+ assert_predicate resp, :etag?
+ assert_not_predicate resp, :weak_etag?
+ assert_predicate resp, :strong_etag?
assert_equal('"202cb962ac59075b964b07152d234b70"', resp.etag)
end
test "read charset and content type" do
resp = ActionDispatch::Response.new.tap { |response|
- response.charset = 'utf-16'
+ response.charset = "utf-16"
response.content_type = Mime[:xml]
- response.body = 'Hello'
+ response.body = "Hello"
}
resp.to_a
- assert_equal('utf-16', resp.charset)
+ 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 with default charset utf-8" do
- original = ActionDispatch::Response.default_charset
- begin
- resp = ActionDispatch::Response.new(200, { "Content-Type" => "text/xml" })
- assert_equal('utf-8', resp.charset)
- ensure
- ActionDispatch::Response.default_charset = original
- end
+ 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.create.tap { |response|
- response.body = 'Hello'
+ 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
@@ -281,32 +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.create.tap { |response|
- response.body = 'Hello'
+ 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
@@ -319,74 +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']
+ _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'
+ @response.set_header "Foo", "1"
end
- test 'has_header?' do
- assert @response.has_header? 'Foo'
- assert_not @response.has_header? 'foo'
+ 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')
+ 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')
+ 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')
+ assert_nil @response.set_header("Foo", nil)
+ assert @response.has_header?("Foo")
+ assert_nil @response.get_header("Foo")
end
- test 'delete_header' do
+ test "delete_header" do
assert_nil @response.delete_header(nil)
- assert_nil @response.delete_header('foo')
- assert @response.has_header?('Foo')
+ assert_nil @response.delete_header("foo")
+ assert @response.has_header?("Foo")
- assert_equal '1', @response.delete_header('Foo')
- assert_not @response.has_header?('Foo')
+ assert_equal "1", @response.delete_header("Foo")
+ assert_not @response.has_header?("Foo")
end
- test 'add_header' do
+ 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')
+ 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')
+ assert_equal "1,2", @response.add_header("Foo", nil)
+ assert_equal "1,2", @response.get_header("Foo")
# 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')
+ 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')
+ assert_equal "1", @response.add_header("Bar", "1")
+ assert @response.has_header?("Bar")
+ assert_equal "1", @response.get_header("Bar")
end
end
@@ -395,87 +456,121 @@ 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('W/"202cb962ac59075b964b07152d234b70"', @response.headers['ETag'])
+ assert_equal("public", @response.headers["Cache-Control"])
+ assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.headers["ETag"])
assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.etag)
- assert_equal({:public => true}, @response.cache_control)
+ assert_equal({ public: true }, @response.cache_control)
end
test "response cache control from rackish app" do
@app = lambda { |env|
[200,
- {'ETag' => 'W/"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('W/"202cb962ac59075b964b07152d234b70"', @response.headers['ETag'])
+ assert_equal("public", @response.headers["Cache-Control"])
+ assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.headers["ETag"])
assert_equal('W/"202cb962ac59075b964b07152d234b70"', @response.etag)
- assert_equal({:public => true}, @response.cache_control)
+ 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.charset = "utf-16"
resp.content_type = Mime[:xml]
- resp.body = 'Hello'
+ resp.body = "Hello"
resp.request = ActionDispatch::Request.empty
}.to_a
}
- get '/'
+ get "/"
assert_response :success
- assert_equal('utf-16', @response.charset)
+ 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("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 "strong ETag validator" do
@app = lambda { |env|
ActionDispatch::Response.new.tap { |resp|
- resp.strong_etag = '123'
- resp.body = 'Hello'
+ resp.strong_etag = "123"
+ resp.body = "Hello"
resp.request = ActionDispatch::Request.empty
}.to_a
}
- get '/'
+ get "/"
assert_response :ok
- assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers['ETag'])
+ assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers["ETag"])
assert_equal('"202cb962ac59075b964b07152d234b70"', @response.etag)
end
+
+ test "response Content-Type with optional parameters" do
+ @app = lambda { |env|
+ [
+ 200,
+ { "Content-Type" => "text/csv; charset=utf-16; header=present" },
+ ["Hello"]
+ ]
+ }
+
+ get "/"
+ assert_response :success
+
+ assert_equal("text/csv; charset=utf-16; header=present", @response.headers["Content-Type"])
+ assert_equal("text/csv", @response.content_type)
+ assert_equal("utf-16", @response.charset)
+ end
+
+ test "response Content-Type with quoted-string" do
+ @app = lambda { |env|
+ [
+ 200,
+ { "Content-Type" => 'text/csv; header=present; charset="utf-16"' },
+ ["Hello"]
+ ]
+ }
+
+ get "/"
+ assert_response :success
+
+ assert_equal('text/csv; header=present; charset="utf-16"', @response.headers["Content-Type"])
+ assert_equal("text/csv", @response.content_type)
+ assert_equal("utf-16", @response.charset)
+ 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 9d0d23d6de..fe1f1995d8 100644
--- a/actionpack/test/dispatch/routing/inspector_test.rb
+++ b/actionpack/test/dispatch/routing/inspector_test.rb
@@ -1,6 +1,9 @@
-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)
@@ -13,16 +16,10 @@ end
module ActionDispatch
module Routing
class RoutesInspectorTest < ActiveSupport::TestCase
- def setup
+ setup do
@set = ActionDispatch::Routing::RouteSet.new
end
- def draw(options = nil, &block)
- @set.draw(&block)
- inspector = ActionDispatch::Routing::RoutesInspector.new(@set.routes)
- inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, options).split("\n")
- end
-
def test_displaying_routes_for_engines
engine = Class.new(Rails::Engine) do
def self.inspect
@@ -30,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
@@ -71,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 [
@@ -82,7 +79,7 @@ module ActionDispatch
def test_articles_inspect_with_multiple_verbs
output = draw do
- match 'articles/:id', to: 'articles#update', via: [:put, :patch]
+ match "articles/:id", to: "articles#update", via: [:put, :patch]
end
assert_equal [
@@ -93,7 +90,7 @@ module ActionDispatch
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 [
@@ -122,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 [
@@ -134,7 +131,7 @@ module ActionDispatch
def test_inspect_routes_shows_dynamic_action_route
output = draw do
ActiveSupport::Deprecation.silence do
- get 'api/:action' => 'api'
+ get "api/:action" => "api"
end
end
@@ -147,7 +144,7 @@ module ActionDispatch
def test_inspect_routes_shows_controller_and_action_only_route
output = draw do
ActiveSupport::Deprecation.silence do
- get ':controller/:action'
+ get ":controller/:action"
end
end
@@ -160,7 +157,7 @@ module ActionDispatch
def test_inspect_routes_shows_controller_and_action_route_with_constraints
output = draw do
ActiveSupport::Deprecation.silence do
- get ':controller(/:action(/:id))', :id => /\d+/
+ get ":controller(/:action(/:id))", id: /\d+/
end
end
@@ -172,18 +169,18 @@ module ActionDispatch
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_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 [
@@ -194,13 +191,13 @@ module ActionDispatch
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
@@ -217,7 +214,7 @@ module ActionDispatch
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 [
@@ -228,7 +225,7 @@ module ActionDispatch
def test_rails_routes_shows_named_route_with_mounted_rack_app
output = draw do
- mount MountedRackApp => '/foo'
+ mount MountedRackApp => "/foo"
end
assert_equal [
@@ -239,7 +236,7 @@ module ActionDispatch
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 [
@@ -256,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
@@ -269,7 +266,7 @@ module ActionDispatch
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)
@@ -277,8 +274,8 @@ module ActionDispatch
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 [
@@ -289,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 [
@@ -303,7 +300,7 @@ module ActionDispatch
end
def test_routes_can_be_filtered
- output = draw('posts') do
+ output = draw(grep: "posts") do
resources :articles
resources :posts
end
@@ -319,8 +316,76 @@ module ActionDispatch
" DELETE /posts/:id(.:format) posts#destroy"], output
end
+ def test_routes_when_expanded
+ previous_console_winsize = IO.console.winsize
+ IO.console.winsize = [0, 23]
+
+ engine = Class.new(Rails::Engine) do
+ def self.inspect
+ "Blog::Engine"
+ end
+ end
+ engine.routes.draw do
+ get "/cart", to: "cart#show"
+ end
+
+ output = draw(formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) do
+ get "/custom/assets", to: "custom_assets#show"
+ get "/custom/furnitures", to: "custom_furnitures#show"
+ mount engine => "/blog", :as => "blog"
+ end
+
+ assert_equal ["--[ Route 1 ]----------",
+ "Prefix | custom_assets",
+ "Verb | GET",
+ "URI | /custom/assets(.:format)",
+ "Controller#Action | custom_assets#show",
+ "--[ Route 2 ]----------",
+ "Prefix | custom_furnitures",
+ "Verb | GET",
+ "URI | /custom/furnitures(.:format)",
+ "Controller#Action | custom_furnitures#show",
+ "--[ Route 3 ]----------",
+ "Prefix | blog",
+ "Verb | ",
+ "URI | /blog",
+ "Controller#Action | Blog::Engine",
+ "",
+ "[ Routes for Blog::Engine ]",
+ "--[ Route 1 ]----------",
+ "Prefix | cart",
+ "Verb | GET",
+ "URI | /cart(.:format)",
+ "Controller#Action | cart#show"], output
+ ensure
+ IO.console.winsize = previous_console_winsize
+ end
+
+ def test_no_routes_matched_filter_when_expanded
+ output = draw(grep: "rails/dummy", formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) do
+ get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/
+ end
+
+ assert_equal [
+ "No routes were found for this grep pattern.",
+ "For more information about routes, see the Rails guide: 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('admin/posts') do
+ output = draw(grep: "admin/posts") do
resources :articles
namespace :admin do
resources :posts
@@ -338,23 +403,22 @@ module ActionDispatch
" DELETE /admin/posts/:id(.:format) admin/posts#destroy"], output
end
-
def test_regression_route_with_controller_regexp
output = draw do
ActiveSupport::Deprecation.silence do
- get ':controller(/:action)', controller: /api\/[^\/]+/, format: false
+ 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
output = draw do
- get '/cart', to: 'cart#show'
+ get "/cart", to: "cart#show"
end
assert_equal [
@@ -364,36 +428,36 @@ module ActionDispatch
end
def test_routes_with_undefined_filter
- output = draw(controller: 'Rails::MissingController') do
- get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/
+ 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: http://guides.rubyonrails.org/routing.html."
+ "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('rails/dummy') do
- get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/
+ output = draw(grep: "rails/dummy") do
+ get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/
end
assert_equal [
- "No routes were found for this controller",
- "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html."
+ "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('Rails::DummyController') {}
+ 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: http://guides.rubyonrails.org/routing.html."
+ "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
], output
end
@@ -404,13 +468,13 @@ module ActionDispatch
end
end
engine.routes.draw do
- get '/cart', to: 'cart#show'
- post '/cart', to: 'cart#create'
- patch '/cart', to: 'cart#update'
+ 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'
+ get "/custom/assets", to: "custom_assets#show"
mount engine => "/blog", as: "blog", internal: true
end
@@ -420,6 +484,12 @@ module ActionDispatch
], 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
index f1b2e8cfc7..31559bffc7 100644
--- a/actionpack/test/dispatch/routing/ipv6_redirect_test.rb
+++ b/actionpack/test/dispatch/routing/ipv6_redirect_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class IPv6IntegrationTest < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new
@@ -7,17 +9,17 @@ class IPv6IntegrationTest < ActionDispatch::IntegrationTest
class ::BadRouteRequestController < ActionController::Base
include Routes.url_helpers
def index
- render :text => foo_path
+ render plain: foo_path
end
def foo
- redirect_to :action => :index
+ 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
+ get "/", to: "bad_route_request#index", as: :index
+ get "/foo", to: "bad_route_request#foo", as: :foo
end
def _routes
@@ -32,14 +34,13 @@ class IPv6IntegrationTest < ActionDispatch::IntegrationTest
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 }
+ "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
+ get "/foo", env: request_env
assert_response :redirect
- assert_equal 'http://[fd07:2fa:6cff:2112:225:90ff:fec7:22aa]:3000/', redirect_to_url
+ assert_equal "http://[fd07:2fa:6cff:2112:225:90ff:fec7:22aa]:3000/", redirect_to_url
end
-
end
diff --git a/actionpack/test/dispatch/routing/non_dispatch_routed_app_test.rb b/actionpack/test/dispatch/routing/non_dispatch_routed_app_test.rb
new file mode 100644
index 0000000000..676a8c38d4
--- /dev/null
+++ b/actionpack/test/dispatch/routing/non_dispatch_routed_app_test.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module ActionDispatch
+ module Routing
+ class NonDispatchRoutedAppTest < ActionDispatch::IntegrationTest
+ # For example, Grape::API
+ class SimpleApp
+ def self.call(env)
+ [ 200, { "Content-Type" => "text/plain" }, [] ]
+ end
+
+ def self.routes
+ []
+ end
+ end
+
+ setup { @app = SimpleApp }
+
+ test "does not except" do
+ get "/foo"
+ assert_response :success
+ end
+ end
+ end
+end
diff --git a/actionpack/test/dispatch/routing/route_set_test.rb b/actionpack/test/dispatch/routing/route_set_test.rb
index 9327fe12c6..e6a2c35798 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,109 +23,109 @@ module ActionDispatch
assert empty?
draw do
- get 'foo', to: SimpleApp.new('foo#index')
+ get "foo", to: SimpleApp.new("foo#index")
end
assert_not empty?
end
- test "url helpers are added when route is added" do
+ 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
+ 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
+ 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 dc9943bb13..de0c9f3460 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -1,10 +1,13 @@
-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
@@ -28,96 +31,111 @@ 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
ActiveSupport::Deprecation.silence do
- get '/:controller(/:action(/:id(.:format)))'
+ get "/:controller(/:action(/:id(.:format)))"
end
end
end
@@ -128,12 +146,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
draw do
namespace :admin do
ActiveSupport::Deprecation.silence do
- get 'hello/:controllers/:action'
+ 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
@@ -144,30 +162,30 @@ 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
@@ -184,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
@@ -215,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
@@ -229,241 +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
- tc = self
draw do
- scope "pagemark", :controller => "pagemarks", :as => :pagemark do
- tc.assert_deprecated do
- get "new", :path => "build"
- end
- 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/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'
+ 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
ActiveSupport::Deprecation.silence do
- get "/local/:action", :controller => "local"
+ 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
@@ -473,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
@@ -487,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)
@@ -517,45 +537,45 @@ 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
@@ -569,166 +589,166 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
@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
@@ -749,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
@@ -931,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
@@ -1087,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
@@ -1109,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
@@ -1127,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
@@ -1285,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
+ 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 "/", root_path
+ get "/"
+ assert_equal "projects#index", @response.body
end
def test_scope_with_format_option
@@ -1377,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
@@ -1415,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
@@ -1481,130 +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
ActiveSupport::Deprecation.silence do
- get ':controller/:action/admin'
+ 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
ActiveSupport::Deprecation.silence do
- get ':action/callback', :action => /twitter|github/, :controller => "callbacks", :as => :callback
+ 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
@@ -1612,240 +1648,276 @@ 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
@@ -1855,31 +1927,31 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
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
ActiveSupport::Deprecation.silence do
- get "whatever/:controller(/:action(/:id))", :id => /\d+/
+ 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
@@ -1890,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
@@ -1913,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
@@ -1979,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
@@ -1991,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
@@ -2099,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_false_inside_nested_shallow_resource
@@ -2168,13 +2240,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
@@ -2186,22 +2258,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
@@ -2210,120 +2282,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
@@ -2331,11 +2403,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
@@ -2350,79 +2422,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
@@ -2430,17 +2502,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
@@ -2449,8 +2521,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
@@ -2458,15 +2530,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
@@ -2474,15 +2546,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
@@ -2490,66 +2562,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
@@ -2557,42 +2629,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
@@ -2602,34 +2674,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
@@ -2639,54 +2711,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
@@ -2694,38 +2766,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
@@ -2733,62 +2805,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
@@ -2797,22 +2869,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
@@ -2821,22 +2893,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
@@ -2845,22 +2917,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
@@ -2869,13 +2941,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
@@ -2885,30 +2957,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
@@ -2918,169 +2990,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
@@ -3090,43 +3162,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
@@ -3143,67 +3215,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
- 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
@@ -3212,15 +3284,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
@@ -3228,124 +3300,134 @@ 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_colon_containing_custom_param
+ ex = assert_raises(ArgumentError) {
+ draw do
+ resources :profiles, param: "username/:is_admin"
+ end
+ }
+
+ assert_match(/:param option can't contain colon/, ex.message)
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
@@ -3353,63 +3435,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
@@ -3423,228 +3505,228 @@ 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'
+ root to: "projects#index"
end
- params = ActionController::Parameters.new(id: '1')
+ params = ActionController::Parameters.new(id: "1")
- assert_raises ArgumentError do
+ 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'
+ root to: "projects#index"
end
- params = ActionController::Parameters.new(id: '1')
+ params = ActionController::Parameters.new(id: "1")
params.permit!
- assert_equal '/?id=1', root_path(params)
+ assert_equal "/?id=1", root_path(params)
end
def test_dynamic_controller_segments_are_deprecated
assert_deprecated do
draw do
- get '/:controller', action: 'index'
+ get "/:controller", action: "index"
end
end
end
@@ -3652,16 +3734,86 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_dynamic_action_segments_are_deprecated
assert_deprecated do
draw do
- get '/pages/:action', controller: 'pages'
+ get "/pages/:action", controller: "pages"
end
end
end
+ def test_multiple_roots_raises_error
+ ex = assert_raises(ArgumentError) {
+ draw do
+ root "pages#index", constraints: { host: "www.example.com" }
+ root "admin/pages#index", constraints: { host: "admin.example.com" }
+ end
+ }
+ assert_match(/Invalid route name, already in use: 'root'/, ex.message)
+ end
+
+ def test_multiple_named_roots
+ draw do
+ namespace :foo do
+ root "pages#index", constraints: { host: "www.example.com" }
+ root "admin/pages#index", constraints: { host: "admin.example.com" }, as: :admin_root
+ end
+
+ root "pages#index", constraints: { host: "www.example.com" }
+ root "admin/pages#index", constraints: { host: "admin.example.com" }, as: :admin_root
+ 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
@@ -3687,9 +3839,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
@@ -3726,13 +3878,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
@@ -3742,7 +3894,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
@@ -3770,7 +3922,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
@@ -3778,28 +3930,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
@@ -3822,7 +3974,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)
@@ -3831,7 +3983,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)
@@ -3840,7 +3992,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)
@@ -3849,20 +4001,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
@@ -3870,7 +4022,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
@@ -3881,7 +4033,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
@@ -3891,7 +4043,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
@@ -3909,7 +4061,7 @@ class TestDefaultScope < ActionDispatch::IntegrationTest
end
DefaultScopeRoutes = ActionDispatch::Routing::RouteSet.new
- DefaultScopeRoutes.default_scope = {:module => :blog}
+ DefaultScopeRoutes.default_scope = { module: :blog }
DefaultScopeRoutes.draw do
resources :posts
end
@@ -3923,7 +4075,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
@@ -3939,7 +4091,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
@@ -3951,14 +4103,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
@@ -3967,14 +4119,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
@@ -3983,22 +4135,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
@@ -4006,7 +4158,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
@@ -4015,23 +4167,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
@@ -4043,7 +4185,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
@@ -4056,7 +4209,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
@@ -4067,25 +4220,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
@@ -4114,9 +4266,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
@@ -4128,9 +4280,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
@@ -4140,7 +4292,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
@@ -4148,21 +4300,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
@@ -4178,17 +4330,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
+ ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
+ get "/foo" => ok, as: :foo
ActiveSupport::Deprecation.silence do
- get '/post(/:action(/:id))' => ok, as: :posts
+ 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
+ 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
@@ -4196,51 +4348,51 @@ 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
@@ -4259,9 +4411,9 @@ class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest
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
@@ -4271,7 +4423,7 @@ class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest
include Routes.url_helpers
- test "url helpers do not ignore nil parameters when using non-optimized routes" do
+ test "URL helpers do not ignore nil parameters when using non-optimized routes" do
Routes.stub :optimize_routes_generation?, false do
get "/categories/1"
assert_response :success
@@ -4283,21 +4435,21 @@ 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
@@ -4306,63 +4458,66 @@ 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 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|
- ActiveSupport::Deprecation.silence do
- set.draw do
- get "/bar/:id", :to => redirect("/foo/show/%{id}")
- get "/foo/show(/:id)", :to => "test_invalid_urls/foo#show"
+ set.draw do
+ get "/bar/:id", to: redirect("/foo/show/%{id}")
- ActiveSupport::Deprecation.silence do
- get "/foo(/:action(/:id))", :controller => "test_invalid_urls/foo"
- get "/:controller(/:action(/:id))"
- end
+ ok = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
+ get "/foobar/:id", to: ok
+
+ ActiveSupport::Deprecation.silence do
+ get "/:controller(/:action(/:id))"
end
end
@@ -4372,20 +4527,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
@@ -4397,27 +4563,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
@@ -4426,34 +4592,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
@@ -4461,12 +4630,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
@@ -4475,46 +4644,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
@@ -4523,8 +4692,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
@@ -4534,8 +4703,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
@@ -4548,14 +4717,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
@@ -4563,9 +4732,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
@@ -4578,11 +4747,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
@@ -4591,8 +4760,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
@@ -4605,11 +4774,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
@@ -4617,7 +4786,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
@@ -4626,29 +4795,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
+
+ 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}"
- # Non-optimized url helper
- error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil) }
+ 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') }
+ # Optimized URL helper
+ 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') }
+ # Non-optimized URL helper
+ error = assert_raises(ActionController::UrlGenerationError, message) { product_path(id: nil, "id" => "url-tested") }
assert_equal message, error.message
end
end
@@ -4662,9 +4834,9 @@ class TestDefaultUrlOptions < ActionDispatch::IntegrationTest
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
@@ -4677,7 +4849,7 @@ 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
@@ -4695,7 +4867,7 @@ class TestErrorsInController < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new
Routes.draw do
ActiveSupport::Deprecation.silence do
- get '/:controller(/:action)'
+ get "/:controller(/:action)"
end
end
@@ -4706,20 +4878,20 @@ class TestErrorsInController < ActionDispatch::IntegrationTest
end
def test_legit_no_method_errors_are_not_caught
- get '/posts/foo'
+ get "/posts/foo"
assert_equal 500, response.status
end
def test_legit_name_errors_are_not_caught
- get '/posts/bar'
+ get "/posts/bar"
assert_equal 500, response.status
end
def test_legit_routing_not_found_responses
- get '/posts/baz'
+ get "/posts/baz"
assert_equal 404, response.status
- get '/i_do_not_exist'
+ get "/i_do_not_exist"
assert_equal 404, response.status
end
end
@@ -4727,17 +4899,17 @@ end
class TestPartialDynamicPathSegments < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new
Routes.draw do
- ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+ 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 "/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
+ 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
@@ -4747,42 +4919,269 @@ class TestPartialDynamicPathSegments < ActionDispatch::IntegrationTest
end
def test_paths_with_partial_dynamic_segments_are_recognised
- get '/david-bowie/changes-song'
+ get "/david-bowie/changes-song"
assert_equal 200, response.status
- assert_params artist: 'david-bowie', song: 'changes'
+ assert_params artist: "david-bowie", song: "changes"
- get '/david-bowie/song-changes'
+ get "/david-bowie/song-changes"
assert_equal 200, response.status
- assert_params artist: 'david-bowie', song: 'changes'
+ assert_params artist: "david-bowie", song: "changes"
- get '/songs/song-changes'
+ get "/songs/song-changes"
assert_equal 200, response.status
- assert_params song: 'changes'
+ assert_params song: "changes"
- get '/songs/changes-song'
+ get "/songs/changes-song"
assert_equal 200, response.status
- assert_params song: 'changes'
+ assert_params song: "changes"
- get '/optional/songs/song-changes'
+ get "/optional/songs/song-changes"
assert_equal 200, response.status
- assert_params song: 'changes'
+ assert_params song: "changes"
- get '/optional/songs/changes-song'
+ get "/optional/songs/changes-song"
assert_equal 200, response.status
- assert_params song: 'changes'
+ assert_params song: "changes"
- get '/optional/david-bowie/changes-song'
+ get "/optional/david-bowie/changes-song"
assert_equal 200, response.status
- assert_params artist: 'david-bowie', song: 'changes'
+ assert_params artist: "david-bowie", song: "changes"
- get '/optional/david-bowie/song-changes'
+ get "/optional/david-bowie/song-changes"
assert_equal 200, response.status
- assert_params artist: 'david-bowie', song: 'changes'
+ assert_params artist: "david-bowie", song: "changes"
end
private
- def assert_params(params)
- assert_equal(params, request.path_parameters)
+ 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::CachingKeyGenerator.new(
+ ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33", iterations: 1000)
+ )
+ Rotations = ActiveSupport::Messages::RotationConfiguration.new
+ SIGNED_COOKIE_SALT = "signed cookie"
+ ENCRYPTED_SIGNED_COOKIE_SALT = "signed encrypted cookie"
+
+ class KeyGeneratorMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ env["action_dispatch.key_generator"] ||= Generator
+ env["action_dispatch.cookies_rotations"] ||= Rotations
+ env["action_dispatch.signed_cookie_salt"] = SIGNED_COOKIE_SALT
+ env["action_dispatch.encrypted_signed_cookie_salt"] = ENCRYPTED_SIGNED_COOKIE_SALT
+
+ @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 d38d1bbce6..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
@@ -37,7 +39,7 @@ module ActionDispatch
assert @env
session = Request::Session.find ActionDispatch::Request.new @env
- session['foo'] = 'bar'
+ session["foo"] = "bar"
as.call(@env)
session1 = Request::Session.find ActionDispatch::Request.new @env
@@ -47,10 +49,10 @@ module ActionDispatch
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 769de1a1e0..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
@@ -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
@@ -165,13 +167,13 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- get ':action', :to => ::CacheStoreTest::TestController
+ 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.use ActionDispatch::Session::CacheStore, key: "_session_id", cache: @cache
middleware.delete ActionDispatch::ShowExceptions
end
diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb
index 09cb1d925f..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
@@ -21,7 +28,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def set_session_value
session[:foo] = "bar"
- render plain: Rack::Utils.escape(Verifier.generate(session.to_hash))
+ render body: nil
end
def get_session_value
@@ -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,64 +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)
- cookie_body = nil
Time.stub :now, time do
expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
- cookies[SessionKey] = SignedBar
+ get "/set_session_value"
- get '/set_session_value'
assert_response :success
-
- cookie_body = response.body
- assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly",
- headers['Set-Cookie']
+ assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar"
end
# Second request does not access the session
- time = Time.local(2008, 4, 25)
+ time = time + 3.hours
Time.stub :now, time do
expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
- get '/no_session_access'
+ get "/no_session_access"
+
assert_response :success
+ assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar"
+ end
+ end
+ end
+
+ def test_session_store_with_expire_after_does_not_accept_expired_session
+ with_test_route_set(expire_after: 5.hours) do
+ # First request accesses the session
+ time = Time.local(2017, 11, 12)
+
+ Time.stub :now, time do
+ expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
+
+ get "/set_session_value"
+ get "/get_session_value"
- assert_equal "_myapp_session=#{cookie_body}; path=/; expires=#{expected_expiry}; HttpOnly",
- headers['Set-Cookie']
+ assert_response :success
+ assert_equal 'foo: "bar"', response.body
+ assert_session_cookie "path=/; expires=#{expected_expiry}; HttpOnly", "foo" => "bar"
+ end
+
+ # Second request is beyond the expiry time and the session is invalidated
+ time += 5.hours + 1.minute
+
+ Time.stub :now, time do
+ get "/get_session_value"
+
+ assert_response :success
+ assert_equal "foo: nil", response.body
end
end
end
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
@@ -338,7 +384,15 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def get(path, *args)
args[0] ||= {}
args[0][:headers] ||= {}
- args[0][:headers]["action_dispatch.key_generator"] ||= Generator
+ args[0][:headers].tap do |config|
+ config["action_dispatch.secret_key_base"] = SessionSecret
+ config["action_dispatch.authenticated_encrypted_cookie_salt"] = SessionSalt
+ config["action_dispatch.use_authenticated_cookie_encryption"] = true
+
+ config["action_dispatch.key_generator"] ||= Generator
+ config["action_dispatch.cookies_rotations"] ||= Rotations
+ end
+
super(path, *args)
end
@@ -346,11 +400,11 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- get ':action', :to => ::CookieStoreTest::TestController
+ 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
@@ -360,5 +414,4 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
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 18cb227dad..ac685a7dca 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
@@ -35,17 +37,18 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
end
begin
- require 'dalli'
- ss = Dalli::Client.new('localhost:11211').stats
- raise Dalli::DalliError unless ss['localhost:11211']
+ require "dalli"
+ servers = ENV["MEMCACHE_SERVERS"] || "localhost:11211"
+ ss = Dalli::Client.new(servers).stats
+ raise Dalli::DalliError unless ss[servers]
def test_setting_and_getting_session_value
with_test_route_set do
- 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 +58,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 +68,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 +89,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 +100,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 +123,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 +139,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 +154,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 +168,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
@@ -188,12 +191,14 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
with_routing do |set|
set.draw do
ActiveSupport::Deprecation.silence do
- get ':action', :to => ::MemCacheStoreTest::TestController
+ 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.use ActionDispatch::Session::MemCacheStore,
+ key: "_session_id", namespace: "mem_cache_store_test:#{SecureRandom.hex(10)}",
+ memcache_server: ENV["MEMCACHE_SERVERS"] || "localhost:11211"
middleware.delete ActionDispatch::ShowExceptions
end
diff --git a/actionpack/test/dispatch/session/test_session_test.rb b/actionpack/test/dispatch/session/test_session_test.rb
index 3e61d123e3..e90162a5fe 100644
--- a/actionpack/test/dispatch/session/test_session_test.rb
+++ b/actionpack/test/dispatch/session/test_session_test.rb
@@ -1,63 +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'))
+ 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))
+ 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'))
+ 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 })
+ 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 14894d4b82..6fafa4e426 100644
--- a/actionpack/test/dispatch/show_exceptions_test.rb
+++ b/actionpack/test/dispatch/show_exceptions_test.rb
@@ -1,28 +1,31 @@
-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 "/invalid_mimetype"
+ raise Mime::Type::InvalidMimeType
when "/bad_params", "/bad_params.json"
begin
raise StandardError.new
rescue
- raise ActionDispatch::ParamsParser::ParseError
+ 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"
begin
raise AbstractController::ActionNotFound.new
rescue
- raise ActionView::Template::Error.new('template')
+ raise ActionView::Template::Error.new("template")
end
else
raise "puke!"
@@ -35,32 +38,36 @@ 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 "/", env: { "action_dispatch.show_exceptions" => false }
end
end
test "rescue with error page" do
@app = ProductionApp
- get "/", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/", env: { "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", env: { "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", env: { "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", env: { "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", env: { "action_dispatch.show_exceptions" => true }
assert_response 405
assert_equal "", body
+
+ get "/invalid_mimetype", headers: { "Accept" => "text/html,*", "action_dispatch.show_exceptions" => true }
+ assert_response 406
+ assert_equal "", body
end
test "localize rescue error page" do
@@ -69,11 +76,11 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
begin
@app = ProductionApp
- get "/", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/", env: { "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", env: { "action_dispatch.show_exceptions" => true }
assert_response 404
assert_equal "404 error fixture\n", body
ensure
@@ -84,14 +91,14 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
test "sets the HTTP charset parameter" do
@app = ProductionApp
- get "/", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/", env: { "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", env: { "action_dispatch.show_exceptions" => true }
assert_response 404
assert_match(/404 error/, body)
end
@@ -105,7 +112,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
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", env: { "action_dispatch.show_exceptions" => true }
assert_response 404
assert_equal "YOU FAILED", body
end
@@ -116,7 +123,7 @@ 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", env: { "action_dispatch.show_exceptions" => true }
assert_response 405
assert_equal "", body
end
@@ -124,12 +131,12 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
test "bad params exception is returned in the correct format" do
@app = ProductionApp
- get "/bad_params", headers: { 'action_dispatch.show_exceptions' => true }
+ get "/bad_params", env: { "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 }
+ get "/bad_params.json", env: { "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)
diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb
index 668b2b6cfe..baf46e7c7e 100644
--- a/actionpack/test/dispatch/ssl_test.rb
+++ b/actionpack/test/dispatch/ssl_test.rb
@@ -1,7 +1,9 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class SSLTest < ActionDispatch::IntegrationTest
- HEADERS = Rack::Utils::HeaderHash.new 'Content-Type' => 'text/html'
+ HEADERS = Rack::Utils::HeaderHash.new "Content-Type" => "text/html"
attr_accessor :app
@@ -12,26 +14,16 @@ class SSLTest < ActionDispatch::IntegrationTest
end
class RedirectSSLTest < SSLTest
-
- def assert_not_redirected(url, headers: {}, redirect: {}, deprecated_host: nil,
- deprecated_port: nil)
-
- self.app = build_app ssl_options: { redirect: redirect,
- host: deprecated_host, port: deprecated_port
- }
-
+ def assert_not_redirected(url, headers: {}, redirect: {})
+ self.app = build_app ssl_options: { redirect: redirect }
get url, headers: headers
assert_response :ok
end
- def assert_redirected(redirect: {}, deprecated_host: nil, deprecated_port: nil,
- from: 'http://a/b?c=d', to: from.sub('http', 'https'))
-
+ 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,
- host: deprecated_host, port: deprecated_port
- }
+ self.app = build_app ssl_options: { redirect: redirect }
get from
assert_response redirect[:status] || 301
@@ -39,130 +31,128 @@ class RedirectSSLTest < SSLTest
assert_equal redirect[:body].join, @response.body
end
- test 'exclude can avoid redirect' do
- excluding = { exclude: -> request { request.path =~ /healthcheck/ } }
+ def assert_post_redirected(redirect: {}, from: "http://a/b?c=d",
+ to: from.sub("http", "https"))
+
+ self.app = build_app ssl_options: { redirect: redirect }
- assert_not_redirected 'http://example.org/healthcheck', redirect: excluding
- assert_redirected from: 'http://example.org/', redirect: excluding
+ post from
+ assert_response redirect[:status] || 307
+ assert_redirected_to to
end
- test 'https is not redirected' do
- assert_not_redirected 'https://example.org'
+ test "exclude can avoid redirect" do
+ excluding = { exclude: -> request { request.path =~ /healthcheck/ } }
+
+ assert_not_redirected "http://example.org/healthcheck", redirect: excluding
+ assert_redirected from: "http://example.org/", redirect: excluding
end
- test 'proxied https is not redirected' do
- assert_not_redirected 'http://example.org', headers: { 'HTTP_X_FORWARDED_PROTO' => 'https' }
+ test "https is not redirected" do
+ assert_not_redirected "https://example.org"
end
- test 'http is redirected to https' do
- assert_redirected
+ test "proxied https is not redirected" do
+ assert_not_redirected "http://example.org", headers: { "HTTP_X_FORWARDED_PROTO" => "https" }
end
- test 'redirect with non-301 status' do
- assert_redirected redirect: { status: 307 }
+ test "http is redirected to https" do
+ assert_redirected
end
- test 'redirect with custom body' do
- assert_redirected redirect: { body: ['foo'] }
+ test "http POST is redirected to https with status 307" do
+ assert_post_redirected
end
- test 'redirect to specific host' do
- assert_redirected redirect: { host: 'ssl' }, to: 'https://ssl/b?c=d'
+ test "redirect with non-301 status" do
+ assert_redirected redirect: { status: 307 }
end
- test 'redirect to default port' do
- assert_redirected redirect: { port: 443 }
+ test "redirect with custom body" do
+ assert_redirected redirect: { body: ["foo"] }
end
- test 'redirect to non-default port' do
- assert_redirected redirect: { port: 8443 }, to: 'https://a:8443/b?c=d'
+ test "redirect to specific host" do
+ assert_redirected redirect: { host: "ssl" }, to: "https://ssl/b?c=d"
end
- test 'redirect to different host and non-default port' do
- assert_redirected redirect: { host: 'ssl', port: 8443 }, to: 'https://ssl:8443/b?c=d'
+ test "redirect to default port" do
+ assert_redirected redirect: { port: 443 }
end
- test 'redirect to different host including port' do
- assert_redirected redirect: { host: 'ssl:443' }, to: 'https://ssl:443/b?c=d'
+ test "redirect to non-default port" do
+ assert_redirected redirect: { port: 8443 }, to: "https://a:8443/b?c=d"
end
- test ':host is deprecated, moved within redirect: { host: … }' do
- assert_deprecated do
- assert_redirected deprecated_host: 'foo', to: 'https://foo/b?c=d'
- end
+ 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
- test ':port is deprecated, moved within redirect: { port: … }' do
- assert_deprecated do
- assert_redirected deprecated_port: 1, to: 'https://a:1/b?c=d'
- end
+ test "redirect to different host including port" do
+ assert_redirected redirect: { host: "ssl:443" }, to: "https://ssl:443/b?c=d"
end
- test 'no redirect with redirect set to false' do
- assert_not_redirected 'http://example.org', redirect: false
+ test "no redirect with redirect set to false" do
+ assert_not_redirected "http://example.org", redirect: false
end
end
class StrictTransportSecurityTest < SSLTest
- EXPECTED = 'max-age=15552000'
- EXPECTED_WITH_SUBDOMAINS = 'max-age=15552000; includeSubDomains'
+ EXPECTED = "max-age=31536000"
+ EXPECTED_WITH_SUBDOMAINS = "max-age=31536000; includeSubDomains"
- def assert_hsts(expected, url: 'https://example.org', hsts: { subdomains: true }, headers: {})
+ def assert_hsts(expected, url: "https://example.org", hsts: { subdomains: true }, headers: {})
self.app = build_app ssl_options: { hsts: hsts }, headers: headers
get url
- assert_equal expected, response.headers['Strict-Transport-Security']
+ if expected.nil?
+ assert_nil response.headers["Strict-Transport-Security"]
+ else
+ assert_equal expected, response.headers["Strict-Transport-Security"]
+ end
end
- test 'enabled by default' do
+ test "enabled by default" do
assert_hsts EXPECTED_WITH_SUBDOMAINS
end
- test 'not sent with http:// responses' do
- assert_hsts nil, url: 'http://example.org'
+ test "not sent with http:// responses" do
+ assert_hsts nil, url: "http://example.org"
end
- test 'defers to app-provided header' do
- assert_hsts 'app-provided', headers: { 'Strict-Transport-Security' => 'app-provided' }
+ test "defers to app-provided header" do
+ assert_hsts "app-provided", headers: { "Strict-Transport-Security" => "app-provided" }
end
- test 'hsts: true enables default settings' do
- assert_hsts EXPECTED, hsts: true
+ test "hsts: true enables default settings" do
+ assert_hsts EXPECTED_WITH_SUBDOMAINS, hsts: true
end
- test 'hsts: false sets max-age to zero, clearing browser HSTS settings' do
- assert_hsts 'max-age=0', hsts: false
+ test "hsts: false sets max-age to zero, clearing browser HSTS settings" do
+ assert_hsts "max-age=0; includeSubDomains", hsts: false
end
- test ':expires sets max-age' do
- assert_deprecated do
- assert_hsts 'max-age=500', hsts: { expires: 500 }
- end
+ test ":expires sets max-age" do
+ assert_hsts "max-age=500; includeSubDomains", hsts: { expires: 500 }
end
- test ':expires supports AS::Duration arguments' do
- assert_deprecated do
- assert_hsts 'max-age=31557600', hsts: { expires: 1.year }
- end
+ test ":expires supports AS::Duration arguments" do
+ assert_hsts "max-age=31556952; includeSubDomains", hsts: { expires: 1.year }
end
- test 'include subdomains' do
+ test "include subdomains" do
assert_hsts "#{EXPECTED}; includeSubDomains", hsts: { subdomains: true }
end
- test 'exclude subdomains' do
+ test "exclude subdomains" do
assert_hsts EXPECTED, hsts: { subdomains: false }
end
- test 'opt in to browser preload lists' do
- assert_deprecated do
- assert_hsts "#{EXPECTED}; preload", hsts: { preload: true }
- end
+ test "opt in to browser preload lists" do
+ assert_hsts "#{EXPECTED_WITH_SUBDOMAINS}; preload", hsts: { preload: true }
end
- test 'opt out of browser preload lists' do
- assert_deprecated do
- assert_hsts EXPECTED, hsts: { preload: false }
- end
+ test "opt out of browser preload lists" do
+ assert_hsts EXPECTED_WITH_SUBDOMAINS, hsts: { preload: false }
end
end
@@ -171,60 +161,68 @@ class SecureCookiesTest < SSLTest
def get(**options)
self.app = build_app(**options)
- super 'https://example.org'
+ super "https://example.org"
end
def assert_cookies(*expected)
- assert_equal expected, response.headers['Set-Cookie'].split("\n")
+ assert_equal expected, response.headers["Set-Cookie"].split("\n")
end
def test_flag_cookies_as_secure
- get headers: { 'Set-Cookie' => DEFAULT }
- assert_cookies 'id=1; path=/; secure', 'token=abc; path=/; secure; HttpOnly'
+ get headers: { "Set-Cookie" => DEFAULT }
+ assert_cookies "id=1; path=/; secure", "token=abc; path=/; secure; HttpOnly"
end
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'
+ get headers: { "Set-Cookie" => "problem=def; path=/; HttpOnly; secure" }
+ assert_cookies "problem=def; path=/; HttpOnly; secure"
end
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'
+ get headers: { "Set-Cookie" => "problem=def; path=/; HttpOnly; secure" }
+ assert_cookies "problem=def; path=/; HttpOnly; secure"
end
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'
+ 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_before
- get headers: { 'Set-Cookie' => 'problem=def; path=/;secure; HttpOnly' }
- assert_cookies 'problem=def; path=/;secure; HttpOnly'
+ 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'
+ 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'
+ 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 }
+ 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']
+ assert_nil response.headers["Set-Cookie"]
end
def test_keeps_original_headers_behavior
- get headers: { 'Connection' => %w[close] }
- assert_equal 'close', response.headers['Connection']
+ 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 ea8b5e904e..d44aa00122 100644
--- a/actionpack/test/dispatch/static_test.rb
+++ b/actionpack/test/dispatch/static_test.rb
@@ -1,9 +1,11 @@
-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!"]]
+ [200, { "Content-Type" => "text/plain" }, ["Hello, World!"]]
}
def setup
@@ -29,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
@@ -37,23 +39,13 @@ 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_handles_urls_with_null_byte
assert_equal "Hello, World!", get("/doorkeeper%00").body
end
- def test_sets_cache_control
- app = assert_deprecated do
- ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60")
- end
- response = Rack::MockRequest.new(app).request("GET", "/index.html")
-
- assert_html "/index.html", response
- assert_equal "public, max-age=60", response.headers["Cache-Control"]
- end
-
def test_serves_static_index_at_root
assert_html "/index.html", get("/index.html")
assert_html "/index.html", get("/index")
@@ -79,9 +71,17 @@ module StaticTests
end
def test_served_static_file_with_non_english_filename
- assert_html "means hello in Japanese\n", get("/foo/#{Rack::Utils.escape("ã“ã‚“ã«ã¡ã¯.html")}")
+ assert_html "means hello in Japanese\n", get("/foo/%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF.html")
end
+ def test_served_gzipped_static_file_with_non_english_filename
+ response = get("/foo/%E3%81%95%E3%82%88%E3%81%86%E3%81%AA%E3%82%89.html", "HTTP_ACCEPT_ENCODING" => "gzip")
+
+ assert_gzip "/foo/ã•ã‚ˆã†ãªã‚‰.html", response
+ assert_equal "text/html", response.headers["Content-Type"]
+ assert_equal "Accept-Encoding", response.headers["Vary"]
+ assert_equal "gzip", response.headers["Content-Encoding"]
+ end
def test_serves_static_file_with_exclamation_mark_in_filename
with_static_file "/foo/foo!bar.html" do |file|
@@ -148,65 +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', "REQUEST_METHOD" => 'POST'}
+ 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',
+ "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 "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")
@@ -226,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
@@ -258,7 +267,7 @@ class StaticTest < ActiveSupport::TestCase
def setup
super
@root = "#{FIXTURE_LOAD_PATH}/public"
- @app = ActionDispatch::Static.new(DummyApp, @root, headers: {'Cache-Control' => "public, max-age=60"})
+ @app = ActionDispatch::Static.new(DummyApp, @root, headers: { "Cache-Control" => "public, max-age=60" })
end
def public_path
@@ -268,17 +277,17 @@ 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
@@ -294,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, headers: {'Cache-Control' => "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..0d08f17af3
--- /dev/null
+++ b/actionpack/test/dispatch/system_testing/driver_test.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_dispatch/system_testing/driver"
+require "selenium/webdriver"
+
+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_instance_of Selenium::WebDriver::Chrome::Options, driver.instance_variable_get(:@browser).options
+ assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size)
+ assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options)
+ end
+
+ 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_instance_of Selenium::WebDriver::Firefox::Options, driver.instance_variable_get(:@browser).options
+ assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size)
+ assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options)
+ end
+
+ 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
+
+ test "define extra capabilities using chrome" do
+ driver_option = nil
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :chrome) do |option|
+ option.add_argument("start-maximized")
+ option.add_emulation(device_name: "iphone 6")
+ option.add_preference(:detach, true)
+
+ driver_option = option
+ end
+ driver.use
+
+ expected = { args: ["start-maximized"], mobileEmulation: { deviceName: "iphone 6" }, prefs: { detach: true } }
+ assert_equal expected, driver_option.as_json
+ end
+
+ test "define extra capabilities using headless_chrome" do
+ driver_option = nil
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :headless_chrome) do |option|
+ option.add_argument("start-maximized")
+ option.add_emulation(device_name: "iphone 6")
+ option.add_preference(:detach, true)
+
+ driver_option = option
+ end
+ driver.use
+
+ expected = { args: ["start-maximized"], mobileEmulation: { deviceName: "iphone 6" }, prefs: { detach: true } }
+ assert_equal expected, driver_option.as_json
+ end
+
+ test "define extra capabilities using firefox" do
+ driver_option = nil
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :firefox) do |option|
+ option.add_preference("browser.startup.homepage", "http://www.seleniumhq.com/")
+ option.add_argument("--host=127.0.0.1")
+
+ driver_option = option
+ end
+ driver.use
+
+ expected = { "moz:firefoxOptions" => { args: ["--host=127.0.0.1"], prefs: { "browser.startup.homepage" => "http://www.seleniumhq.com/" } } }
+ assert_equal expected, driver_option.as_json
+ end
+
+ test "define extra capabilities using headless_firefox" do
+ driver_option = nil
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :headless_firefox) do |option|
+ option.add_preference("browser.startup.homepage", "http://www.seleniumhq.com/")
+ option.add_argument("--host=127.0.0.1")
+
+ driver_option = option
+ end
+ driver.use
+
+ expected = { "moz:firefoxOptions" => { args: ["--host=127.0.0.1"], prefs: { "browser.startup.homepage" => "http://www.seleniumhq.com/" } } }
+ assert_equal expected, driver_option.as_json
+ end
+
+ test "does not define extra capabilities" do
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :firefox)
+
+ assert_nothing_raised do
+ driver.use
+ end
+ end
+end
diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
new file mode 100644
index 0000000000..b756b91379
--- /dev/null
+++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_dispatch/system_testing/test_helpers/screenshot_helper"
+require "capybara/dsl"
+require "selenium/webdriver"
+
+class ScreenshotHelperTest < ActiveSupport::TestCase
+ test "image path is saved in tmp directory" do
+ 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
+ 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
+
+ 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..847b09dcfe
--- /dev/null
+++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "selenium/webdriver"
+
+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 51c469a61a..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,27 +32,27 @@ 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 there is no application config" do
@@ -60,32 +62,66 @@ class TestRequestTest < ActiveSupport::TestCase
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 a4f9d56a6a..2629a61057 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)
@@ -17,4 +19,12 @@ class TestResponseTest < ActiveSupport::TestCase
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
end
diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb
index 55ebbd5143..03e5274541 100644
--- a/actionpack/test/dispatch/uploaded_file_test.rb
+++ b/actionpack/test/dispatch/uploaded_file_test.rb
@@ -1,4 +1,8 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "tempfile"
+require "stringio"
module ActionDispatch
class UploadedFileTest < ActiveSupport::TestCase
@@ -9,97 +13,118 @@ 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: Tempfile.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: Tempfile.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: Tempfile.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: Tempfile.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: Tempfile.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: Tempfile.new)
+ assert_equal "foo", uf.headers
end
def test_tempfile
- uf = Http::UploadedFile.new(:tempfile => 'foo')
- assert_equal 'foo', uf.tempfile
+ tf = Tempfile.new
+ uf = Http::UploadedFile.new(tempfile: tf)
+ assert_equal tf, uf.tempfile
end
- def test_to_io_returns_the_tempfile
- tf = Object.new
- uf = Http::UploadedFile.new(:tempfile => tf)
- assert_equal tf, uf.to_io
+ def test_to_io_returns_file
+ tf = Tempfile.new
+ uf = Http::UploadedFile.new(tempfile: tf)
+ assert_equal tf.to_io, 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 = Tempfile.new
+ uf = Http::UploadedFile.new(tempfile: tf)
+ assert_equal tf.path, 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 = Tempfile.new
+ tf.close
+ uf = Http::UploadedFile.new(tempfile: tf)
+ assert_equal tf, uf.open
+ assert_not tf.closed?
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 = Tempfile.new
+ uf = Http::UploadedFile.new(tempfile: tf)
+ uf.close
+ assert tf.closed?
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 = Tempfile.new
+ uf = Http::UploadedFile.new(tempfile: tf)
+ uf.close(true)
+ assert tf.closed?
+ assert_nil tf.path
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 = Tempfile.new
+ tf << "thunderhorse"
+ tf.rewind
+ uf = Http::UploadedFile.new(tempfile: tf)
+ 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)
- 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)
- assert_raises(NoMethodError) do
- uf.read
- end
+ tf = Tempfile.new
+ tf << "thunderhorse"
+ tf.rewind
+ uf = Http::UploadedFile.new(tempfile: tf)
+ assert_equal "thunder", uf.read(7)
+ assert_equal "horse", uf.read(5, String.new)
end
def test_delegate_eof_to_tempfile
- tf = Class.new { def eof?; true end; }
- uf = Http::UploadedFile.new(:tempfile => tf.new)
- assert uf.eof?
- end
-
- def test_respond_to?
- tf = Class.new { def read; yield end }
- uf = Http::UploadedFile.new(:tempfile => tf.new)
- assert uf.respond_to?(:headers), 'responds to headers'
- assert uf.respond_to?(:read), 'responds to read'
+ tf = Tempfile.new
+ tf << "thunderhorse"
+ uf = Http::UploadedFile.new(tempfile: tf)
+ assert_equal true, uf.eof?
+ tf.rewind
+ assert_equal false, uf.eof?
+ end
+
+ def test_delegate_to_path_to_tempfile
+ tf = Tempfile.new
+ uf = Http::UploadedFile.new(tempfile: tf)
+ assert_equal tf.to_path, uf.to_path
+ end
+
+ def test_io_copy_stream
+ tf = Tempfile.new
+ tf << "thunderhorse"
+ tf.rewind
+ uf = Http::UploadedFile.new(tempfile: tf)
+ result = StringIO.new
+ IO.copy_stream(uf, result)
+ assert_equal "thunderhorse", result.string
end
end
end
diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb
index 8c9782bb90..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
@@ -13,11 +15,11 @@ module TestUrlGeneration
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,25 +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'
+ 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/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
index 01453323ef..951c761995 100644
--- a/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb
+++ b/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb
@@ -1,3 +1,3 @@
<body>
-<%= cache 'with_options', skip_digest: true, expires_in: 1.minute do %><p>ERB</p><% end %>
+<%= cache 'with_options', skip_digest: true, expires_in: 10 do %><p>ERB</p><% end %>
</body>
diff --git a/actionpack/test/fixtures/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/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/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/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 adf85b860c..b0622ac71a 100644
--- a/actionpack/test/journey/nodes/symbol_test.rb
+++ b/actionpack/test/journey/nodes/symbol_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
@@ -6,10 +8,10 @@ module ActionDispatch
class TestSymbol < ActiveSupport::TestCase
def test_default_regexp?
sym = Symbol.new "foo"
- assert sym.default_regexp?
+ assert_predicate sym, :default_regexp?
sym.regexp = nil
- assert_not sym.default_regexp?
+ assert_not_predicate sym, :default_regexp?
end
end
end
diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb
index 72858f5eda..fcfaba96b0 100644
--- a/actionpack/test/journey/path/pattern_test.rb
+++ b/actionpack/test/journey/path/pattern_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
@@ -8,22 +10,22 @@ module ActionDispatch
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
+ define_method(:"test_to_regexp_#{Regexp.escape(path)}") do
path = Pattern.build(
path,
- { :controller => /.+/ },
+ { controller: /.+/ },
SEPARATORS,
true
)
@@ -32,22 +34,22 @@ module ActionDispatch
end
{
- '/:controller(/:action)' => %r{\A/(#{x})(?:/([^/.?]+))?},
- '/:controller/foo' => %r{\A/(#{x})/foo},
- '/:controller/:action' => %r{\A/(#{x})/([^/.?]+)},
- '/:controller' => %r{\A/(#{x})},
- '/:controller(/:action(/:id))' => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?},
- '/:controller/:action.xml' => %r{\A/(#{x})/([^/.?]+)\.xml},
- '/:controller.:format' => %r{\A/(#{x})\.([^/.?]+)},
- '/:controller(.:format)' => %r{\A/(#{x})(?:\.([^/.?]+))?},
- '/:controller/*foo' => %r{\A/(#{x})/(.+)},
- '/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar},
- '/:foo|*bar' => %r{\A/(?:([^/.?]+)|(.+))},
+ "/:controller(/:action)" => %r{\A/(#{x})(?:/([^/.?]+))?(?:\b|\Z)},
+ "/:controller/foo" => %r{\A/(#{x})/foo(?:\b|\Z)},
+ "/:controller/:action" => %r{\A/(#{x})/([^/.?]+)(?:\b|\Z)},
+ "/:controller" => %r{\A/(#{x})(?:\b|\Z)},
+ "/:controller(/:action(/:id))" => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?(?:\b|\Z)},
+ "/:controller/:action.xml" => %r{\A/(#{x})/([^/.?]+)\.xml(?:\b|\Z)},
+ "/:controller.:format" => %r{\A/(#{x})\.([^/.?]+)(?:\b|\Z)},
+ "/:controller(.:format)" => %r{\A/(#{x})(?:\.([^/.?]+))?(?:\b|\Z)},
+ "/:controller/*foo" => %r{\A/(#{x})/(.+)(?:\b|\Z)},
+ "/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar(?:\b|\Z)},
+ "/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))(?:\b|\Z)},
}.each do |path, expected|
- define_method(:"test_to_non_anchored_regexp_#{path}") do
+ define_method(:"test_to_non_anchored_regexp_#{Regexp.escape(path)}") do
path = Pattern.build(
path,
- { :controller => /.+/ },
+ { controller: /.+/ },
SEPARATORS,
false
)
@@ -56,21 +58,21 @@ module ActionDispatch
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
+ define_method(:"test_names_#{Regexp.escape(path)}") do
path = Pattern.build(
path,
- { :controller => /.+/ },
+ { controller: /.+/ },
SEPARATORS,
true
)
@@ -80,8 +82,8 @@ module ActionDispatch
def test_to_regexp_with_extended_group
path = Pattern.build(
- '/page/:name',
- { :name => /
+ "/page/:name",
+ { name: /
#ROFL
(tender|love
#MAO
@@ -89,16 +91,16 @@ module ActionDispatch
SEPARATORS,
true
)
- 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
@@ -107,31 +109,31 @@ module ActionDispatch
def test_to_regexp_match_non_optional
path = Pattern.build(
- '/:name',
- { :name => /\d+/ },
+ "/:name",
+ { name: /\d+/ },
SEPARATORS,
true
)
- assert_match(path, '/123')
- assert_no_match(path, '/')
+ assert_match(path, "/123")
+ assert_no_match(path, "/")
end
def test_to_regexp_with_group
path = Pattern.build(
- '/page/:name',
- { :name => /(tender|love)/ },
+ "/page/:name",
+ { name: /(tender|love)/ },
SEPARATORS,
true
)
- 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 => /./ }
+ requirements = { name: /(tender|love)/, value: /./ }
path = Pattern.build(
- '/page/:name/:value',
+ "/page/:name/:value",
requirements,
SEPARATORS,
true
@@ -146,26 +148,26 @@ module ActionDispatch
def test_match_data_with_group
path = Pattern.build(
- '/page/:name',
- { :name => /(tender|love)/ },
+ "/page/:name",
+ { name: /(tender|love)/ },
SEPARATORS,
true
)
- 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
path = Pattern.build(
- '/page/:name/:id',
- { :name => /t(((ender|love)))()/ },
+ "/page/:name/:id",
+ { name: /t(((ender|love)))()/ },
SEPARATORS,
true
)
- 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
@@ -173,8 +175,8 @@ module ActionDispatch
def test_star_with_custom_re
z = /\d+/
path = Pattern.build(
- '/page/*foo',
- { :foo => z },
+ "/page/*foo",
+ { foo: z },
SEPARATORS,
true
)
@@ -183,76 +185,76 @@ module ActionDispatch
def test_insensitive_regexp_with_group
path = Pattern.build(
- '/page/:name/aaron',
- { :name => /(tender|love)/i },
+ "/page/:name/aaron",
+ { name: /(tender|love)/i },
SEPARATORS,
true
)
- 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
- path = Pattern.build('/:controller', { }, SEPARATORS, true)
+ 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]
@@ -262,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 22c3b8113d..a8bf4a11e2 100644
--- a/actionpack/test/journey/route_test.rb
+++ b/actionpack/test/journey/route_test.rb
@@ -1,11 +1,13 @@
-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.build("name", app, path, {}, [], defaults)
@@ -16,7 +18,7 @@ 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.build("name", app, path, {}, [], defaults)
@@ -26,71 +28,69 @@ module ActionDispatch
end
def test_path_requirements_override_defaults
- path = Path::Pattern.build(':name', { name: /love/ }, '/', true)
- defaults = { name: 'tender' }
- route = Route.build('name', nil, path, {}, [], 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.build("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)'
+ path = Path::Pattern.from_string "/messages/:id(.:format)"
route = Route.build("name", nil, path, {}, [],
- { :controller => 'foo', :action => 'bar' })
+ controller: "foo", action: "bar")
assert_equal(//, route.ip)
end
def test_format_with_star
- path = Path::Pattern.from_string '/:controller/*extra'
+ 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',
- })
+ 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.build("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.build("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.build("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.build("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.build "name", nil, path, constraints, [:controller, :action], defaults
@@ -98,7 +98,7 @@ module ActionDispatch
path = Path::Pattern.from_string "/:controller(/:action(/:id))(.:format)"
generic = Route.build "name", nil, path, constraints, [], {}
- knowledge = {:id=>20, :controller=>"pages", :action=>"show"}
+ knowledge = { "id" => true, "controller" => true, "action" => true }
routes = [specific, generic]
diff --git a/actionpack/test/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 75caf56d32..f8d89def6a 100644
--- a/actionpack/test/journey/router_test.rb
+++ b/actionpack/test/journey/router_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
@@ -6,8 +8,8 @@ module ActionDispatch
attr_reader :mapper, :routes, :route_set, :router
def setup
- @app = Routing::RouteSet::Dispatcher.new({})
- @route_set = ActionDispatch::Routing::RouteSet.new
+ @app = Routing::RouteSet::Dispatcher.new({})
+ @route_set = ActionDispatch::Routing::RouteSet.new
@routes = @route_set.router.routes
@router = @route_set.router
@formatter = @route_set.formatter
@@ -15,9 +17,9 @@ module ActionDispatch
end
def test_dashes
- get '/foo-bar-baz', to: 'foo#bar'
+ 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
@@ -26,10 +28,10 @@ module ActionDispatch
end
def test_unicode
- get '/ã»ã’', to: 'foo#bar'
+ get "/ã»ã’", to: "foo#bar"
- #match the escaped version of /ã»ã’
- 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
@@ -38,10 +40,10 @@ module ActionDispatch
end
def test_regexp_first_precedence
- get "/whois/:domain", :domain => /\w+\.[\w\.]+/, to: "foo#bar"
+ 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|
@@ -51,47 +53,47 @@ module ActionDispatch
r = list.first
- assert_equal '/whois/:domain(.:format)', r.path.spec.to_s
+ assert_equal "/whois/:domain(.:format)", r.path.spec.to_s
end
def test_required_parts_verified_are_anchored
- get "/foo/:id", :id => /\d/, anchor: false, to: "foo#bar"
+ get "/foo/:id", id: /\d/, anchor: false, to: "foo#bar"
assert_raises(ActionController::UrlGenerationError) do
- @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { })
+ @formatter.generate(nil, { controller: "foo", action: "bar", id: "10" }, {})
end
end
def test_required_parts_are_verified_when_building
- get "/foo/:id", :id => /\d+/, anchor: false, to: "foo#bar"
+ get "/foo/:id", id: /\d+/, anchor: false, to: "foo#bar"
- path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { })
- assert_equal '/foo/10', path
+ 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
- get "/foo(/:id)", :id => /\d/, :to => "foo#bar"
+ get "/foo(/:id)", id: /\d/, to: "foo#bar"
- path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { })
- assert_equal '/foo/10', path
+ path, _ = @formatter.generate(nil, { controller: "foo", action: "bar", id: "10" }, {})
+ assert_equal "/foo/10", path
- path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar" }, { })
- assert_equal '/foo', path
+ path, _ = @formatter.generate(nil, { controller: "foo", action: "bar" }, {})
+ assert_equal "/foo", path
- path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :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"
- get "/foo/:id", :as => route_name, :id => /\d+/, :to => "foo#bar"
+ get "/foo/:id", as: route_name, id: /\d+/, to: "foo#bar"
error = assert_raises(ActionController::UrlGenerationError) do
- @formatter.generate(route_name, { }, { })
+ @formatter.generate(route_name, {}, {})
end
assert_match(/missing required keys: \[:id\]/, error.message)
@@ -101,7 +103,7 @@ 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)
@@ -109,58 +111,58 @@ module ActionDispatch
def test_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']
+ 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
- app = lambda { |env| [200, {}, ['success!']] }
- get '/weblog', :to => app
+ app = lambda { |env| [200, {}, ["success!"]] }
+ 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
- get '/foo(/:id)', to: "foo#bar", id: nil
+ get "/foo(/:id)", to: "foo#bar", id: nil
- env = rails_env 'PATH_INFO' => '/foo/10'
+ env = rails_env "PATH_INFO" => "/foo/10"
router.recognize(env) do |r, params|
- assert_equal({:id => '10', :controller => "foo", :action => "bar"}, params)
+ assert_equal({ id: "10", controller: "foo", action: "bar" }, params)
end
- env = rails_env 'PATH_INFO' => '/foo'
+ env = rails_env "PATH_INFO" => "/foo"
router.recognize(env) do |r, params|
- assert_equal({:id => nil, :controller => "foo", :action => "bar"}, params)
+ assert_equal({ id: nil, controller: "foo", action: "bar" }, params)
end
end
def test_recognize_with_unbound_regexp
get "/foo", anchor: false, to: "foo#bar"
- env = rails_env 'PATH_INFO' => '/foo/bar'
+ env = rails_env "PATH_INFO" => "/foo/bar"
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
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) { |*_| }
- 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
@@ -172,7 +174,7 @@ module ActionDispatch
].each do |path|
get path, to: "foo#bar"
end
- env = rails_env 'PATH_INFO' => '/messages/unknown/path'
+ env = rails_env "PATH_INFO" => "/messages/unknown/path"
yielded = false
router.recognize(env) do |*whatever|
@@ -184,33 +186,33 @@ module ActionDispatch
def test_required_part_in_recall
get "/messages/:a/:b", to: "foo#bar"
- path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :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
get "/*path", to: "foo#bar"
- path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar" }, { :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
- get "/messages/:action(/:id(.:format))", to: 'foo#bar'
- get "/messages/:id(.:format)", to: 'bar#baz'
+ get "/messages/:action(/:id(.:format))", to: "foo#bar"
+ get "/messages/:id(.:format)", to: "bar#baz"
- path, _ = @formatter.generate(nil, { :controller => "foo", :id => 10 }, { :action => 'index' })
+ path, _ = @formatter.generate(nil, { controller: "foo", id: 10 }, { action: "index" })
assert_equal "/messages/index/10", path
end
def test_nil_path_parts_are_ignored
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
@@ -219,11 +221,11 @@ module ActionDispatch
get "/", Hash[params]
path, _ = @formatter.generate(nil, Hash[params], {})
- assert_equal '/', path
+ assert_equal "/", path
end
def test_generate_calls_param_proc
- get '/:controller(/:action)', to: "foo#bar"
+ get "/:controller(/:action)", to: "foo#bar"
parameterized = []
params = [ [:controller, "tasks"],
@@ -233,71 +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
- get '/:controller(/:action)', to: 'foo#bar'
+ 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
- get '/:controller(/:action)', to: "foo#bar"
+ 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
- get '/:controller(/:action)', to: "foo#bar"
+ 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
- get '/:controller(/:action)', to: "foo#bar"
+ 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
- get '/:controller/:action/:name', to: "foo#bar"
- primarty_parameters = {
- :id => 1,
- :controller => "tasks",
- :action => "show",
- :relative_url_root => nil
+ get "/:controller/:action/:name", to: "foo#bar"
+ primary_parameters = {
+ 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)
+ request_parameters = primary_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, _|k.to_s }].inspect}, missing required keys: #{[missing_key.to_sym].inspect}"
error = assert_raises(ActionController::UrlGenerationError) do
@formatter.generate(
@@ -307,42 +309,42 @@ module ActionDispatch
end
def test_generate_uses_recall_if_needed
- get '/:controller(/:action(/:id))', to: "foo#bar"
+ 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
- get '/:controller(/:action)', to: 'foo#bar', as: '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
- get "/:controller(/:action(/:id))", to: 'foo#bar'
+ get "/:controller(/:action(/:id))", to: "foo#bar"
route = @routes.first
- env = rails_env 'PATH_INFO' => request_path
- called = false
+ env = rails_env "PATH_INFO" => request_path
+ called = false
router.recognize(env) do |r, params|
assert_equal route, r
- assert_equal({ :action => "bar" }.merge(expected), params)
+ assert_equal({ action: "bar" }.merge(expected), params)
called = true
end
@@ -351,19 +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
- get '/:segment/*splat', to: 'foo#bar'
+ 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|
assert_equal route, r
- assert_equal(expected.merge(:controller=>"foo", :action=>"bar"), params)
+ assert_equal(expected.merge(controller: "foo", action: "bar"), params)
called = true
end
@@ -372,15 +374,15 @@ module ActionDispatch
end
def test_namespaced_controller
- get "/:controller(/:action(/:id))", { :controller => /.+?/ }
+ 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|
@@ -395,8 +397,8 @@ module ActionDispatch
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|
assert_equal route, r
@@ -408,11 +410,11 @@ module ActionDispatch
end
def test_recognize_head_route
- match "/books(/:action(.:format))", via: 'head', to: 'foo#bar'
+ match "/books(/:action(.:format))", via: "head", to: "foo#bar"
env = rails_env(
- 'PATH_INFO' => '/books/list.rss',
- 'REQUEST_METHOD' => 'HEAD'
+ "PATH_INFO" => "/books/list.rss",
+ "REQUEST_METHOD" => "HEAD"
)
called = false
@@ -424,10 +426,10 @@ module ActionDispatch
end
def test_recognize_head_request_as_get_route
- get "/books(/:action(.:format))", to: 'foo#bar'
+ get "/books(/:action(.:format))", to: "foo#bar"
- env = rails_env 'PATH_INFO' => '/books/list.rss',
- "REQUEST_METHOD" => "HEAD"
+ env = rails_env "PATH_INFO" => "/books/list.rss",
+ "REQUEST_METHOD" => "HEAD"
called = false
router.recognize(env) do |r, params|
@@ -440,7 +442,7 @@ module ActionDispatch
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
@@ -454,7 +456,7 @@ module ActionDispatch
def test_recognize_cares_about_post_verbs
match "/books(/:action(.:format))", to: "foo#bar", via: :post
- env = rails_env 'PATH_INFO' => '/books/list.rss',
+ env = rails_env "PATH_INFO" => "/books/list.rss",
"REQUEST_METHOD" => "POST"
called = false
@@ -469,7 +471,7 @@ module ActionDispatch
match "/books(/:action(.:format))", to: "foo#bar", via: [:post, :get]
%w( POST GET ).each do |verb|
- env = rails_env 'PATH_INFO' => '/books/list.rss',
+ env = rails_env "PATH_INFO" => "/books/list.rss",
"REQUEST_METHOD" => verb
called = false
@@ -480,8 +482,8 @@ module ActionDispatch
assert called
end
- env = rails_env 'PATH_INFO' => '/books/list.rss',
- "REQUEST_METHOD" => 'PUT'
+ env = rails_env "PATH_INFO" => "/books/list.rss",
+ "REQUEST_METHOD" => "PUT"
called = false
router.recognize(env) do |r, params|
@@ -491,43 +493,52 @@ module ActionDispatch
assert_not called
end
+ def test_eager_load_with_routes
+ get "/foo-bar", to: "foo#bar"
+ assert_nil router.eager_load!
+ end
+
+ def test_eager_load_without_routes
+ assert_nil router.eager_load!
+ end
+
private
- def get *args
- ActiveSupport::Deprecation.silence do
- mapper.get(*args)
+ def get(*args)
+ ActiveSupport::Deprecation.silence do
+ mapper.get(*args)
+ end
end
- end
- def match *args
- ActiveSupport::Deprecation.silence do
- mapper.match(*args)
+ def match(*args)
+ ActiveSupport::Deprecation.silence do
+ mapper.match(*args)
+ end
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
+ 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 f8293dfc5f..d5c81a8421 100644
--- a/actionpack/test/journey/routes_test.rb
+++ b/actionpack/test/journey/routes_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
module ActionDispatch
module Journey
@@ -6,7 +8,7 @@ module ActionDispatch
attr_reader :routes, :mapper
def setup
- @route_set = ActionDispatch::Routing::RouteSet.new
+ @route_set = ActionDispatch::Routing::RouteSet.new
@routes = @route_set.router.routes
@router = @route_set.router
@mapper = ActionDispatch::Routing::Mapper.new @route_set
@@ -14,45 +16,45 @@ module ActionDispatch
end
def test_clear
- mapper.get "/foo(/:id)", to: "foo#bar", as: 'aaron'
- assert_not_predicate 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
- mapper.get "/foo(/:id)", to: "foo#bar", as: 'aaron'
+ mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron"
ast = routes.ast
- mapper.get "/foo(/:id)", to: "foo#bar", as: 'gorby'
+ mapper.get "/foo(/:id)", to: "foo#bar", as: "gorby"
assert_not_equal ast, routes.ast
end
def test_simulator_changes
- mapper.get "/foo(/:id)", to: "foo#bar", as: 'aaron'
+ mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron"
sim = routes.simulator
- mapper.get "/foo(/:id)", to: "foo#bar", as: 'gorby'
+ mapper.get "/foo(/:id)", to: "foo#bar", as: "gorby"
assert_not_equal sim, routes.simulator
end
def test_partition_route
- mapper.get "/foo(/:id)", to: "foo#bar", as: 'aaron'
+ mapper.get "/foo(/:id)", to: "foo#bar", as: "aaron"
assert_equal 1, @routes.anchored_routes.length
- assert_predicate @routes.custom_routes, :empty?
+ assert_empty @routes.custom_routes
- mapper.get "/hello/:who", to: "foo#bar", as: 'bar', who: /\d/
+ mapper.get "/hello/:who", to: "foo#bar", as: "bar", who: /\d/
assert_equal 1, @routes.custom_routes.length
assert_equal 1, @routes.anchored_routes.length
end
def test_first_name_wins
- mapper.get "/hello", to: "foo#bar", as: 'aaron'
+ mapper.get "/hello", to: "foo#bar", as: "aaron"
assert_raise(ArgumentError) do
- mapper.get "/aaron", to: "foo#bar", as: 'aaron'
+ mapper.get "/aaron", to: "foo#bar", as: "aaron"
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/actiontext/.gitignore b/actiontext/.gitignore
new file mode 100644
index 0000000000..739e00a2cb
--- /dev/null
+++ b/actiontext/.gitignore
@@ -0,0 +1,5 @@
+/test/dummy/db/*.sqlite3
+/test/dummy/db/*.sqlite3-journal
+/test/dummy/log/*.log
+/test/dummy/tmp/
+/tmp/
diff --git a/actiontext/CHANGELOG.md b/actiontext/CHANGELOG.md
new file mode 100644
index 0000000000..b79ff612fb
--- /dev/null
+++ b/actiontext/CHANGELOG.md
@@ -0,0 +1,15 @@
+## Rails 6.0.0.beta3 (March 11, 2019) ##
+
+* No changes.
+
+
+## Rails 6.0.0.beta2 (February 25, 2019) ##
+
+* No changes.
+
+
+## Rails 6.0.0.beta1 (January 18, 2019) ##
+
+* Added to Rails.
+
+ *DHH*
diff --git a/actiontext/MIT-LICENSE b/actiontext/MIT-LICENSE
new file mode 100644
index 0000000000..e28efa78e1
--- /dev/null
+++ b/actiontext/MIT-LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 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/actiontext/README.md b/actiontext/README.md
new file mode 100644
index 0000000000..aa1ad0153b
--- /dev/null
+++ b/actiontext/README.md
@@ -0,0 +1,9 @@
+# Action Text
+
+Action Text brings rich text content and editing to Rails. It includes the [Trix editor](https://trix-editor.org) that handles everything from formatting to links to quotes to lists to embedded images and galleries. The rich text content generated by the Trix editor is saved in its own RichText model that's associated with any existing Active Record model in the application. Any embedded images (or other attachments) are automatically stored using Active Storage and associated with the included RichText model.
+
+You can read more about Action Text in the [Action Text Overview](https://edgeguides.rubyonrails.org/action_text_overview.html) guide.
+
+## License
+
+Action Text is released under the [MIT License](https://opensource.org/licenses/MIT).
diff --git a/actiontext/Rakefile b/actiontext/Rakefile
new file mode 100644
index 0000000000..36aed17282
--- /dev/null
+++ b/actiontext/Rakefile
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "bundler/setup"
+require "bundler/gem_tasks"
+require "rake/testtask"
+
+task :package
+
+Rake::TestTask.new do |t|
+ t.libs << "test"
+ t.pattern = "test/**/*_test.rb"
+ t.verbose = true
+end
+
+task default: :test
diff --git a/actiontext/actiontext.gemspec b/actiontext/actiontext.gemspec
new file mode 100644
index 0000000000..5583178e12
--- /dev/null
+++ b/actiontext/actiontext.gemspec
@@ -0,0 +1,37 @@
+# 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 = "actiontext"
+ s.version = version
+ s.summary = "Rich text framework."
+ s.description = "Edit and display rich text in Rails applications."
+
+ s.required_ruby_version = ">= 2.5.0"
+
+ s.license = "MIT"
+
+ s.authors = ["Javan Makhmali", "Sam Stephenson", "David Heinemeier Hansson"]
+ s.email = ["javan@javan.us", "sstephenson@gmail.com", "david@loudthinking.com"]
+ s.homepage = "https://rubyonrails.org"
+
+ s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*", "db/**/*", "package.json"]
+ s.require_path = "lib"
+
+ s.metadata = {
+ "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actiontext",
+ "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actiontext/CHANGELOG.md"
+ }
+
+ # 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 "activerecord", version
+ s.add_dependency "activestorage", version
+ s.add_dependency "actionpack", version
+
+ s.add_dependency "nokogiri", ">= 1.8.5"
+end
diff --git a/actiontext/app/helpers/action_text/content_helper.rb b/actiontext/app/helpers/action_text/content_helper.rb
new file mode 100644
index 0000000000..2005033d5c
--- /dev/null
+++ b/actiontext/app/helpers/action_text/content_helper.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "rails-html-sanitizer"
+
+module ActionText
+ module ContentHelper
+ SANITIZER = Rails::Html::Sanitizer.white_list_sanitizer
+ ALLOWED_TAGS = SANITIZER.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ]
+ ALLOWED_ATTRIBUTES = SANITIZER.allowed_attributes + ActionText::Attachment::ATTRIBUTES
+
+ def render_action_text_content(content)
+ content = content.render_attachments do |attachment|
+ unless attachment.in?(content.gallery_attachments)
+ attachment.node.tap do |node|
+ node.inner_html = render(attachment, in_gallery: false).chomp
+ end
+ end
+ end
+
+ content = content.render_attachment_galleries do |attachment_gallery|
+ render(layout: attachment_gallery, object: attachment_gallery) do
+ attachment_gallery.attachments.map do |attachment|
+ attachment.node.inner_html = render(attachment, in_gallery: true).chomp
+ attachment.to_html
+ end.join("").html_safe
+ end.chomp
+ end
+
+ sanitize content.to_html, tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES
+ end
+ end
+end
diff --git a/actiontext/app/helpers/action_text/tag_helper.rb b/actiontext/app/helpers/action_text/tag_helper.rb
new file mode 100644
index 0000000000..1dc6202ae1
--- /dev/null
+++ b/actiontext/app/helpers/action_text/tag_helper.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require "action_view/helpers/tags/placeholderable"
+
+module ActionText
+ module TagHelper
+ cattr_accessor(:id, instance_accessor: false) { 0 }
+
+ # Returns a +trix-editor+ tag that instantiates the Trix JavaScript editor as well as a hidden field
+ # that Trix will write to on changes, so the content will be sent on form submissions.
+ #
+ # ==== Options
+ # * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied.
+ #
+ # ==== Example
+ #
+ # rich_text_area_tag "content", message.content
+ # # <input type="hidden" name="content" id="trix_input_post_1">
+ # # <trix-editor id="content" input="trix_input_post_1" class="trix-content" ...></trix-editor>
+ def rich_text_area_tag(name, value = nil, options = {})
+ options = options.symbolize_keys
+
+ options[:input] ||= "trix_input_#{ActionText::TagHelper.id += 1}"
+ options[:class] ||= "trix-content"
+
+ options[:data] ||= {}
+ options[:data][:direct_upload_url] = main_app.rails_direct_uploads_url
+ options[:data][:blob_url_template] = main_app.rails_service_blob_url(":signed_id", ":filename")
+
+ editor_tag = content_tag("trix-editor", "", options)
+ input_tag = hidden_field_tag(name, value, id: options[:input])
+
+ input_tag + editor_tag
+ end
+ end
+end
+
+module ActionView::Helpers
+ class Tags::ActionText < Tags::Base
+ include Tags::Placeholderable
+
+ delegate :dom_id, to: ActionView::RecordIdentifier
+
+ def render
+ options = @options.stringify_keys
+ add_default_name_and_id(options)
+ options["input"] ||= dom_id(object, [options["id"], :trix_input].compact.join("_")) if object
+ @template_object.rich_text_area_tag(options.delete("name"), editable_value, options)
+ end
+
+ def editable_value
+ value&.body.try(:to_trix_html)
+ end
+ end
+
+ module FormHelper
+ # Returns a +trix-editor+ tag that instantiates the Trix JavaScript editor as well as a hidden field
+ # that Trix will write to on changes, so the content will be sent on form submissions.
+ #
+ # ==== Options
+ # * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied.
+ #
+ # ==== Example
+ # form_with(model: @message) do |form|
+ # form.rich_text_area :content
+ # end
+ # # <input type="hidden" name="message[content]" id="message_content_trix_input_message_1">
+ # # <trix-editor id="content" input="message_content_trix_input_message_1" class="trix-content" ...></trix-editor>
+ def rich_text_area(object_name, method, options = {})
+ Tags::ActionText.new(object_name, method, self, options).render
+ end
+ end
+
+ class FormBuilder
+ def rich_text_area(method, options = {})
+ @template.rich_text_area(@object_name, method, objectify_options(options))
+ end
+ end
+end
diff --git a/actiontext/app/javascript/actiontext/attachment_upload.js b/actiontext/app/javascript/actiontext/attachment_upload.js
new file mode 100644
index 0000000000..77fbc97df6
--- /dev/null
+++ b/actiontext/app/javascript/actiontext/attachment_upload.js
@@ -0,0 +1,45 @@
+import { DirectUpload } from "@rails/activestorage"
+
+export class AttachmentUpload {
+ constructor(attachment, element) {
+ this.attachment = attachment
+ this.element = element
+ this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this)
+ }
+
+ start() {
+ this.directUpload.create(this.directUploadDidComplete.bind(this))
+ }
+
+ directUploadWillStoreFileWithXHR(xhr) {
+ xhr.upload.addEventListener("progress", event => {
+ const progress = event.loaded / event.total * 100
+ this.attachment.setUploadProgress(progress)
+ })
+ }
+
+ directUploadDidComplete(error, attributes) {
+ if (error) {
+ throw new Error(`Direct upload failed: ${error}`)
+ }
+
+ this.attachment.setAttributes({
+ sgid: attributes.attachable_sgid,
+ url: this.createBlobUrl(attributes.signed_id, attributes.filename)
+ })
+ }
+
+ createBlobUrl(signedId, filename) {
+ return this.blobUrlTemplate
+ .replace(":signed_id", signedId)
+ .replace(":filename", encodeURIComponent(filename))
+ }
+
+ get directUploadUrl() {
+ return this.element.dataset.directUploadUrl
+ }
+
+ get blobUrlTemplate() {
+ return this.element.dataset.blobUrlTemplate
+ }
+}
diff --git a/actiontext/app/javascript/actiontext/index.js b/actiontext/app/javascript/actiontext/index.js
new file mode 100644
index 0000000000..0e9251018a
--- /dev/null
+++ b/actiontext/app/javascript/actiontext/index.js
@@ -0,0 +1,10 @@
+import { AttachmentUpload } from "./attachment_upload"
+
+addEventListener("trix-attachment-add", event => {
+ const { attachment, target } = event
+
+ if (attachment.file) {
+ const upload = new AttachmentUpload(attachment, target)
+ upload.start()
+ }
+})
diff --git a/actiontext/app/models/action_text/rich_text.rb b/actiontext/app/models/action_text/rich_text.rb
new file mode 100644
index 0000000000..19fa3e030e
--- /dev/null
+++ b/actiontext/app/models/action_text/rich_text.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module ActionText
+ # The RichText record holds the content produced by the Trix editor in a serialized +body+ attribute.
+ # It also holds all the references to the embedded files, which are stored using Active Storage.
+ # This record is then associated with the Active Record model the application desires to have
+ # rich text content using the +has_rich_text+ class method.
+ class RichText < ActiveRecord::Base
+ self.table_name = "action_text_rich_texts"
+
+ serialize :body, ActionText::Content
+ delegate :to_s, :nil?, to: :body
+
+ belongs_to :record, polymorphic: true, touch: true
+ has_many_attached :embeds
+
+ before_save do
+ self.embeds = body.attachables.grep(ActiveStorage::Blob) if body.present?
+ end
+
+ def to_plain_text
+ body&.to_plain_text.to_s
+ end
+
+ delegate :blank?, :empty?, :present?, to: :to_plain_text
+ end
+end
+
+ActiveSupport.run_load_hooks :action_text_rich_text, ActionText::RichText
diff --git a/actiontext/app/views/action_text/attachables/_missing_attachable.html.erb b/actiontext/app/views/action_text/attachables/_missing_attachable.html.erb
new file mode 100644
index 0000000000..5ffd93b89e
--- /dev/null
+++ b/actiontext/app/views/action_text/attachables/_missing_attachable.html.erb
@@ -0,0 +1 @@
+<%= "☒" -%>
diff --git a/actiontext/app/views/action_text/attachables/_remote_image.html.erb b/actiontext/app/views/action_text/attachables/_remote_image.html.erb
new file mode 100644
index 0000000000..3372f8d940
--- /dev/null
+++ b/actiontext/app/views/action_text/attachables/_remote_image.html.erb
@@ -0,0 +1,8 @@
+<figure class="attachment attachment--preview">
+ <%= image_tag(remote_image.url, width: remote_image.width, height: remote_image.height) %>
+ <% if caption = remote_image.try(:caption) %>
+ <figcaption class="attachment__caption">
+ <%= caption %>
+ </figcaption>
+ <% end %>
+</figure>
diff --git a/actiontext/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb b/actiontext/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb
new file mode 100644
index 0000000000..6bc8674dc5
--- /dev/null
+++ b/actiontext/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb
@@ -0,0 +1,3 @@
+<div class="attachment-gallery attachment-gallery--<%= attachment_gallery.size %>">
+ <%= yield %>
+</div>
diff --git a/actiontext/app/views/action_text/content/_layout.html.erb b/actiontext/app/views/action_text/content/_layout.html.erb
new file mode 100644
index 0000000000..55cb708ac4
--- /dev/null
+++ b/actiontext/app/views/action_text/content/_layout.html.erb
@@ -0,0 +1,3 @@
+<div class="trix-content">
+ <%= render_action_text_content(content) %>
+</div>
diff --git a/actiontext/app/views/active_storage/blobs/_blob.html.erb b/actiontext/app/views/active_storage/blobs/_blob.html.erb
new file mode 100644
index 0000000000..49ba357dd1
--- /dev/null
+++ b/actiontext/app/views/active_storage/blobs/_blob.html.erb
@@ -0,0 +1,14 @@
+<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
+ <% if blob.representable? %>
+ <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
+ <% end %>
+
+ <figcaption class="attachment__caption">
+ <% if caption = blob.try(:caption) %>
+ <%= caption %>
+ <% else %>
+ <span class="attachment__name"><%= blob.filename %></span>
+ <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
+ <% end %>
+ </figcaption>
+</figure>
diff --git a/actiontext/bin/test b/actiontext/bin/test
new file mode 100755
index 0000000000..c53377cc97
--- /dev/null
+++ b/actiontext/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/actiontext/bin/webpack b/actiontext/bin/webpack
new file mode 100755
index 0000000000..ac2adfb6b9
--- /dev/null
+++ b/actiontext/bin/webpack
@@ -0,0 +1,29 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application 'webpack' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require "pathname"
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+bundle_binstub = File.expand_path("../bundle", __FILE__)
+
+if File.file?(bundle_binstub)
+ if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 150))
+ load(bundle_binstub)
+ else
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
+Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
+ end
+end
+
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("webpacker", "webpack")
diff --git a/actiontext/bin/webpack-dev-server b/actiontext/bin/webpack-dev-server
new file mode 100755
index 0000000000..c1ac52d675
--- /dev/null
+++ b/actiontext/bin/webpack-dev-server
@@ -0,0 +1,29 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application 'webpack-dev-server' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require "pathname"
+ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+bundle_binstub = File.expand_path("../bundle", __FILE__)
+
+if File.file?(bundle_binstub)
+ if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 150))
+ load(bundle_binstub)
+ else
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
+Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
+ end
+end
+
+require "rubygems"
+require "bundler/setup"
+
+load Gem.bin_path("webpacker", "webpack-dev-server")
diff --git a/actiontext/db/migrate/20180528164100_create_action_text_tables.rb b/actiontext/db/migrate/20180528164100_create_action_text_tables.rb
new file mode 100644
index 0000000000..e7c66ea6ae
--- /dev/null
+++ b/actiontext/db/migrate/20180528164100_create_action_text_tables.rb
@@ -0,0 +1,13 @@
+class CreateActionTextTables < ActiveRecord::Migration[6.0]
+ def change
+ create_table :action_text_rich_texts do |t|
+ t.string :name, null: false
+ t.text :body, size: :long
+ t.references :record, null: false, polymorphic: true, index: false
+
+ t.timestamps
+
+ t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
+ end
+ end
+end
diff --git a/actiontext/lib/action_text.rb b/actiontext/lib/action_text.rb
new file mode 100644
index 0000000000..0dd6318f89
--- /dev/null
+++ b/actiontext/lib/action_text.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "active_support"
+require "active_support/rails"
+
+require "nokogiri"
+
+module ActionText
+ extend ActiveSupport::Autoload
+
+ autoload :Attachable
+ autoload :AttachmentGallery
+ autoload :Attachment
+ autoload :Attribute
+ autoload :Content
+ autoload :Fragment
+ autoload :HtmlConversion
+ autoload :PlainTextConversion
+ autoload :Serialization
+ autoload :TrixAttachment
+
+ module Attachables
+ extend ActiveSupport::Autoload
+
+ autoload :ContentAttachment
+ autoload :MissingAttachable
+ autoload :RemoteImage
+ end
+
+ module Attachments
+ extend ActiveSupport::Autoload
+
+ autoload :Caching
+ autoload :Minification
+ autoload :TrixConversion
+ end
+end
diff --git a/actiontext/lib/action_text/attachable.rb b/actiontext/lib/action_text/attachable.rb
new file mode 100644
index 0000000000..3343bcc308
--- /dev/null
+++ b/actiontext/lib/action_text/attachable.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+module ActionText
+ module Attachable
+ extend ActiveSupport::Concern
+
+ LOCATOR_NAME = "attachable"
+
+ class << self
+ def from_node(node)
+ if attachable = attachable_from_sgid(node["sgid"])
+ attachable
+ elsif attachable = ActionText::Attachables::ContentAttachment.from_node(node)
+ attachable
+ elsif attachable = ActionText::Attachables::RemoteImage.from_node(node)
+ attachable
+ else
+ ActionText::Attachables::MissingAttachable
+ end
+ end
+
+ def from_attachable_sgid(sgid, options = {})
+ method = sgid.is_a?(Array) ? :locate_many_signed : :locate_signed
+ record = GlobalID::Locator.public_send(method, sgid, options.merge(for: LOCATOR_NAME))
+ record || raise(ActiveRecord::RecordNotFound)
+ end
+
+ private
+ def attachable_from_sgid(sgid)
+ from_attachable_sgid(sgid)
+ rescue ActiveRecord::RecordNotFound
+ nil
+ end
+ end
+
+ class_methods do
+ def from_attachable_sgid(sgid)
+ ActionText::Attachable.from_attachable_sgid(sgid, only: self)
+ end
+ end
+
+ def attachable_sgid
+ to_sgid(expires_in: nil, for: LOCATOR_NAME).to_s
+ end
+
+ def attachable_content_type
+ try(:content_type) || "application/octet-stream"
+ end
+
+ def attachable_filename
+ filename.to_s if respond_to?(:filename)
+ end
+
+ def attachable_filesize
+ try(:byte_size) || try(:filesize)
+ end
+
+ def attachable_metadata
+ try(:metadata) || {}
+ end
+
+ def previewable_attachable?
+ false
+ end
+
+ def as_json(*)
+ super.merge(attachable_sgid: attachable_sgid)
+ end
+
+ def to_trix_content_attachment_partial_path
+ to_partial_path
+ end
+
+ def to_rich_text_attributes(attributes = {})
+ attributes.dup.tap do |attrs|
+ attrs[:sgid] = attachable_sgid
+ attrs[:content_type] = attachable_content_type
+ attrs[:previewable] = true if previewable_attachable?
+ attrs[:filename] = attachable_filename
+ attrs[:filesize] = attachable_filesize
+ attrs[:width] = attachable_metadata[:width]
+ attrs[:height] = attachable_metadata[:height]
+ end.compact
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/attachables/content_attachment.rb b/actiontext/lib/action_text/attachables/content_attachment.rb
new file mode 100644
index 0000000000..804f74713f
--- /dev/null
+++ b/actiontext/lib/action_text/attachables/content_attachment.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module ActionText
+ module Attachables
+ class ContentAttachment
+ include ActiveModel::Model
+
+ def self.from_node(node)
+ if node["content-type"]
+ if matches = node["content-type"].match(/vnd\.rubyonrails\.(.+)\.html/)
+ attachment = new(name: matches[1])
+ attachment if attachment.valid?
+ end
+ end
+ end
+
+ attr_accessor :name
+ validates_inclusion_of :name, in: %w( horizontal-rule )
+
+ def attachable_plain_text_representation(caption)
+ case name
+ when "horizontal-rule"
+ " ┄ "
+ else
+ " "
+ end
+ end
+
+ def to_partial_path
+ "action_text/attachables/content_attachment"
+ end
+
+ def to_trix_content_attachment_partial_path
+ "action_text/attachables/content_attachments/#{name.underscore}"
+ end
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/attachables/missing_attachable.rb b/actiontext/lib/action_text/attachables/missing_attachable.rb
new file mode 100644
index 0000000000..2f3bd40563
--- /dev/null
+++ b/actiontext/lib/action_text/attachables/missing_attachable.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActionText
+ module Attachables
+ module MissingAttachable
+ extend ActiveModel::Naming
+
+ def self.to_partial_path
+ "action_text/attachables/missing_attachable"
+ end
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/attachables/remote_image.rb b/actiontext/lib/action_text/attachables/remote_image.rb
new file mode 100644
index 0000000000..650b11862b
--- /dev/null
+++ b/actiontext/lib/action_text/attachables/remote_image.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module ActionText
+ module Attachables
+ class RemoteImage
+ extend ActiveModel::Naming
+
+ class << self
+ def from_node(node)
+ if node["url"] && content_type_is_image?(node["content-type"])
+ new(attributes_from_node(node))
+ end
+ end
+
+ private
+ def content_type_is_image?(content_type)
+ content_type.to_s =~ /^image(\/.+|$)/
+ end
+
+ def attributes_from_node(node)
+ { url: node["url"],
+ content_type: node["content-type"],
+ width: node["width"],
+ height: node["height"] }
+ end
+ end
+
+ attr_reader :url, :content_type, :width, :height
+
+ def initialize(attributes = {})
+ @url = attributes[:url]
+ @content_type = attributes[:content_type]
+ @width = attributes[:width]
+ @height = attributes[:height]
+ end
+
+ def attachable_plain_text_representation(caption)
+ "[#{caption || "Image"}]"
+ end
+
+ def to_partial_path
+ "action_text/attachables/remote_image"
+ end
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/attachment.rb b/actiontext/lib/action_text/attachment.rb
new file mode 100644
index 0000000000..e90a3e7d48
--- /dev/null
+++ b/actiontext/lib/action_text/attachment.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+module ActionText
+ class Attachment
+ include Attachments::TrixConversion, Attachments::Minification, Attachments::Caching
+
+ TAG_NAME = "action-text-attachment"
+ SELECTOR = TAG_NAME
+ ATTRIBUTES = %w( sgid content-type url href filename filesize width height previewable presentation caption )
+
+ class << self
+ def fragment_by_canonicalizing_attachments(content)
+ fragment_by_minifying_attachments(fragment_by_converting_trix_attachments(content))
+ end
+
+ def from_node(node, attachable = nil)
+ new(node, attachable || ActionText::Attachable.from_node(node))
+ end
+
+ def from_attachables(attachables)
+ Array(attachables).map { |attachable| from_attachable(attachable) }.compact
+ end
+
+ def from_attachable(attachable, attributes = {})
+ if node = node_from_attributes(attachable.to_rich_text_attributes(attributes))
+ new(node, attachable)
+ end
+ end
+
+ def from_attributes(attributes, attachable = nil)
+ if node = node_from_attributes(attributes)
+ from_node(node, attachable)
+ end
+ end
+
+ private
+ def node_from_attributes(attributes)
+ if attributes = process_attributes(attributes).presence
+ ActionText::HtmlConversion.create_element(TAG_NAME, attributes)
+ end
+ end
+
+ def process_attributes(attributes)
+ attributes.transform_keys { |key| key.to_s.underscore.dasherize }.slice(*ATTRIBUTES)
+ end
+ end
+
+ attr_reader :node, :attachable
+
+ delegate :to_param, to: :attachable
+ delegate_missing_to :attachable
+
+ def initialize(node, attachable)
+ @node = node
+ @attachable = attachable
+ end
+
+ def caption
+ node_attributes["caption"].presence
+ end
+
+ def full_attributes
+ node_attributes.merge(attachable_attributes).merge(sgid_attributes)
+ end
+
+ def with_full_attributes
+ self.class.from_attributes(full_attributes, attachable)
+ end
+
+ def to_plain_text
+ if respond_to?(:attachable_plain_text_representation)
+ attachable_plain_text_representation(caption)
+ else
+ caption.to_s
+ end
+ end
+
+ def to_html
+ HtmlConversion.node_to_html(node)
+ end
+
+ def to_s
+ to_html
+ end
+
+ def inspect
+ "#<#{self.class.name} attachable=#{attachable.inspect}>"
+ end
+
+ private
+ def node_attributes
+ @node_attributes ||= ATTRIBUTES.map { |name| [ name.underscore, node[name] ] }.to_h.compact
+ end
+
+ def attachable_attributes
+ @attachable_attributes ||= (attachable.try(:to_rich_text_attributes) || {}).stringify_keys
+ end
+
+ def sgid_attributes
+ @sgid_attributes ||= node_attributes.slice("sgid").presence || attachable_attributes.slice("sgid")
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/attachment_gallery.rb b/actiontext/lib/action_text/attachment_gallery.rb
new file mode 100644
index 0000000000..45afbff058
--- /dev/null
+++ b/actiontext/lib/action_text/attachment_gallery.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module ActionText
+ class AttachmentGallery
+ include ActiveModel::Model
+
+ class << self
+ def fragment_by_canonicalizing_attachment_galleries(content)
+ fragment_by_replacing_attachment_gallery_nodes(content) do |node|
+ "<#{TAG_NAME}>#{node.inner_html}</#{TAG_NAME}>"
+ end
+ end
+
+ def fragment_by_replacing_attachment_gallery_nodes(content)
+ Fragment.wrap(content).update do |source|
+ find_attachment_gallery_nodes(source).each do |node|
+ node.replace(yield(node).to_s)
+ end
+ end
+ end
+
+ def find_attachment_gallery_nodes(content)
+ Fragment.wrap(content).find_all(SELECTOR).select do |node|
+ node.children.all? do |child|
+ if child.text?
+ child.text =~ /\A(\n|\ )*\z/
+ else
+ child.matches? ATTACHMENT_SELECTOR
+ end
+ end
+ end
+ end
+
+ def from_node(node)
+ new(node)
+ end
+ end
+
+ attr_reader :node
+
+ def initialize(node)
+ @node = node
+ end
+
+ def attachments
+ @attachments ||= node.css(ATTACHMENT_SELECTOR).map do |node|
+ ActionText::Attachment.from_node(node).with_full_attributes
+ end
+ end
+
+ def size
+ attachments.size
+ end
+
+ def inspect
+ "#<#{self.class.name} size=#{size.inspect}>"
+ end
+
+ TAG_NAME = "div"
+ ATTACHMENT_SELECTOR = "#{ActionText::Attachment::SELECTOR}[presentation=gallery]"
+ SELECTOR = "#{TAG_NAME}:has(#{ATTACHMENT_SELECTOR} + #{ATTACHMENT_SELECTOR})"
+
+ private_constant :TAG_NAME, :ATTACHMENT_SELECTOR, :SELECTOR
+ end
+end
diff --git a/actiontext/lib/action_text/attachments/caching.rb b/actiontext/lib/action_text/attachments/caching.rb
new file mode 100644
index 0000000000..7c727bfc26
--- /dev/null
+++ b/actiontext/lib/action_text/attachments/caching.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module ActionText
+ module Attachments
+ module Caching
+ def cache_key(*args)
+ [self.class.name, cache_digest, *attachable.cache_key(*args)].join("/")
+ end
+
+ private
+ def cache_digest
+ Digest::SHA256.hexdigest(node.to_s)
+ end
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/attachments/minification.rb b/actiontext/lib/action_text/attachments/minification.rb
new file mode 100644
index 0000000000..edc8f876d6
--- /dev/null
+++ b/actiontext/lib/action_text/attachments/minification.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionText
+ module Attachments
+ module Minification
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def fragment_by_minifying_attachments(content)
+ Fragment.wrap(content).replace(ActionText::Attachment::SELECTOR) do |node|
+ node.tap { |n| n.inner_html = "" }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/attachments/trix_conversion.rb b/actiontext/lib/action_text/attachments/trix_conversion.rb
new file mode 100644
index 0000000000..24937d6c22
--- /dev/null
+++ b/actiontext/lib/action_text/attachments/trix_conversion.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module ActionText
+ module Attachments
+ module TrixConversion
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def fragment_by_converting_trix_attachments(content)
+ Fragment.wrap(content).replace(TrixAttachment::SELECTOR) do |node|
+ from_trix_attachment(TrixAttachment.new(node))
+ end
+ end
+
+ def from_trix_attachment(trix_attachment)
+ from_attributes(trix_attachment.attributes)
+ end
+ end
+
+ def to_trix_attachment(content = trix_attachment_content)
+ attributes = full_attributes.dup
+ attributes["content"] = content if content
+ TrixAttachment.from_attributes(attributes)
+ end
+
+ private
+ def trix_attachment_content
+ if partial_path = attachable.try(:to_trix_content_attachment_partial_path)
+ ActionText::Content.renderer.render(partial: partial_path, object: self, as: model_name.element)
+ end
+ end
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/attribute.rb b/actiontext/lib/action_text/attribute.rb
new file mode 100644
index 0000000000..ddc6822a4c
--- /dev/null
+++ b/actiontext/lib/action_text/attribute.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module ActionText
+ module Attribute
+ extend ActiveSupport::Concern
+
+ class_methods do
+ # Provides access to a dependent RichText model that holds the body and attachments for a single named rich text attribute.
+ # This dependent attribute is lazily instantiated and will be auto-saved when it's been changed. Example:
+ #
+ # class Message < ActiveRecord::Base
+ # has_rich_text :content
+ # end
+ #
+ # message = Message.create!(content: "<h1>Funny times!</h1>")
+ # message.content.to_s # => "<h1>Funny times!</h1>"
+ # message.content.to_plain_text # => "Funny times!"
+ #
+ # The dependent RichText model will also automatically process attachments links as sent via the Trix-powered editor.
+ # These attachments are associated with the RichText model using Active Storage.
+ #
+ # If you wish to preload the dependent RichText model, you can use the named scope:
+ #
+ # Message.all.with_rich_text_content # Avoids N+1 queries when you just want the body, not the attachments.
+ # Message.all.with_rich_text_content_and_embeds # Avoids N+1 queries when you just want the body and attachments.
+ def has_rich_text(name)
+ class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def #{name}
+ rich_text_#{name} || build_rich_text_#{name}
+ end
+
+ def #{name}=(body)
+ self.#{name}.body = body
+ end
+ CODE
+
+ has_one :"rich_text_#{name}", -> { where(name: name) },
+ class_name: "ActionText::RichText", as: :record, inverse_of: :record, autosave: true, dependent: :destroy
+
+ scope :"with_rich_text_#{name}", -> { includes("rich_text_#{name}") }
+ scope :"with_rich_text_#{name}_and_embeds", -> { includes("rich_text_#{name}": { embeds_attachments: :blob }) }
+ end
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/content.rb b/actiontext/lib/action_text/content.rb
new file mode 100644
index 0000000000..16bc6fe031
--- /dev/null
+++ b/actiontext/lib/action_text/content.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/attribute_accessors_per_thread"
+
+module ActionText
+ class Content
+ include Serialization
+
+ thread_cattr_accessor :renderer
+
+ attr_reader :fragment
+
+ delegate :blank?, :empty?, :html_safe, :present?, to: :to_html # Delegating to to_html to avoid including the layout
+
+ class << self
+ def fragment_by_canonicalizing_content(content)
+ fragment = ActionText::Attachment.fragment_by_canonicalizing_attachments(content)
+ fragment = ActionText::AttachmentGallery.fragment_by_canonicalizing_attachment_galleries(fragment)
+ fragment
+ end
+ end
+
+ def initialize(content = nil, options = {})
+ options.with_defaults! canonicalize: true
+
+ if options[:canonicalize]
+ @fragment = self.class.fragment_by_canonicalizing_content(content)
+ else
+ @fragment = ActionText::Fragment.wrap(content)
+ end
+ end
+
+ def links
+ @links ||= fragment.find_all("a[href]").map { |a| a["href"] }.uniq
+ end
+
+ def attachments
+ @attachments ||= attachment_nodes.map do |node|
+ attachment_for_node(node)
+ end
+ end
+
+ def attachment_galleries
+ @attachment_galleries ||= attachment_gallery_nodes.map do |node|
+ attachment_gallery_for_node(node)
+ end
+ end
+
+ def gallery_attachments
+ @gallery_attachments ||= attachment_galleries.flat_map(&:attachments)
+ end
+
+ def attachables
+ @attachables ||= attachment_nodes.map do |node|
+ ActionText::Attachable.from_node(node)
+ end
+ end
+
+ def append_attachables(attachables)
+ attachments = ActionText::Attachment.from_attachables(attachables)
+ self.class.new([self.to_s.presence, *attachments].compact.join("\n"))
+ end
+
+ def render_attachments(**options, &block)
+ content = fragment.replace(ActionText::Attachment::SELECTOR) do |node|
+ block.call(attachment_for_node(node, **options))
+ end
+ self.class.new(content, canonicalize: false)
+ end
+
+ def render_attachment_galleries(&block)
+ content = ActionText::AttachmentGallery.fragment_by_replacing_attachment_gallery_nodes(fragment) do |node|
+ block.call(attachment_gallery_for_node(node))
+ end
+ self.class.new(content, canonicalize: false)
+ end
+
+ def to_plain_text
+ render_attachments(with_full_attributes: false, &:to_plain_text).fragment.to_plain_text
+ end
+
+ def to_trix_html
+ render_attachments(&:to_trix_attachment).to_html
+ end
+
+ def to_html
+ fragment.to_html
+ end
+
+ def to_rendered_html_with_layout
+ renderer.render(partial: "action_text/content/layout", locals: { content: self })
+ end
+
+ def to_s
+ to_rendered_html_with_layout
+ end
+
+ def as_json(*)
+ to_html
+ end
+
+ def inspect
+ "#<#{self.class.name} #{to_s.truncate(25).inspect}>"
+ end
+
+ def ==(other)
+ if other.is_a?(self.class)
+ to_s == other.to_s
+ end
+ end
+
+ private
+ def attachment_nodes
+ @attachment_nodes ||= fragment.find_all(ActionText::Attachment::SELECTOR)
+ end
+
+ def attachment_gallery_nodes
+ @attachment_gallery_nodes ||= ActionText::AttachmentGallery.find_attachment_gallery_nodes(fragment)
+ end
+
+ def attachment_for_node(node, with_full_attributes: true)
+ attachment = ActionText::Attachment.from_node(node)
+ with_full_attributes ? attachment.with_full_attributes : attachment
+ end
+
+ def attachment_gallery_for_node(node)
+ ActionText::AttachmentGallery.from_node(node)
+ end
+ end
+end
+
+ActiveSupport.run_load_hooks :action_text_content, ActionText::Content
diff --git a/actiontext/lib/action_text/engine.rb b/actiontext/lib/action_text/engine.rb
new file mode 100644
index 0000000000..51ff5b575b
--- /dev/null
+++ b/actiontext/lib/action_text/engine.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require "rails"
+require "action_controller/railtie"
+require "active_record/railtie"
+require "active_storage/engine"
+
+require "action_text"
+
+module ActionText
+ class Engine < Rails::Engine
+ isolate_namespace ActionText
+ config.eager_load_namespaces << ActionText
+
+ initializer "action_text.attribute" do
+ ActiveSupport.on_load(:active_record) do
+ include ActionText::Attribute
+ end
+ end
+
+ initializer "action_text.attachable" do
+ ActiveSupport.on_load(:active_storage_blob) do
+ include ActionText::Attachable
+
+ def previewable_attachable?
+ representable?
+ end
+ end
+ end
+
+ initializer "action_text.helper" do
+ ActiveSupport.on_load(:action_controller_base) do
+ helper ActionText::Engine.helpers
+ end
+ end
+
+ initializer "action_text.renderer" do |app|
+ app.executor.to_run { ActionText::Content.renderer = ApplicationController.renderer }
+ app.executor.to_complete { ActionText::Content.renderer = ApplicationController.renderer }
+
+ ActiveSupport.on_load(:action_text_content) do
+ self.renderer = ApplicationController.renderer
+ end
+
+ ActiveSupport.on_load(:action_controller_base) do
+ before_action { ActionText::Content.renderer = ApplicationController.renderer.new(request.env) }
+ end
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/fragment.rb b/actiontext/lib/action_text/fragment.rb
new file mode 100644
index 0000000000..af276b2b26
--- /dev/null
+++ b/actiontext/lib/action_text/fragment.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module ActionText
+ class Fragment
+ class << self
+ def wrap(fragment_or_html)
+ case fragment_or_html
+ when self
+ fragment_or_html
+ when Nokogiri::HTML::DocumentFragment
+ new(fragment_or_html)
+ else
+ from_html(fragment_or_html)
+ end
+ end
+
+ def from_html(html)
+ new(ActionText::HtmlConversion.fragment_for_html(html.to_s.strip))
+ end
+ end
+
+ attr_reader :source
+
+ def initialize(source)
+ @source = source
+ end
+
+ def find_all(selector)
+ source.css(selector)
+ end
+
+ def update
+ yield source = self.source.clone
+ self.class.new(source)
+ end
+
+ def replace(selector)
+ update do |source|
+ source.css(selector).each do |node|
+ node.replace(yield(node).to_s)
+ end
+ end
+ end
+
+ def to_plain_text
+ @plain_text ||= PlainTextConversion.node_to_plain_text(source)
+ end
+
+ def to_html
+ @html ||= HtmlConversion.node_to_html(source)
+ end
+
+ def to_s
+ to_html
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/gem_version.rb b/actiontext/lib/action_text/gem_version.rb
new file mode 100644
index 0000000000..ecd32d5f69
--- /dev/null
+++ b/actiontext/lib/action_text/gem_version.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActionText
+ # Returns the currently-loaded version of Action Text 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 = "beta3"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/actiontext/lib/action_text/html_conversion.rb b/actiontext/lib/action_text/html_conversion.rb
new file mode 100644
index 0000000000..1e1062ea3f
--- /dev/null
+++ b/actiontext/lib/action_text/html_conversion.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module ActionText
+ module HtmlConversion
+ extend self
+
+ def node_to_html(node)
+ node.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_HTML)
+ end
+
+ def fragment_for_html(html)
+ document.fragment(html)
+ end
+
+ def create_element(tag_name, attributes = {})
+ document.create_element(tag_name, attributes)
+ end
+
+ private
+ def document
+ Nokogiri::HTML::Document.new.tap { |doc| doc.encoding = "UTF-8" }
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/plain_text_conversion.rb b/actiontext/lib/action_text/plain_text_conversion.rb
new file mode 100644
index 0000000000..0eb4e2e7da
--- /dev/null
+++ b/actiontext/lib/action_text/plain_text_conversion.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+module ActionText
+ module PlainTextConversion
+ extend self
+
+ def node_to_plain_text(node)
+ remove_trailing_newlines(plain_text_for_node(node))
+ end
+
+ private
+ def plain_text_for_node(node, index = 0)
+ if respond_to?(plain_text_method_for_node(node), true)
+ send(plain_text_method_for_node(node), node, index)
+ else
+ plain_text_for_node_children(node)
+ end
+ end
+
+ def plain_text_for_node_children(node)
+ node.children.each_with_index.map do |child, index|
+ plain_text_for_node(child, index)
+ end.compact.join("")
+ end
+
+ def plain_text_method_for_node(node)
+ :"plain_text_for_#{node.name}_node"
+ end
+
+ def plain_text_for_block(node, index = 0)
+ "#{remove_trailing_newlines(plain_text_for_node_children(node))}\n\n"
+ end
+
+ %i[ h1 p ul ol ].each do |element|
+ alias_method :"plain_text_for_#{element}_node", :plain_text_for_block
+ end
+
+ def plain_text_for_br_node(node, index)
+ "\n"
+ end
+
+ def plain_text_for_text_node(node, index)
+ remove_trailing_newlines(node.text)
+ end
+
+ def plain_text_for_div_node(node, index)
+ "#{remove_trailing_newlines(plain_text_for_node_children(node))}\n"
+ end
+
+ def plain_text_for_figcaption_node(node, index)
+ "[#{remove_trailing_newlines(plain_text_for_node_children(node))}]"
+ end
+
+ def plain_text_for_blockquote_node(node, index)
+ text = plain_text_for_block(node)
+ text.sub(/\A(\s*)(.+?)(\s*)\Z/m, '\1“\2â€\3')
+ end
+
+ def plain_text_for_li_node(node, index)
+ bullet = bullet_for_li_node(node, index)
+ text = remove_trailing_newlines(plain_text_for_node_children(node))
+ "#{bullet} #{text}\n"
+ end
+
+ def remove_trailing_newlines(text)
+ text.chomp("")
+ end
+
+ def bullet_for_li_node(node, index)
+ if list_node_name_for_li_node(node) == "ol"
+ "#{index + 1}."
+ else
+ "•"
+ end
+ end
+
+ def list_node_name_for_li_node(node)
+ node.ancestors.lazy.map(&:name).grep(/^[uo]l$/).first
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/serialization.rb b/actiontext/lib/action_text/serialization.rb
new file mode 100644
index 0000000000..8ecf8c9157
--- /dev/null
+++ b/actiontext/lib/action_text/serialization.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module ActionText
+ module Serialization
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def load(content)
+ new(content) if content
+ end
+
+ def dump(content)
+ case content
+ when nil
+ nil
+ when self
+ content.to_html
+ else
+ new(content).to_html
+ end
+ end
+ end
+
+ # Marshal compatibility
+
+ class_methods do
+ alias_method :_load, :load
+ end
+
+ def _dump(*)
+ self.class.dump(self)
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/trix_attachment.rb b/actiontext/lib/action_text/trix_attachment.rb
new file mode 100644
index 0000000000..c16c1c090d
--- /dev/null
+++ b/actiontext/lib/action_text/trix_attachment.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+module ActionText
+ class TrixAttachment
+ TAG_NAME = "figure"
+ SELECTOR = "[data-trix-attachment]"
+
+ COMPOSED_ATTRIBUTES = %w( caption presentation )
+ ATTRIBUTES = %w( sgid contentType url href filename filesize width height previewable content ) + COMPOSED_ATTRIBUTES
+ ATTRIBUTE_TYPES = {
+ "previewable" => ->(value) { value.to_s == "true" },
+ "filesize" => ->(value) { Integer(value.to_s) rescue value },
+ "width" => ->(value) { Integer(value.to_s) rescue nil },
+ "height" => ->(value) { Integer(value.to_s) rescue nil },
+ :default => ->(value) { value.to_s }
+ }
+
+ class << self
+ def from_attributes(attributes)
+ attributes = process_attributes(attributes)
+
+ trix_attachment_attributes = attributes.except(*COMPOSED_ATTRIBUTES)
+ trix_attributes = attributes.slice(*COMPOSED_ATTRIBUTES)
+
+ node = ActionText::HtmlConversion.create_element(TAG_NAME)
+ node["data-trix-attachment"] = JSON.generate(trix_attachment_attributes)
+ node["data-trix-attributes"] = JSON.generate(trix_attributes) if trix_attributes.any?
+
+ new(node)
+ end
+
+ private
+ def process_attributes(attributes)
+ typecast_attribute_values(transform_attribute_keys(attributes))
+ end
+
+ def transform_attribute_keys(attributes)
+ attributes.transform_keys { |key| key.to_s.underscore.camelize(:lower) }
+ end
+
+ def typecast_attribute_values(attributes)
+ attributes.map do |key, value|
+ typecast = ATTRIBUTE_TYPES[key] || ATTRIBUTE_TYPES[:default]
+ [key, typecast.call(value)]
+ end.to_h
+ end
+ end
+
+ attr_reader :node
+
+ def initialize(node)
+ @node = node
+ end
+
+ def attributes
+ @attributes ||= attachment_attributes.merge(composed_attributes).slice(*ATTRIBUTES)
+ end
+
+ def to_html
+ ActionText::HtmlConversion.node_to_html(node)
+ end
+
+ def to_s
+ to_html
+ end
+
+ private
+ def attachment_attributes
+ read_json_object_attribute("data-trix-attachment")
+ end
+
+ def composed_attributes
+ read_json_object_attribute("data-trix-attributes")
+ end
+
+ def read_json_object_attribute(name)
+ read_json_attribute(name) || {}
+ end
+
+ def read_json_attribute(name)
+ if value = node[name]
+ begin
+ JSON.parse(value)
+ rescue => e
+ Rails.logger.error "[#{self.class.name}] Couldn't parse JSON #{value} from NODE #{node.inspect}"
+ Rails.logger.error "[#{self.class.name}] Failed with #{e.class}: #{e.backtrace}"
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/actiontext/lib/action_text/version.rb b/actiontext/lib/action_text/version.rb
new file mode 100644
index 0000000000..ed72859fa4
--- /dev/null
+++ b/actiontext/lib/action_text/version.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require_relative "gem_version"
+
+module ActionText
+ # Returns the currently-loaded version of Action Text as a <tt>Gem::Version</tt>.
+ def self.version
+ gem_version
+ end
+end
diff --git a/actiontext/lib/tasks/actiontext.rake b/actiontext/lib/tasks/actiontext.rake
new file mode 100644
index 0000000000..4f90e4930c
--- /dev/null
+++ b/actiontext/lib/tasks/actiontext.rake
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+namespace :action_text do
+ # Prevent migration installation task from showing up twice.
+ Rake::Task["install:migrations"].clear_comments
+
+ desc "Copy over the migration, stylesheet, and JavaScript files"
+ task install: %w( environment run_installer copy_migrations )
+
+ task :run_installer do
+ installer_template = File.expand_path("../templates/installer.rb", __dir__)
+ system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{installer_template}"
+ end
+
+ task :copy_migrations do
+ Rake::Task["active_storage:install:migrations"].invoke
+ Rake::Task["railties:install:migrations"].reenable # Otherwise you can't run 2 migration copy tasks in one invocation
+ Rake::Task["action_text:install:migrations"].invoke
+ end
+end
diff --git a/actiontext/lib/templates/actiontext.scss b/actiontext/lib/templates/actiontext.scss
new file mode 100644
index 0000000000..7cb26e74ac
--- /dev/null
+++ b/actiontext/lib/templates/actiontext.scss
@@ -0,0 +1,36 @@
+//
+// Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and
+// the trix-editor content (whether displayed or under editing). Feel free to incorporate this
+// inclusion directly in any other asset bundle and remove this file.
+//
+//= require trix/dist/trix
+
+// We need to override trix.css’s image gallery styles to accommodate the
+// <action-text-attachment> element we wrap around attachments. Otherwise,
+// images in galleries will be squished by the max-width: 33%; rule.
+.trix-content {
+ .attachment-gallery {
+ > action-text-attachment,
+ > .attachment {
+ flex: 1 0 33%;
+ padding: 0 0.5em;
+ max-width: 33%;
+ }
+
+ &.attachment-gallery--2,
+ &.attachment-gallery--4 {
+ > action-text-attachment,
+ > .attachment {
+ flex-basis: 50%;
+ max-width: 50%;
+ }
+ }
+ }
+
+ action-text-attachment {
+ .attachment {
+ padding: 0 !important;
+ max-width: 100% !important;
+ }
+ }
+}
diff --git a/actiontext/lib/templates/fixtures.yml b/actiontext/lib/templates/fixtures.yml
new file mode 100644
index 0000000000..8b371ea604
--- /dev/null
+++ b/actiontext/lib/templates/fixtures.yml
@@ -0,0 +1,4 @@
+# one:
+# record: name_of_fixture (ClassOfFixture)
+# name: content
+# body: <p>In a <i>million</i> stars!</p>
diff --git a/actiontext/lib/templates/installer.rb b/actiontext/lib/templates/installer.rb
new file mode 100644
index 0000000000..a8000eb9fc
--- /dev/null
+++ b/actiontext/lib/templates/installer.rb
@@ -0,0 +1,32 @@
+require "pathname"
+require "json"
+
+APPLICATION_PACK_PATH = Pathname.new("app/javascript/packs/application.js")
+JS_PACKAGE_PATH = Pathname.new("#{__dir__}/../../package.json")
+
+JS_PACKAGE = JSON.load(JS_PACKAGE_PATH)
+JS_DEPENDENCIES = JS_PACKAGE["peerDependencies"].dup.merge \
+ JS_PACKAGE["name"] => "^#{JS_PACKAGE["version"]}"
+
+say "Copying actiontext.scss to app/assets/stylesheets"
+copy_file "#{__dir__}/actiontext.scss", "app/assets/stylesheets/actiontext.scss"
+
+say "Copying fixtures to test/fixtures/action_text/rich_texts.yml"
+copy_file "#{__dir__}/fixtures.yml", "test/fixtures/action_text/rich_texts.yml"
+
+say "Copying blob rendering partial to app/views/active_storage/blobs/_blob.html.erb"
+copy_file "#{__dir__}/../../app/views/active_storage/blobs/_blob.html.erb",
+ "app/views/active_storage/blobs/_blob.html.erb"
+
+say "Installing JavaScript dependencies"
+run "yarn add #{JS_DEPENDENCIES.map { |name, version| "#{name}@#{version}" }.join(" ")}"
+
+if APPLICATION_PACK_PATH.exist?
+ JS_DEPENDENCIES.keys.each do |name|
+ line = %[require("#{name}")]
+ unless APPLICATION_PACK_PATH.read.include? line
+ say "Adding #{name} to #{APPLICATION_PACK_PATH}"
+ append_to_file APPLICATION_PACK_PATH, "\n#{line}"
+ end
+ end
+end
diff --git a/actiontext/package.json b/actiontext/package.json
new file mode 100644
index 0000000000..f67fea1642
--- /dev/null
+++ b/actiontext/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "@rails/actiontext",
+ "version": "6.0.0-beta3",
+ "description": "Edit and display rich text in Rails applications",
+ "main": "app/javascript/actiontext/index.js",
+ "files": [
+ "app/javascript/actiontext/*.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": "Basecamp, LLC",
+ "contributors": [
+ "Javan Makhmali <javan@javan.us>",
+ "Sam Stephenson <sstephenson@gmail.com>"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@rails/activestorage": "^6.0.0-alpha"
+ },
+ "peerDependencies": {
+ "trix": "^1.0.0"
+ }
+}
diff --git a/actiontext/test/dummy/.babelrc b/actiontext/test/dummy/.babelrc
new file mode 100644
index 0000000000..ded31c0d80
--- /dev/null
+++ b/actiontext/test/dummy/.babelrc
@@ -0,0 +1,18 @@
+{
+ "presets": [
+ ["env", {
+ "modules": false,
+ "targets": {
+ "browsers": "> 1%",
+ "uglify": true
+ },
+ "useBuiltIns": true
+ }]
+ ],
+
+ "plugins": [
+ "syntax-dynamic-import",
+ "transform-object-rest-spread",
+ ["transform-class-properties", { "spec": true }]
+ ]
+}
diff --git a/actiontext/test/dummy/.postcssrc.yml b/actiontext/test/dummy/.postcssrc.yml
new file mode 100644
index 0000000000..150dac3c6c
--- /dev/null
+++ b/actiontext/test/dummy/.postcssrc.yml
@@ -0,0 +1,3 @@
+plugins:
+ postcss-import: {}
+ postcss-cssnext: {}
diff --git a/actiontext/test/dummy/Rakefile b/actiontext/test/dummy/Rakefile
new file mode 100644
index 0000000000..e85f913914
--- /dev/null
+++ b/actiontext/test/dummy/Rakefile
@@ -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/actiontext/test/dummy/app/assets/config/manifest.js b/actiontext/test/dummy/app/assets/config/manifest.js
new file mode 100644
index 0000000000..b16e53d6d5
--- /dev/null
+++ b/actiontext/test/dummy/app/assets/config/manifest.js
@@ -0,0 +1,3 @@
+//= link_tree ../images
+//= link_directory ../javascripts .js
+//= link_directory ../stylesheets .css
diff --git a/actiontext/test/dummy/app/assets/images/.keep b/actiontext/test/dummy/app/assets/images/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actiontext/test/dummy/app/assets/images/.keep
diff --git a/actiontext/test/dummy/app/assets/stylesheets/application.css b/actiontext/test/dummy/app/assets/stylesheets/application.css
new file mode 100644
index 0000000000..e4b10eb997
--- /dev/null
+++ b/actiontext/test/dummy/app/assets/stylesheets/application.css
@@ -0,0 +1,16 @@
+/*
+ * 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 trix/dist/trix.css
+ *= require_tree .
+ *= require_self
+ */
diff --git a/actiontext/test/dummy/app/assets/stylesheets/messages.css b/actiontext/test/dummy/app/assets/stylesheets/messages.css
new file mode 100644
index 0000000000..afad32db02
--- /dev/null
+++ b/actiontext/test/dummy/app/assets/stylesheets/messages.css
@@ -0,0 +1,4 @@
+/*
+ Place all the styles related to the matching controller here.
+ They will automatically be included in application.css.
+*/
diff --git a/actiontext/test/dummy/app/assets/stylesheets/scaffold.css b/actiontext/test/dummy/app/assets/stylesheets/scaffold.css
new file mode 100644
index 0000000000..cd4f3de38d
--- /dev/null
+++ b/actiontext/test/dummy/app/assets/stylesheets/scaffold.css
@@ -0,0 +1,80 @@
+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;
+}
+
+pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+}
+
+a {
+ color: #000;
+}
+
+a:visited {
+ color: #666;
+}
+
+a:hover {
+ color: #fff;
+ background-color: #000;
+}
+
+th {
+ padding-bottom: 5px;
+}
+
+td {
+ padding: 0 5px 7px;
+}
+
+div.field,
+div.actions {
+ margin-bottom: 10px;
+}
+
+#notice {
+ color: green;
+}
+
+.field_with_errors {
+ padding: 2px;
+ background-color: red;
+ display: table;
+}
+
+#error_explanation {
+ width: 450px;
+ border: 2px solid red;
+ padding: 7px 7px 0;
+ margin-bottom: 20px;
+ background-color: #f0f0f0;
+}
+
+#error_explanation h2 {
+ text-align: left;
+ font-weight: bold;
+ padding: 5px 5px 5px 15px;
+ font-size: 12px;
+ margin: -7px -7px 0;
+ background-color: #c00;
+ color: #fff;
+}
+
+#error_explanation ul li {
+ font-size: 12px;
+ list-style: square;
+}
+
+label {
+ display: block;
+}
diff --git a/actiontext/test/dummy/app/channels/application_cable/channel.rb b/actiontext/test/dummy/app/channels/application_cable/channel.rb
new file mode 100644
index 0000000000..d672697283
--- /dev/null
+++ b/actiontext/test/dummy/app/channels/application_cable/channel.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/actiontext/test/dummy/app/channels/application_cable/connection.rb b/actiontext/test/dummy/app/channels/application_cable/connection.rb
new file mode 100644
index 0000000000..0ff5442f47
--- /dev/null
+++ b/actiontext/test/dummy/app/channels/application_cable/connection.rb
@@ -0,0 +1,4 @@
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ end
+end
diff --git a/actiontext/test/dummy/app/controllers/application_controller.rb b/actiontext/test/dummy/app/controllers/application_controller.rb
new file mode 100644
index 0000000000..09705d12ab
--- /dev/null
+++ b/actiontext/test/dummy/app/controllers/application_controller.rb
@@ -0,0 +1,2 @@
+class ApplicationController < ActionController::Base
+end
diff --git a/actiontext/test/dummy/app/controllers/concerns/.keep b/actiontext/test/dummy/app/controllers/concerns/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actiontext/test/dummy/app/controllers/concerns/.keep
diff --git a/actiontext/test/dummy/app/controllers/messages_controller.rb b/actiontext/test/dummy/app/controllers/messages_controller.rb
new file mode 100644
index 0000000000..81c79a2d1e
--- /dev/null
+++ b/actiontext/test/dummy/app/controllers/messages_controller.rb
@@ -0,0 +1,58 @@
+class MessagesController < ApplicationController
+ before_action :set_message, only: [:show, :edit, :update, :destroy]
+
+ # GET /messages
+ def index
+ @messages = Message.all
+ end
+
+ # GET /messages/1
+ def show
+ end
+
+ # GET /messages/new
+ def new
+ @message = Message.new
+ end
+
+ # GET /messages/1/edit
+ def edit
+ end
+
+ # POST /messages
+ def create
+ @message = Message.new(message_params)
+
+ if @message.save
+ redirect_to @message, notice: 'Message was successfully created.'
+ else
+ render :new
+ end
+ end
+
+ # PATCH/PUT /messages/1
+ def update
+ if @message.update(message_params)
+ redirect_to @message, notice: 'Message was successfully updated.'
+ else
+ render :edit
+ end
+ end
+
+ # DELETE /messages/1
+ def destroy
+ @message.destroy
+ redirect_to messages_url, notice: 'Message was successfully destroyed.'
+ end
+
+ private
+ # Use callbacks to share common setup or constraints between actions.
+ def set_message
+ @message = Message.find(params[:id])
+ end
+
+ # Only allow a trusted parameter "white list" through.
+ def message_params
+ params.require(:message).permit(:subject, :content)
+ end
+end
diff --git a/actiontext/test/dummy/app/helpers/application_helper.rb b/actiontext/test/dummy/app/helpers/application_helper.rb
new file mode 100644
index 0000000000..de6be7945c
--- /dev/null
+++ b/actiontext/test/dummy/app/helpers/application_helper.rb
@@ -0,0 +1,2 @@
+module ApplicationHelper
+end
diff --git a/actiontext/test/dummy/app/helpers/messages_helper.rb b/actiontext/test/dummy/app/helpers/messages_helper.rb
new file mode 100644
index 0000000000..f1bca9f6ca
--- /dev/null
+++ b/actiontext/test/dummy/app/helpers/messages_helper.rb
@@ -0,0 +1,2 @@
+module MessagesHelper
+end
diff --git a/actiontext/test/dummy/app/javascript/packs/application.js b/actiontext/test/dummy/app/javascript/packs/application.js
new file mode 100644
index 0000000000..13ac17ed58
--- /dev/null
+++ b/actiontext/test/dummy/app/javascript/packs/application.js
@@ -0,0 +1 @@
+import "@rails/actiontext"
diff --git a/actiontext/test/dummy/app/jobs/application_job.rb b/actiontext/test/dummy/app/jobs/application_job.rb
new file mode 100644
index 0000000000..a009ace51c
--- /dev/null
+++ b/actiontext/test/dummy/app/jobs/application_job.rb
@@ -0,0 +1,2 @@
+class ApplicationJob < ActiveJob::Base
+end
diff --git a/actiontext/test/dummy/app/mailers/application_mailer.rb b/actiontext/test/dummy/app/mailers/application_mailer.rb
new file mode 100644
index 0000000000..286b2239d1
--- /dev/null
+++ b/actiontext/test/dummy/app/mailers/application_mailer.rb
@@ -0,0 +1,4 @@
+class ApplicationMailer < ActionMailer::Base
+ default from: 'from@example.com'
+ layout 'mailer'
+end
diff --git a/actiontext/test/dummy/app/models/application_record.rb b/actiontext/test/dummy/app/models/application_record.rb
new file mode 100644
index 0000000000..10a4cba84d
--- /dev/null
+++ b/actiontext/test/dummy/app/models/application_record.rb
@@ -0,0 +1,3 @@
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
diff --git a/actiontext/test/dummy/app/models/concerns/.keep b/actiontext/test/dummy/app/models/concerns/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actiontext/test/dummy/app/models/concerns/.keep
diff --git a/actiontext/test/dummy/app/models/message.rb b/actiontext/test/dummy/app/models/message.rb
new file mode 100644
index 0000000000..7bce50753c
--- /dev/null
+++ b/actiontext/test/dummy/app/models/message.rb
@@ -0,0 +1,7 @@
+class Message < ApplicationRecord
+ has_rich_text :content
+ has_rich_text :body
+
+ has_one :review
+ accepts_nested_attributes_for :review
+end
diff --git a/actiontext/test/dummy/app/models/page.rb b/actiontext/test/dummy/app/models/page.rb
new file mode 100644
index 0000000000..dfebf282a7
--- /dev/null
+++ b/actiontext/test/dummy/app/models/page.rb
@@ -0,0 +1,4 @@
+class Page < ApplicationRecord
+ include ActionText::Attachable
+end
+
diff --git a/actiontext/test/dummy/app/models/person.rb b/actiontext/test/dummy/app/models/person.rb
new file mode 100644
index 0000000000..0ded356d5b
--- /dev/null
+++ b/actiontext/test/dummy/app/models/person.rb
@@ -0,0 +1,7 @@
+class Person < ApplicationRecord
+ include ActionText::Attachable
+
+ def to_trix_content_attachment_partial_path
+ "people/trix_content_attachment"
+ end
+end
diff --git a/actiontext/test/dummy/app/models/review.rb b/actiontext/test/dummy/app/models/review.rb
new file mode 100644
index 0000000000..e54a37685d
--- /dev/null
+++ b/actiontext/test/dummy/app/models/review.rb
@@ -0,0 +1,5 @@
+class Review < ApplicationRecord
+ belongs_to :message
+
+ has_rich_text :content
+end
diff --git a/actiontext/test/dummy/app/views/layouts/application.html.erb b/actiontext/test/dummy/app/views/layouts/application.html.erb
new file mode 100644
index 0000000000..f221f512b7
--- /dev/null
+++ b/actiontext/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_pack_tag 'application' %>
+ </head>
+
+ <body>
+ <%= yield %>
+ </body>
+</html>
diff --git a/actiontext/test/dummy/app/views/layouts/mailer.html.erb b/actiontext/test/dummy/app/views/layouts/mailer.html.erb
new file mode 100644
index 0000000000..cbd34d2e9d
--- /dev/null
+++ b/actiontext/test/dummy/app/views/layouts/mailer.html.erb
@@ -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/actiontext/test/dummy/app/views/layouts/mailer.text.erb b/actiontext/test/dummy/app/views/layouts/mailer.text.erb
new file mode 100644
index 0000000000..37f0bddbd7
--- /dev/null
+++ b/actiontext/test/dummy/app/views/layouts/mailer.text.erb
@@ -0,0 +1 @@
+<%= yield %>
diff --git a/actiontext/test/dummy/app/views/messages/_form.html.erb b/actiontext/test/dummy/app/views/messages/_form.html.erb
new file mode 100644
index 0000000000..3b8a174884
--- /dev/null
+++ b/actiontext/test/dummy/app/views/messages/_form.html.erb
@@ -0,0 +1,27 @@
+<%= form_with(model: message, local: true) do |form| %>
+ <% if message.errors.any? %>
+ <div id="error_explanation">
+ <h2><%= pluralize(message.errors.count, "error") %> prohibited this message from being saved:</h2>
+
+ <ul>
+ <% message.errors.full_messages.each do |message| %>
+ <li><%= message %></li>
+ <% end %>
+ </ul>
+ </div>
+ <% end %>
+
+ <div class="field">
+ <%= form.label :subject %>
+ <%= form.text_field :subject %>
+ </div>
+
+ <div class="field">
+ <%= form.label :content %>
+ <%= form.rich_text_area :content, class: "trix-content" %>
+ </div>
+
+ <div class="actions">
+ <%= form.submit %>
+ </div>
+<% end %>
diff --git a/actiontext/test/dummy/app/views/messages/edit.html.erb b/actiontext/test/dummy/app/views/messages/edit.html.erb
new file mode 100644
index 0000000000..90ad68c788
--- /dev/null
+++ b/actiontext/test/dummy/app/views/messages/edit.html.erb
@@ -0,0 +1,6 @@
+<h1>Editing Message</h1>
+
+<%= render 'form', message: @message %>
+
+<%= link_to 'Show', @message %> |
+<%= link_to 'Back', messages_path %>
diff --git a/actiontext/test/dummy/app/views/messages/index.html.erb b/actiontext/test/dummy/app/views/messages/index.html.erb
new file mode 100644
index 0000000000..a8c97468c6
--- /dev/null
+++ b/actiontext/test/dummy/app/views/messages/index.html.erb
@@ -0,0 +1,29 @@
+<p id="notice"><%= notice %></p>
+
+<h1>Messages</h1>
+
+<table>
+ <thead>
+ <tr>
+ <th>Subject</th>
+ <th>Content</th>
+ <th colspan="3"></th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <% @messages.each do |message| %>
+ <tr>
+ <td><%= message.subject %></td>
+ <td><%= message.content %></td>
+ <td><%= link_to 'Show', message %></td>
+ <td><%= link_to 'Edit', edit_message_path(message) %></td>
+ <td><%= link_to 'Destroy', message, method: :delete, data: { confirm: 'Are you sure?' } %></td>
+ </tr>
+ <% end %>
+ </tbody>
+</table>
+
+<br>
+
+<%= link_to 'New Message', new_message_path %>
diff --git a/actiontext/test/dummy/app/views/messages/new.html.erb b/actiontext/test/dummy/app/views/messages/new.html.erb
new file mode 100644
index 0000000000..6cbd3b8ffe
--- /dev/null
+++ b/actiontext/test/dummy/app/views/messages/new.html.erb
@@ -0,0 +1,5 @@
+<h1>New Message</h1>
+
+<%= render 'form', message: @message %>
+
+<%= link_to 'Back', messages_path %>
diff --git a/actiontext/test/dummy/app/views/messages/show.html.erb b/actiontext/test/dummy/app/views/messages/show.html.erb
new file mode 100644
index 0000000000..25fad1efba
--- /dev/null
+++ b/actiontext/test/dummy/app/views/messages/show.html.erb
@@ -0,0 +1,13 @@
+<p id="notice"><%= notice %></p>
+
+<p>
+ <strong>Subject:</strong>
+ <%= @message.subject %>
+</p>
+
+<div class="trix-content">
+ <%= @message.content.html_safe %>
+</div>
+
+<%= link_to 'Edit', edit_message_path(@message) %> |
+<%= link_to 'Back', messages_path %>
diff --git a/actiontext/test/dummy/app/views/people/_trix_content_attachment.html.erb b/actiontext/test/dummy/app/views/people/_trix_content_attachment.html.erb
new file mode 100644
index 0000000000..7db2334126
--- /dev/null
+++ b/actiontext/test/dummy/app/views/people/_trix_content_attachment.html.erb
@@ -0,0 +1,3 @@
+<span class="mentionable-person" gid="<%= person.to_gid %>">
+ <%= person.name %>
+</span>
diff --git a/actiontext/test/dummy/bin/bundle b/actiontext/test/dummy/bin/bundle
new file mode 100755
index 0000000000..f19acf5b5c
--- /dev/null
+++ b/actiontext/test/dummy/bin/bundle
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+load Gem.bin_path('bundler', 'bundle')
diff --git a/actiontext/test/dummy/bin/rails b/actiontext/test/dummy/bin/rails
new file mode 100755
index 0000000000..0739660237
--- /dev/null
+++ b/actiontext/test/dummy/bin/rails
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path('../config/application', __dir__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/actiontext/test/dummy/bin/rake b/actiontext/test/dummy/bin/rake
new file mode 100755
index 0000000000..17240489f6
--- /dev/null
+++ b/actiontext/test/dummy/bin/rake
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/actiontext/test/dummy/bin/setup b/actiontext/test/dummy/bin/setup
new file mode 100755
index 0000000000..94fd4d7977
--- /dev/null
+++ b/actiontext/test/dummy/bin/setup
@@ -0,0 +1,36 @@
+#!/usr/bin/env ruby
+require 'fileutils'
+include FileUtils
+
+# path to your application root.
+APP_ROOT = File.expand_path('..', __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+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')
+
+ # Install JavaScript dependencies if using Yarn
+ # system('bin/yarn')
+
+ # 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! 'bin/rails db:setup'
+
+ 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/actiontext/test/dummy/bin/update b/actiontext/test/dummy/bin/update
new file mode 100755
index 0000000000..58bfaed518
--- /dev/null
+++ b/actiontext/test/dummy/bin/update
@@ -0,0 +1,31 @@
+#!/usr/bin/env ruby
+require 'fileutils'
+include FileUtils
+
+# path to your application root.
+APP_ROOT = File.expand_path('..', __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+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')
+
+ # Install JavaScript dependencies if using Yarn
+ # system('bin/yarn')
+
+ puts "\n== Updating database =="
+ system! 'bin/rails db:migrate'
+
+ 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/actiontext/test/dummy/bin/yarn b/actiontext/test/dummy/bin/yarn
new file mode 100755
index 0000000000..460dd565b4
--- /dev/null
+++ b/actiontext/test/dummy/bin/yarn
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+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/actiontext/test/dummy/config.ru b/actiontext/test/dummy/config.ru
new file mode 100644
index 0000000000..f7ba0b527b
--- /dev/null
+++ b/actiontext/test/dummy/config.ru
@@ -0,0 +1,5 @@
+# This file is used by Rack-based servers to start the application.
+
+require_relative 'config/environment'
+
+run Rails.application
diff --git a/actiontext/test/dummy/config/application.rb b/actiontext/test/dummy/config/application.rb
new file mode 100644
index 0000000000..79eeb6ba0e
--- /dev/null
+++ b/actiontext/test/dummy/config/application.rb
@@ -0,0 +1,19 @@
+require_relative 'boot'
+
+require 'rails/all'
+
+Bundler.require(*Rails.groups)
+require "action_text"
+
+module Dummy
+ class Application < Rails::Application
+ # Initialize configuration defaults for originally generated Rails version.
+ config.load_defaults 6.0
+
+ # 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.
+ end
+end
+
diff --git a/actiontext/test/dummy/config/boot.rb b/actiontext/test/dummy/config/boot.rb
new file mode 100644
index 0000000000..c9aef85d40
--- /dev/null
+++ b/actiontext/test/dummy/config/boot.rb
@@ -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/actiontext/test/dummy/config/cable.yml b/actiontext/test/dummy/config/cable.yml
new file mode 100644
index 0000000000..1cd0f8363e
--- /dev/null
+++ b/actiontext/test/dummy/config/cable.yml
@@ -0,0 +1,10 @@
+development:
+ adapter: async
+
+test:
+ adapter: async
+
+production:
+ adapter: redis
+ url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
+ channel_prefix: dummy_production
diff --git a/actiontext/test/dummy/config/database.yml b/actiontext/test/dummy/config/database.yml
new file mode 100644
index 0000000000..0d02f24980
--- /dev/null
+++ b/actiontext/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/actiontext/test/dummy/config/environment.rb b/actiontext/test/dummy/config/environment.rb
new file mode 100644
index 0000000000..426333bb46
--- /dev/null
+++ b/actiontext/test/dummy/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the Rails application.
+require_relative 'application'
+
+# Initialize the Rails application.
+Rails.application.initialize!
diff --git a/actiontext/test/dummy/config/environments/development.rb b/actiontext/test/dummy/config/environments/development.rb
new file mode 100644
index 0000000000..f09b9a00c4
--- /dev/null
+++ b/actiontext/test/dummy/config/environments/development.rb
@@ -0,0 +1,63 @@
+Rails.application.configure do
+ # Verifies that versions and hashed value of the package contents in the project's package.json
+ # config.webpacker.check_yarn_integrity = true
+ # 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.
+ # 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
+
+ # Store uploaded files on the local file system (see config/storage.yml for options)
+ config.active_storage.service = :local
+
+ # Don't care if the mailer can't send.
+ config.action_mailer.raise_delivery_errors = false
+
+ config.action_mailer.perform_caching = false
+
+ # 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
+
+ # Highlight code that triggered database queries in logs.
+ config.active_record.verbose_query_logs = true
+
+ # 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/actiontext/test/dummy/config/environments/production.rb b/actiontext/test/dummy/config/environments/production.rb
new file mode 100644
index 0000000000..2aaa79f620
--- /dev/null
+++ b/actiontext/test/dummy/config/environments/production.rb
@@ -0,0 +1,96 @@
+Rails.application.configure do
+ # Verifies that versions and hashed value of the package contents in the project's package.json
+ config.webpacker.check_yarn_integrity = false
+ # 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
+
+ # 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.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
+
+ # Compress JavaScripts and CSS.
+ config.assets.js_compressor = :uglifier
+ # 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
+
+ # Store uploaded files on the local file system (see config/storage.yml for options)
+ config.active_storage.service = :local
+
+ # 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
+
+ # 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 = "dummy_#{Rails.env}"
+
+ 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
+
+ # 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/actiontext/test/dummy/config/environments/test.rb b/actiontext/test/dummy/config/environments/test.rb
new file mode 100644
index 0000000000..0a38fd3ce9
--- /dev/null
+++ b/actiontext/test/dummy/config/environments/test.rb
@@ -0,0 +1,46 @@
+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.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
+
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+
+ # Store uploaded files on the local file system in a temporary directory
+ config.active_storage.service = :test
+
+ 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
+
+ # Print deprecation notices to the stderr.
+ config.active_support.deprecation = :stderr
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
+end
diff --git a/actiontext/test/dummy/config/initializers/application_controller_renderer.rb b/actiontext/test/dummy/config/initializers/application_controller_renderer.rb
new file mode 100644
index 0000000000..89d2efab2b
--- /dev/null
+++ b/actiontext/test/dummy/config/initializers/application_controller_renderer.rb
@@ -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/actiontext/test/dummy/config/initializers/assets.rb b/actiontext/test/dummy/config/initializers/assets.rb
new file mode 100644
index 0000000000..4b828e80cb
--- /dev/null
+++ b/actiontext/test/dummy/config/initializers/assets.rb
@@ -0,0 +1,14 @@
+# 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/actiontext/test/dummy/config/initializers/backtrace_silencers.rb b/actiontext/test/dummy/config/initializers/backtrace_silencers.rb
new file mode 100644
index 0000000000..59385cdf37
--- /dev/null
+++ b/actiontext/test/dummy/config/initializers/backtrace_silencers.rb
@@ -0,0 +1,7 @@
+# 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/actiontext/test/dummy/config/initializers/content_security_policy.rb b/actiontext/test/dummy/config/initializers/content_security_policy.rb
new file mode 100644
index 0000000000..edde7f42b8
--- /dev/null
+++ b/actiontext/test/dummy/config/initializers/content_security_policy.rb
@@ -0,0 +1,22 @@
+# 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, :unsafe_inline
+
+# # Specify URI for violation reports
+# # policy.report_uri "/csp-violation-report-endpoint"
+# end
+
+# 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/actiontext/test/dummy/config/initializers/cookies_serializer.rb b/actiontext/test/dummy/config/initializers/cookies_serializer.rb
new file mode 100644
index 0000000000..5a6a32d371
--- /dev/null
+++ b/actiontext/test/dummy/config/initializers/cookies_serializer.rb
@@ -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/actiontext/test/dummy/config/initializers/filter_parameter_logging.rb b/actiontext/test/dummy/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000000..4a994e1e7b
--- /dev/null
+++ b/actiontext/test/dummy/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,4 @@
+# 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/actiontext/test/dummy/config/initializers/inflections.rb b/actiontext/test/dummy/config/initializers/inflections.rb
new file mode 100644
index 0000000000..ac033bf9dc
--- /dev/null
+++ b/actiontext/test/dummy/config/initializers/inflections.rb
@@ -0,0 +1,16 @@
+# 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/actiontext/test/dummy/config/initializers/mime_types.rb b/actiontext/test/dummy/config/initializers/mime_types.rb
new file mode 100644
index 0000000000..dc1899682b
--- /dev/null
+++ b/actiontext/test/dummy/config/initializers/mime_types.rb
@@ -0,0 +1,4 @@
+# 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/actiontext/test/dummy/config/initializers/wrap_parameters.rb b/actiontext/test/dummy/config/initializers/wrap_parameters.rb
new file mode 100644
index 0000000000..bbfc3961bf
--- /dev/null
+++ b/actiontext/test/dummy/config/initializers/wrap_parameters.rb
@@ -0,0 +1,14 @@
+# 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/actiontext/test/dummy/config/locales/en.yml b/actiontext/test/dummy/config/locales/en.yml
new file mode 100644
index 0000000000..cf9b342d0a
--- /dev/null
+++ b/actiontext/test/dummy/config/locales/en.yml
@@ -0,0 +1,33 @@
+# Files in the config/locales directory are used for internationalization
+# and are automatically loaded by Rails. If you want to use locales other
+# than English, add the necessary files in this directory.
+#
+# To use the locales, use `I18n.t`:
+#
+# I18n.t 'hello'
+#
+# In views, this is aliased to just `t`:
+#
+# <%= t('hello') %>
+#
+# To use a different locale, set it with `I18n.locale`:
+#
+# I18n.locale = :es
+#
+# 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 https://guides.rubyonrails.org/i18n.html.
+
+en:
+ hello: "Hello world"
diff --git a/actiontext/test/dummy/config/puma.rb b/actiontext/test/dummy/config/puma.rb
new file mode 100644
index 0000000000..71ed69a895
--- /dev/null
+++ b/actiontext/test/dummy/config/puma.rb
@@ -0,0 +1,34 @@
+# 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.
+#
+threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
+threads threads_count, threads_count
+
+# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
+#
+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 web server 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/actiontext/test/dummy/config/routes.rb b/actiontext/test/dummy/config/routes.rb
new file mode 100644
index 0000000000..1fc667e242
--- /dev/null
+++ b/actiontext/test/dummy/config/routes.rb
@@ -0,0 +1,4 @@
+Rails.application.routes.draw do
+ resources :messages
+ # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
+end
diff --git a/actiontext/test/dummy/config/spring.rb b/actiontext/test/dummy/config/spring.rb
new file mode 100644
index 0000000000..9fa7863f99
--- /dev/null
+++ b/actiontext/test/dummy/config/spring.rb
@@ -0,0 +1,6 @@
+%w[
+ .ruby-version
+ .rbenv-vars
+ tmp/restart.txt
+ tmp/caching-dev.txt
+].each { |path| Spring.watch(path) }
diff --git a/actiontext/test/dummy/config/storage.yml b/actiontext/test/dummy/config/storage.yml
new file mode 100644
index 0000000000..53e562e0fc
--- /dev/null
+++ b/actiontext/test/dummy/config/storage.yml
@@ -0,0 +1,35 @@
+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
+# path: your_azure_storage_path
+# 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/actiontext/test/dummy/config/webpack/development.js b/actiontext/test/dummy/config/webpack/development.js
new file mode 100644
index 0000000000..81269f6513
--- /dev/null
+++ b/actiontext/test/dummy/config/webpack/development.js
@@ -0,0 +1,3 @@
+const environment = require('./environment')
+
+module.exports = environment.toWebpackConfig()
diff --git a/actiontext/test/dummy/config/webpack/environment.js b/actiontext/test/dummy/config/webpack/environment.js
new file mode 100644
index 0000000000..d16d9af743
--- /dev/null
+++ b/actiontext/test/dummy/config/webpack/environment.js
@@ -0,0 +1,3 @@
+const { environment } = require('@rails/webpacker')
+
+module.exports = environment
diff --git a/actiontext/test/dummy/config/webpack/production.js b/actiontext/test/dummy/config/webpack/production.js
new file mode 100644
index 0000000000..81269f6513
--- /dev/null
+++ b/actiontext/test/dummy/config/webpack/production.js
@@ -0,0 +1,3 @@
+const environment = require('./environment')
+
+module.exports = environment.toWebpackConfig()
diff --git a/actiontext/test/dummy/config/webpack/test.js b/actiontext/test/dummy/config/webpack/test.js
new file mode 100644
index 0000000000..81269f6513
--- /dev/null
+++ b/actiontext/test/dummy/config/webpack/test.js
@@ -0,0 +1,3 @@
+const environment = require('./environment')
+
+module.exports = environment.toWebpackConfig()
diff --git a/actiontext/test/dummy/config/webpacker.yml b/actiontext/test/dummy/config/webpacker.yml
new file mode 100644
index 0000000000..d3f24e1b4b
--- /dev/null
+++ b/actiontext/test/dummy/config/webpacker.yml
@@ -0,0 +1,65 @@
+# 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
+
+ # 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
+ - .png
+ - .svg
+ - .gif
+ - .jpeg
+ - .jpg
+
+development:
+ <<: *default
+ compile: 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/actiontext/test/dummy/db/migrate/20180208205311_create_messages.rb b/actiontext/test/dummy/db/migrate/20180208205311_create_messages.rb
new file mode 100644
index 0000000000..3552840824
--- /dev/null
+++ b/actiontext/test/dummy/db/migrate/20180208205311_create_messages.rb
@@ -0,0 +1,8 @@
+class CreateMessages < ActiveRecord::Migration[6.0]
+ def change
+ create_table :messages do |t|
+ t.string :subject
+ t.timestamps
+ end
+ end
+end
diff --git a/actiontext/test/dummy/db/migrate/20180212164506_create_active_storage_tables.rb b/actiontext/test/dummy/db/migrate/20180212164506_create_active_storage_tables.rb
new file mode 100644
index 0000000000..0b2ce257c8
--- /dev/null
+++ b/actiontext/test/dummy/db/migrate/20180212164506_create_active_storage_tables.rb
@@ -0,0 +1,27 @@
+# This migration comes from active_storage (originally 20170806125915)
+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/actiontext/test/dummy/db/migrate/20180528164100_create_action_text_tables.rb b/actiontext/test/dummy/db/migrate/20180528164100_create_action_text_tables.rb
new file mode 100644
index 0000000000..e7c66ea6ae
--- /dev/null
+++ b/actiontext/test/dummy/db/migrate/20180528164100_create_action_text_tables.rb
@@ -0,0 +1,13 @@
+class CreateActionTextTables < ActiveRecord::Migration[6.0]
+ def change
+ create_table :action_text_rich_texts do |t|
+ t.string :name, null: false
+ t.text :body, size: :long
+ t.references :record, null: false, polymorphic: true, index: false
+
+ t.timestamps
+
+ t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
+ end
+ end
+end
diff --git a/actiontext/test/dummy/db/migrate/20181003185713_create_people.rb b/actiontext/test/dummy/db/migrate/20181003185713_create_people.rb
new file mode 100644
index 0000000000..6928a8e20d
--- /dev/null
+++ b/actiontext/test/dummy/db/migrate/20181003185713_create_people.rb
@@ -0,0 +1,9 @@
+class CreatePeople < ActiveRecord::Migration[6.0]
+ def change
+ create_table :people do |t|
+ t.string :name
+
+ t.timestamps
+ end
+ end
+end
diff --git a/actiontext/test/dummy/db/migrate/20190305172303_create_pages.rb b/actiontext/test/dummy/db/migrate/20190305172303_create_pages.rb
new file mode 100644
index 0000000000..3a71e55d94
--- /dev/null
+++ b/actiontext/test/dummy/db/migrate/20190305172303_create_pages.rb
@@ -0,0 +1,9 @@
+class CreatePages < ActiveRecord::Migration[6.0]
+ def change
+ create_table :pages do |t|
+ t.string :title
+
+ t.timestamps
+ end
+ end
+end
diff --git a/actiontext/test/dummy/db/migrate/20190317200724_create_reviews.rb b/actiontext/test/dummy/db/migrate/20190317200724_create_reviews.rb
new file mode 100644
index 0000000000..96e0eab287
--- /dev/null
+++ b/actiontext/test/dummy/db/migrate/20190317200724_create_reviews.rb
@@ -0,0 +1,8 @@
+class CreateReviews < ActiveRecord::Migration[6.0]
+ def change
+ create_table :reviews do |t|
+ t.belongs_to :message, null: false
+ t.string :author_name, null: false
+ end
+ end
+end
diff --git a/actiontext/test/dummy/db/schema.rb b/actiontext/test/dummy/db/schema.rb
new file mode 100644
index 0000000000..03e99b29d2
--- /dev/null
+++ b/actiontext/test/dummy/db/schema.rb
@@ -0,0 +1,71 @@
+# 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.
+#
+# 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.
+
+ActiveRecord::Schema.define(version: 2019_03_17_200724) do
+
+ create_table "action_text_rich_texts", force: :cascade do |t|
+ t.string "name", null: false
+ t.text "body"
+ t.string "record_type", null: false
+ t.integer "record_id", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
+ end
+
+ create_table "active_storage_attachments", force: :cascade do |t|
+ t.string "name", null: false
+ t.string "record_type", null: false
+ t.integer "record_id", null: false
+ t.integer "blob_id", null: false
+ t.datetime "created_at", null: false
+ t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
+ t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
+ end
+
+ create_table "active_storage_blobs", force: :cascade 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"], name: "index_active_storage_blobs_on_key", unique: true
+ end
+
+ create_table "messages", force: :cascade do |t|
+ t.string "subject"
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ end
+
+ create_table "pages", force: :cascade do |t|
+ t.string "title"
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ end
+
+ create_table "people", force: :cascade do |t|
+ t.string "name"
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
+ end
+
+ create_table "reviews", force: :cascade do |t|
+ t.integer "message_id", null: false
+ t.string "author_name", null: false
+ t.index ["message_id"], name: "index_reviews_on_message_id"
+ end
+
+ add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
+end
diff --git a/actiontext/test/dummy/lib/assets/.keep b/actiontext/test/dummy/lib/assets/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actiontext/test/dummy/lib/assets/.keep
diff --git a/actiontext/test/dummy/log/.keep b/actiontext/test/dummy/log/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actiontext/test/dummy/log/.keep
diff --git a/actiontext/test/dummy/package.json b/actiontext/test/dummy/package.json
new file mode 100644
index 0000000000..177a97c1e6
--- /dev/null
+++ b/actiontext/test/dummy/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "dummy",
+ "private": true,
+ "dependencies": {
+ "@rails/webpacker": "^4.0.2",
+ "actiontext": "file:../.."
+ },
+ "devDependencies": {
+ "webpack-dev-server": "^3.2.1"
+ }
+}
diff --git a/actiontext/test/dummy/public/404.html b/actiontext/test/dummy/public/404.html
new file mode 100644
index 0000000000..2be3af26fc
--- /dev/null
+++ b/actiontext/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/actiontext/test/dummy/public/422.html b/actiontext/test/dummy/public/422.html
new file mode 100644
index 0000000000..c08eac0d1d
--- /dev/null
+++ b/actiontext/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/actiontext/test/dummy/public/500.html b/actiontext/test/dummy/public/500.html
new file mode 100644
index 0000000000..78a030af22
--- /dev/null
+++ b/actiontext/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/actiontext/test/dummy/public/apple-touch-icon-precomposed.png b/actiontext/test/dummy/public/apple-touch-icon-precomposed.png
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actiontext/test/dummy/public/apple-touch-icon-precomposed.png
diff --git a/actiontext/test/dummy/public/apple-touch-icon.png b/actiontext/test/dummy/public/apple-touch-icon.png
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actiontext/test/dummy/public/apple-touch-icon.png
diff --git a/actiontext/test/dummy/public/favicon.ico b/actiontext/test/dummy/public/favicon.ico
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actiontext/test/dummy/public/favicon.ico
diff --git a/actiontext/test/dummy/storage/.keep b/actiontext/test/dummy/storage/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actiontext/test/dummy/storage/.keep
diff --git a/actiontext/test/dummy/tmp/.keep b/actiontext/test/dummy/tmp/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actiontext/test/dummy/tmp/.keep
diff --git a/actiontext/test/dummy/tmp/storage/.keep b/actiontext/test/dummy/tmp/storage/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/actiontext/test/dummy/tmp/storage/.keep
diff --git a/actiontext/test/dummy/yarn.lock b/actiontext/test/dummy/yarn.lock
new file mode 100644
index 0000000000..6cd01debdc
--- /dev/null
+++ b/actiontext/test/dummy/yarn.lock
@@ -0,0 +1,7592 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
+ integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==
+ dependencies:
+ "@babel/highlight" "^7.0.0"
+
+"@babel/core@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.3.4.tgz#921a5a13746c21e32445bf0798680e9d11a6530b"
+ integrity sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/generator" "^7.3.4"
+ "@babel/helpers" "^7.2.0"
+ "@babel/parser" "^7.3.4"
+ "@babel/template" "^7.2.2"
+ "@babel/traverse" "^7.3.4"
+ "@babel/types" "^7.3.4"
+ convert-source-map "^1.1.0"
+ debug "^4.1.0"
+ json5 "^2.1.0"
+ lodash "^4.17.11"
+ resolve "^1.3.2"
+ semver "^5.4.1"
+ source-map "^0.5.0"
+
+"@babel/generator@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.3.4.tgz#9aa48c1989257877a9d971296e5b73bfe72e446e"
+ integrity sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg==
+ dependencies:
+ "@babel/types" "^7.3.4"
+ jsesc "^2.5.1"
+ lodash "^4.17.11"
+ source-map "^0.5.0"
+ trim-right "^1.0.1"
+
+"@babel/helper-annotate-as-pure@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32"
+ integrity sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-builder-binary-assignment-operator-visitor@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz#6b69628dfe4087798e0c4ed98e3d4a6b2fbd2f5f"
+ integrity sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==
+ dependencies:
+ "@babel/helper-explode-assignable-expression" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-call-delegate@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-call-delegate/-/helper-call-delegate-7.1.0.tgz#6a957f105f37755e8645343d3038a22e1449cc4a"
+ integrity sha512-YEtYZrw3GUK6emQHKthltKNZwszBcHK58Ygcis+gVUrF4/FmTVr5CCqQNSfmvg2y+YDEANyYoaLz/SHsnusCwQ==
+ dependencies:
+ "@babel/helper-hoist-variables" "^7.0.0"
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-create-class-features-plugin@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.4.tgz#092711a7a3ad8ea34de3e541644c2ce6af1f6f0c"
+ integrity sha512-uFpzw6L2omjibjxa8VGZsJUPL5wJH0zzGKpoz0ccBkzIa6C8kWNUbiBmQ0rgOKWlHJ6qzmfa6lTiGchiV8SC+g==
+ dependencies:
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-member-expression-to-functions" "^7.0.0"
+ "@babel/helper-optimise-call-expression" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-replace-supers" "^7.3.4"
+ "@babel/helper-split-export-declaration" "^7.0.0"
+
+"@babel/helper-define-map@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.1.0.tgz#3b74caec329b3c80c116290887c0dd9ae468c20c"
+ integrity sha512-yPPcW8dc3gZLN+U1mhYV91QU3n5uTbx7DUdf8NnPbjS0RMwBuHi9Xt2MUgppmNz7CJxTBWsGczTiEp1CSOTPRg==
+ dependencies:
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/types" "^7.0.0"
+ lodash "^4.17.10"
+
+"@babel/helper-explode-assignable-expression@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz#537fa13f6f1674df745b0c00ec8fe4e99681c8f6"
+ integrity sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==
+ dependencies:
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-function-name@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53"
+ integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==
+ dependencies:
+ "@babel/helper-get-function-arity" "^7.0.0"
+ "@babel/template" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-get-function-arity@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3"
+ integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-hoist-variables@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.0.0.tgz#46adc4c5e758645ae7a45deb92bab0918c23bb88"
+ integrity sha512-Ggv5sldXUeSKsuzLkddtyhyHe2YantsxWKNi7A+7LeD12ExRDWTRk29JCXpaHPAbMaIPZSil7n+lq78WY2VY7w==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-member-expression-to-functions@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz#8cd14b0a0df7ff00f009e7d7a436945f47c7a16f"
+ integrity sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-module-imports@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d"
+ integrity sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-module-transforms@^7.1.0":
+ version "7.2.2"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.2.2.tgz#ab2f8e8d231409f8370c883d20c335190284b963"
+ integrity sha512-YRD7I6Wsv+IHuTPkAmAS4HhY0dkPobgLftHp0cRGZSdrRvmZY8rFvae/GVu3bD00qscuvK3WPHB3YdNpBXUqrA==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@babel/helper-simple-access" "^7.1.0"
+ "@babel/helper-split-export-declaration" "^7.0.0"
+ "@babel/template" "^7.2.2"
+ "@babel/types" "^7.2.2"
+ lodash "^4.17.10"
+
+"@babel/helper-optimise-call-expression@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz#a2920c5702b073c15de51106200aa8cad20497d5"
+ integrity sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-plugin-utils@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250"
+ integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==
+
+"@babel/helper-regex@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.0.0.tgz#2c1718923b57f9bbe64705ffe5640ac64d9bdb27"
+ integrity sha512-TR0/N0NDCcUIUEbqV6dCO+LptmmSQFQ7q70lfcEB4URsjD0E1HzicrwUH+ap6BAQ2jhCX9Q4UqZy4wilujWlkg==
+ dependencies:
+ lodash "^4.17.10"
+
+"@babel/helper-remap-async-to-generator@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz#361d80821b6f38da75bd3f0785ece20a88c5fe7f"
+ integrity sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.0.0"
+ "@babel/helper-wrap-function" "^7.1.0"
+ "@babel/template" "^7.1.0"
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.3.4.tgz#a795208e9b911a6eeb08e5891faacf06e7013e13"
+ integrity sha512-pvObL9WVf2ADs+ePg0jrqlhHoxRXlOa+SHRHzAXIz2xkYuOHfGl+fKxPMaS4Fq+uje8JQPobnertBBvyrWnQ1A==
+ dependencies:
+ "@babel/helper-member-expression-to-functions" "^7.0.0"
+ "@babel/helper-optimise-call-expression" "^7.0.0"
+ "@babel/traverse" "^7.3.4"
+ "@babel/types" "^7.3.4"
+
+"@babel/helper-simple-access@^7.1.0":
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz#65eeb954c8c245beaa4e859da6188f39d71e585c"
+ integrity sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==
+ dependencies:
+ "@babel/template" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-split-export-declaration@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0.tgz#3aae285c0311c2ab095d997b8c9a94cad547d813"
+ integrity sha512-MXkOJqva62dfC0w85mEf/LucPPS/1+04nmmRMPEBUB++hiiThQ2zPtX/mEWQ3mtzCEjIJvPY8nuwxXtQeQwUag==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@babel/helper-wrap-function@^7.1.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa"
+ integrity sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==
+ dependencies:
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/template" "^7.1.0"
+ "@babel/traverse" "^7.1.0"
+ "@babel/types" "^7.2.0"
+
+"@babel/helpers@^7.2.0":
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.3.1.tgz#949eec9ea4b45d3210feb7dc1c22db664c9e44b9"
+ integrity sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA==
+ dependencies:
+ "@babel/template" "^7.1.2"
+ "@babel/traverse" "^7.1.5"
+ "@babel/types" "^7.3.0"
+
+"@babel/highlight@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4"
+ integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==
+ dependencies:
+ chalk "^2.0.0"
+ esutils "^2.0.2"
+ js-tokens "^4.0.0"
+
+"@babel/parser@^7.2.2", "@babel/parser@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.3.4.tgz#a43357e4bbf4b92a437fb9e465c192848287f27c"
+ integrity sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ==
+
+"@babel/plugin-proposal-async-generator-functions@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e"
+ integrity sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-remap-async-to-generator" "^7.1.0"
+ "@babel/plugin-syntax-async-generators" "^7.2.0"
+
+"@babel/plugin-proposal-class-properties@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.4.tgz#410f5173b3dc45939f9ab30ca26684d72901405e"
+ integrity sha512-lUf8D3HLs4yYlAo8zjuneLvfxN7qfKv1Yzbj5vjqaqMJxgJA3Ipwp4VUJ+OrOdz53Wbww6ahwB8UhB2HQyLotA==
+ dependencies:
+ "@babel/helper-create-class-features-plugin" "^7.3.4"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-proposal-json-strings@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz#568ecc446c6148ae6b267f02551130891e29f317"
+ integrity sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-syntax-json-strings" "^7.2.0"
+
+"@babel/plugin-proposal-object-rest-spread@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.4.tgz#47f73cf7f2a721aad5c0261205405c642e424654"
+ integrity sha512-j7VQmbbkA+qrzNqbKHrBsW3ddFnOeva6wzSe/zB7T+xaxGc+RCpwo44wCmRixAIGRoIpmVgvzFzNJqQcO3/9RA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
+
+"@babel/plugin-proposal-optional-catch-binding@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5"
+ integrity sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
+
+"@babel/plugin-proposal-unicode-property-regex@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.2.0.tgz#abe7281fe46c95ddc143a65e5358647792039520"
+ integrity sha512-LvRVYb7kikuOtIoUeWTkOxQEV1kYvL5B6U3iWEGCzPNRus1MzJweFqORTj+0jkxozkTSYNJozPOddxmqdqsRpw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-regex" "^7.0.0"
+ regexpu-core "^4.2.0"
+
+"@babel/plugin-syntax-async-generators@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f"
+ integrity sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-dynamic-import@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612"
+ integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-json-strings@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz#72bd13f6ffe1d25938129d2a186b11fd62951470"
+ integrity sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-object-rest-spread@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e"
+ integrity sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-syntax-optional-catch-binding@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz#a94013d6eda8908dfe6a477e7f9eda85656ecf5c"
+ integrity sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-arrow-functions@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz#9aeafbe4d6ffc6563bf8f8372091628f00779550"
+ integrity sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-async-to-generator@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.3.4.tgz#4e45408d3c3da231c0e7b823f407a53a7eb3048c"
+ integrity sha512-Y7nCzv2fw/jEZ9f678MuKdMo99MFDJMT/PvD9LisrR5JDFcJH6vYeH6RnjVt3p5tceyGRvTtEN0VOlU+rgHZjA==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-remap-async-to-generator" "^7.1.0"
+
+"@babel/plugin-transform-block-scoped-functions@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz#5d3cc11e8d5ddd752aa64c9148d0db6cb79fd190"
+ integrity sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-block-scoping@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.3.4.tgz#5c22c339de234076eee96c8783b2fed61202c5c4"
+ integrity sha512-blRr2O8IOZLAOJklXLV4WhcEzpYafYQKSGT3+R26lWG41u/FODJuBggehtOwilVAcFu393v3OFj+HmaE6tVjhA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ lodash "^4.17.11"
+
+"@babel/plugin-transform-classes@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.3.4.tgz#dc173cb999c6c5297e0b5f2277fdaaec3739d0cc"
+ integrity sha512-J9fAvCFBkXEvBimgYxCjvaVDzL6thk0j0dBvCeZmIUDBwyt+nv6HfbImsSrWsYXfDNDivyANgJlFXDUWRTZBuA==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.0.0"
+ "@babel/helper-define-map" "^7.1.0"
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-optimise-call-expression" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-replace-supers" "^7.3.4"
+ "@babel/helper-split-export-declaration" "^7.0.0"
+ globals "^11.1.0"
+
+"@babel/plugin-transform-computed-properties@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz#83a7df6a658865b1c8f641d510c6f3af220216da"
+ integrity sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-destructuring@^7.2.0", "@babel/plugin-transform-destructuring@^7.3.2":
+ version "7.3.2"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.3.2.tgz#f2f5520be055ba1c38c41c0e094d8a461dd78f2d"
+ integrity sha512-Lrj/u53Ufqxl/sGxyjsJ2XNtNuEjDyjpqdhMNh5aZ+XFOdThL46KBj27Uem4ggoezSYBxKWAil6Hu8HtwqesYw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-dotall-regex@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.2.0.tgz#f0aabb93d120a8ac61e925ea0ba440812dbe0e49"
+ integrity sha512-sKxnyHfizweTgKZf7XsXu/CNupKhzijptfTM+bozonIuyVrLWVUvYjE2bhuSBML8VQeMxq4Mm63Q9qvcvUcciQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-regex" "^7.0.0"
+ regexpu-core "^4.1.3"
+
+"@babel/plugin-transform-duplicate-keys@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.2.0.tgz#d952c4930f312a4dbfff18f0b2914e60c35530b3"
+ integrity sha512-q+yuxW4DsTjNceUiTzK0L+AfQ0zD9rWaTLiUqHA8p0gxx7lu1EylenfzjeIWNkPy6e/0VG/Wjw9uf9LueQwLOw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-exponentiation-operator@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz#a63868289e5b4007f7054d46491af51435766008"
+ integrity sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==
+ dependencies:
+ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-for-of@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.2.0.tgz#ab7468befa80f764bb03d3cb5eef8cc998e1cad9"
+ integrity sha512-Kz7Mt0SsV2tQk6jG5bBv5phVbkd0gd27SgYD4hH1aLMJRchM0dzHaXvrWhVZ+WxAlDoAKZ7Uy3jVTW2mKXQ1WQ==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-function-name@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.2.0.tgz#f7930362829ff99a3174c39f0afcc024ef59731a"
+ integrity sha512-kWgksow9lHdvBC2Z4mxTsvc7YdY7w/V6B2vy9cTIPtLEE9NhwoWivaxdNM/S37elu5bqlLP/qOY906LukO9lkQ==
+ dependencies:
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-literals@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz#690353e81f9267dad4fd8cfd77eafa86aba53ea1"
+ integrity sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-modules-amd@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.2.0.tgz#82a9bce45b95441f617a24011dc89d12da7f4ee6"
+ integrity sha512-mK2A8ucqz1qhrdqjS9VMIDfIvvT2thrEsIQzbaTdc5QFzhDjQv2CkJJ5f6BXIkgbmaoax3zBr2RyvV/8zeoUZw==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-modules-commonjs@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.2.0.tgz#c4f1933f5991d5145e9cfad1dfd848ea1727f404"
+ integrity sha512-V6y0uaUQrQPXUrmj+hgnks8va2L0zcZymeU7TtWEgdRLNkceafKXEduv7QzgQAE4lT+suwooG9dC7LFhdRAbVQ==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-simple-access" "^7.1.0"
+
+"@babel/plugin-transform-modules-systemjs@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.3.4.tgz#813b34cd9acb6ba70a84939f3680be0eb2e58861"
+ integrity sha512-VZ4+jlGOF36S7TjKs8g4ojp4MEI+ebCQZdswWb/T9I4X84j8OtFAyjXjt/M16iIm5RIZn0UMQgg/VgIwo/87vw==
+ dependencies:
+ "@babel/helper-hoist-variables" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-modules-umd@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz#7678ce75169f0877b8eb2235538c074268dd01ae"
+ integrity sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==
+ dependencies:
+ "@babel/helper-module-transforms" "^7.1.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-named-capturing-groups-regex@^7.3.0":
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.3.0.tgz#140b52985b2d6ef0cb092ef3b29502b990f9cd50"
+ integrity sha512-NxIoNVhk9ZxS+9lSoAQ/LM0V2UEvARLttEHUrRDGKFaAxOYQcrkN/nLRE+BbbicCAvZPl7wMP0X60HsHE5DtQw==
+ dependencies:
+ regexp-tree "^0.1.0"
+
+"@babel/plugin-transform-new-target@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz#ae8fbd89517fa7892d20e6564e641e8770c3aa4a"
+ integrity sha512-yin069FYjah+LbqfGeTfzIBODex/e++Yfa0rH0fpfam9uTbuEeEOx5GLGr210ggOV77mVRNoeqSYqeuaqSzVSw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-object-super@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.2.0.tgz#b35d4c10f56bab5d650047dad0f1d8e8814b6598"
+ integrity sha512-VMyhPYZISFZAqAPVkiYb7dUe2AsVi2/wCT5+wZdsNO31FojQJa9ns40hzZ6U9f50Jlq4w6qwzdBB2uwqZ00ebg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-replace-supers" "^7.1.0"
+
+"@babel/plugin-transform-parameters@^7.2.0":
+ version "7.3.3"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.3.3.tgz#3a873e07114e1a5bee17d04815662c8317f10e30"
+ integrity sha512-IrIP25VvXWu/VlBWTpsjGptpomtIkYrN/3aDp4UKm7xK6UxZY88kcJ1UwETbzHAlwN21MnNfwlar0u8y3KpiXw==
+ dependencies:
+ "@babel/helper-call-delegate" "^7.1.0"
+ "@babel/helper-get-function-arity" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-regenerator@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.3.4.tgz#1601655c362f5b38eead6a52631f5106b29fa46a"
+ integrity sha512-hvJg8EReQvXT6G9H2MvNPXkv9zK36Vxa1+csAVTpE1J3j0zlHplw76uudEbJxgvqZzAq9Yh45FLD4pk5mKRFQA==
+ dependencies:
+ regenerator-transform "^0.13.4"
+
+"@babel/plugin-transform-runtime@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.3.4.tgz#57805ac8c1798d102ecd75c03b024a5b3ea9b431"
+ integrity sha512-PaoARuztAdd5MgeVjAxnIDAIUet5KpogqaefQvPOmPYCxYoaPhautxDh3aO8a4xHsKgT/b9gSxR0BKK1MIewPA==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ resolve "^1.8.1"
+ semver "^5.5.1"
+
+"@babel/plugin-transform-shorthand-properties@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz#6333aee2f8d6ee7e28615457298934a3b46198f0"
+ integrity sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-spread@^7.2.0":
+ version "7.2.2"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz#3103a9abe22f742b6d406ecd3cd49b774919b406"
+ integrity sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-sticky-regex@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz#a1e454b5995560a9c1e0d537dfc15061fd2687e1"
+ integrity sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-regex" "^7.0.0"
+
+"@babel/plugin-transform-template-literals@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.2.0.tgz#d87ed01b8eaac7a92473f608c97c089de2ba1e5b"
+ integrity sha512-FkPix00J9A/XWXv4VoKJBMeSkyY9x/TqIh76wzcdfl57RJJcf8CehQ08uwfhCDNtRQYtHQKBTwKZDEyjE13Lwg==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-typeof-symbol@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz#117d2bcec2fbf64b4b59d1f9819894682d29f2b2"
+ integrity sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+
+"@babel/plugin-transform-unicode-regex@^7.2.0":
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.2.0.tgz#4eb8db16f972f8abb5062c161b8b115546ade08b"
+ integrity sha512-m48Y0lMhrbXEJnVUaYly29jRXbQ3ksxPrS1Tg8t+MHqzXhtBYAvI51euOBaoAlZLPHsieY9XPVMf80a5x0cPcA==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/helper-regex" "^7.0.0"
+ regexpu-core "^4.1.3"
+
+"@babel/polyfill@^7.2.5":
+ version "7.2.5"
+ resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.2.5.tgz#6c54b964f71ad27edddc567d065e57e87ed7fa7d"
+ integrity sha512-8Y/t3MWThtMLYr0YNC/Q76tqN1w30+b0uQMeFUYauG2UGTR19zyUtFrAzT23zNtBxPp+LbE5E/nwV/q/r3y6ug==
+ dependencies:
+ core-js "^2.5.7"
+ regenerator-runtime "^0.12.0"
+
+"@babel/preset-env@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.4.tgz#887cf38b6d23c82f19b5135298bdb160062e33e1"
+ integrity sha512-2mwqfYMK8weA0g0uBKOt4FE3iEodiHy9/CW0b+nWXcbL+pGzLx8ESYc+j9IIxr6LTDHWKgPm71i9smo02bw+gA==
+ dependencies:
+ "@babel/helper-module-imports" "^7.0.0"
+ "@babel/helper-plugin-utils" "^7.0.0"
+ "@babel/plugin-proposal-async-generator-functions" "^7.2.0"
+ "@babel/plugin-proposal-json-strings" "^7.2.0"
+ "@babel/plugin-proposal-object-rest-spread" "^7.3.4"
+ "@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
+ "@babel/plugin-proposal-unicode-property-regex" "^7.2.0"
+ "@babel/plugin-syntax-async-generators" "^7.2.0"
+ "@babel/plugin-syntax-json-strings" "^7.2.0"
+ "@babel/plugin-syntax-object-rest-spread" "^7.2.0"
+ "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
+ "@babel/plugin-transform-arrow-functions" "^7.2.0"
+ "@babel/plugin-transform-async-to-generator" "^7.3.4"
+ "@babel/plugin-transform-block-scoped-functions" "^7.2.0"
+ "@babel/plugin-transform-block-scoping" "^7.3.4"
+ "@babel/plugin-transform-classes" "^7.3.4"
+ "@babel/plugin-transform-computed-properties" "^7.2.0"
+ "@babel/plugin-transform-destructuring" "^7.2.0"
+ "@babel/plugin-transform-dotall-regex" "^7.2.0"
+ "@babel/plugin-transform-duplicate-keys" "^7.2.0"
+ "@babel/plugin-transform-exponentiation-operator" "^7.2.0"
+ "@babel/plugin-transform-for-of" "^7.2.0"
+ "@babel/plugin-transform-function-name" "^7.2.0"
+ "@babel/plugin-transform-literals" "^7.2.0"
+ "@babel/plugin-transform-modules-amd" "^7.2.0"
+ "@babel/plugin-transform-modules-commonjs" "^7.2.0"
+ "@babel/plugin-transform-modules-systemjs" "^7.3.4"
+ "@babel/plugin-transform-modules-umd" "^7.2.0"
+ "@babel/plugin-transform-named-capturing-groups-regex" "^7.3.0"
+ "@babel/plugin-transform-new-target" "^7.0.0"
+ "@babel/plugin-transform-object-super" "^7.2.0"
+ "@babel/plugin-transform-parameters" "^7.2.0"
+ "@babel/plugin-transform-regenerator" "^7.3.4"
+ "@babel/plugin-transform-shorthand-properties" "^7.2.0"
+ "@babel/plugin-transform-spread" "^7.2.0"
+ "@babel/plugin-transform-sticky-regex" "^7.2.0"
+ "@babel/plugin-transform-template-literals" "^7.2.0"
+ "@babel/plugin-transform-typeof-symbol" "^7.2.0"
+ "@babel/plugin-transform-unicode-regex" "^7.2.0"
+ browserslist "^4.3.4"
+ invariant "^2.2.2"
+ js-levenshtein "^1.1.3"
+ semver "^5.3.0"
+
+"@babel/runtime@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83"
+ integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==
+ dependencies:
+ regenerator-runtime "^0.12.0"
+
+"@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2":
+ version "7.2.2"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907"
+ integrity sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/parser" "^7.2.2"
+ "@babel/types" "^7.2.2"
+
+"@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.3.4.tgz#1330aab72234f8dea091b08c4f8b9d05c7119e06"
+ integrity sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ==
+ dependencies:
+ "@babel/code-frame" "^7.0.0"
+ "@babel/generator" "^7.3.4"
+ "@babel/helper-function-name" "^7.1.0"
+ "@babel/helper-split-export-declaration" "^7.0.0"
+ "@babel/parser" "^7.3.4"
+ "@babel/types" "^7.3.4"
+ debug "^4.1.0"
+ globals "^11.1.0"
+ lodash "^4.17.11"
+
+"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2", "@babel/types@^7.3.0", "@babel/types@^7.3.4":
+ version "7.3.4"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed"
+ integrity sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==
+ dependencies:
+ esutils "^2.0.2"
+ lodash "^4.17.11"
+ to-fast-properties "^2.0.0"
+
+"@csstools/convert-colors@^1.4.0":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
+ integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==
+
+"@rails/activestorage@^6.0.0-alpha":
+ version "6.0.0-alpha"
+ resolved "https://registry.yarnpkg.com/@rails/activestorage/-/activestorage-6.0.0-alpha.tgz#6eb6c0f49bcaa4ad68fcd13a750b9a17f76f086f"
+ integrity sha512-ZQneYyDWrazLFZgyfZ3G4h9Q+TxvSQE5BEenBhvusyAeYMVaYxrZU/PWFcbeyiQe5xz1SeN3g/UejU3ytCYJwQ==
+ dependencies:
+ spark-md5 "^3.0.0"
+
+"@rails/webpacker@^4.0.2":
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-4.0.2.tgz#2c2e96527500b060a84159098449ddb1615c65e8"
+ integrity sha512-TDj/+UHnWaEg8X21E3cGKvptm3BbW1aUtOAXtrYwpK9tkiWq+Dc40Gm2RIZW7rU3jxDDBZgPRiqvr5B0dorIVw==
+ dependencies:
+ "@babel/core" "^7.3.4"
+ "@babel/plugin-proposal-class-properties" "^7.3.4"
+ "@babel/plugin-proposal-object-rest-spread" "^7.3.4"
+ "@babel/plugin-syntax-dynamic-import" "^7.2.0"
+ "@babel/plugin-transform-destructuring" "^7.3.2"
+ "@babel/plugin-transform-regenerator" "^7.3.4"
+ "@babel/plugin-transform-runtime" "^7.3.4"
+ "@babel/polyfill" "^7.2.5"
+ "@babel/preset-env" "^7.3.4"
+ "@babel/runtime" "^7.3.4"
+ babel-loader "^8.0.5"
+ babel-plugin-dynamic-import-node "^2.2.0"
+ babel-plugin-macros "^2.5.0"
+ case-sensitive-paths-webpack-plugin "^2.2.0"
+ compression-webpack-plugin "^2.0.0"
+ css-loader "^2.1.0"
+ file-loader "^3.0.1"
+ flatted "^2.0.0"
+ glob "^7.1.3"
+ js-yaml "^3.12.2"
+ mini-css-extract-plugin "^0.5.0"
+ node-sass "^4.11.0"
+ optimize-css-assets-webpack-plugin "^5.0.1"
+ path-complete-extname "^1.0.0"
+ pnp-webpack-plugin "^1.3.1"
+ postcss-flexbugs-fixes "^4.1.0"
+ postcss-import "^12.0.1"
+ postcss-loader "^3.0.0"
+ postcss-preset-env "^6.6.0"
+ postcss-safe-parser "^4.0.1"
+ sass-loader "^7.1.0"
+ style-loader "^0.23.1"
+ terser-webpack-plugin "^1.2.3"
+ webpack "^4.29.6"
+ webpack-assets-manifest "^3.1.1"
+ webpack-cli "^3.2.3"
+ webpack-sources "^1.3.0"
+
+"@types/q@^1.5.1":
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.1.tgz#48fd98c1561fe718b61733daed46ff115b496e18"
+ integrity sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA==
+
+"@webassemblyjs/ast@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359"
+ integrity sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==
+ dependencies:
+ "@webassemblyjs/helper-module-context" "1.8.5"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.5"
+ "@webassemblyjs/wast-parser" "1.8.5"
+
+"@webassemblyjs/floating-point-hex-parser@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz#1ba926a2923613edce496fd5b02e8ce8a5f49721"
+ integrity sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==
+
+"@webassemblyjs/helper-api-error@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz#c49dad22f645227c5edb610bdb9697f1aab721f7"
+ integrity sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==
+
+"@webassemblyjs/helper-buffer@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz#fea93e429863dd5e4338555f42292385a653f204"
+ integrity sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==
+
+"@webassemblyjs/helper-code-frame@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz#9a740ff48e3faa3022b1dff54423df9aa293c25e"
+ integrity sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==
+ dependencies:
+ "@webassemblyjs/wast-printer" "1.8.5"
+
+"@webassemblyjs/helper-fsm@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz#ba0b7d3b3f7e4733da6059c9332275d860702452"
+ integrity sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==
+
+"@webassemblyjs/helper-module-context@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz#def4b9927b0101dc8cbbd8d1edb5b7b9c82eb245"
+ integrity sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ mamacro "^0.0.3"
+
+"@webassemblyjs/helper-wasm-bytecode@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz#537a750eddf5c1e932f3744206551c91c1b93e61"
+ integrity sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==
+
+"@webassemblyjs/helper-wasm-section@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz#74ca6a6bcbe19e50a3b6b462847e69503e6bfcbf"
+ integrity sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-buffer" "1.8.5"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.5"
+ "@webassemblyjs/wasm-gen" "1.8.5"
+
+"@webassemblyjs/ieee754@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz#712329dbef240f36bf57bd2f7b8fb9bf4154421e"
+ integrity sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==
+ dependencies:
+ "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.5.tgz#044edeb34ea679f3e04cd4fd9824d5e35767ae10"
+ integrity sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==
+ dependencies:
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.5.tgz#a8bf3b5d8ffe986c7c1e373ccbdc2a0915f0cedc"
+ integrity sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==
+
+"@webassemblyjs/wasm-edit@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz#962da12aa5acc1c131c81c4232991c82ce56e01a"
+ integrity sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-buffer" "1.8.5"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.5"
+ "@webassemblyjs/helper-wasm-section" "1.8.5"
+ "@webassemblyjs/wasm-gen" "1.8.5"
+ "@webassemblyjs/wasm-opt" "1.8.5"
+ "@webassemblyjs/wasm-parser" "1.8.5"
+ "@webassemblyjs/wast-printer" "1.8.5"
+
+"@webassemblyjs/wasm-gen@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz#54840766c2c1002eb64ed1abe720aded714f98bc"
+ integrity sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.5"
+ "@webassemblyjs/ieee754" "1.8.5"
+ "@webassemblyjs/leb128" "1.8.5"
+ "@webassemblyjs/utf8" "1.8.5"
+
+"@webassemblyjs/wasm-opt@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz#b24d9f6ba50394af1349f510afa8ffcb8a63d264"
+ integrity sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-buffer" "1.8.5"
+ "@webassemblyjs/wasm-gen" "1.8.5"
+ "@webassemblyjs/wasm-parser" "1.8.5"
+
+"@webassemblyjs/wasm-parser@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz#21576f0ec88b91427357b8536383668ef7c66b8d"
+ integrity sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-api-error" "1.8.5"
+ "@webassemblyjs/helper-wasm-bytecode" "1.8.5"
+ "@webassemblyjs/ieee754" "1.8.5"
+ "@webassemblyjs/leb128" "1.8.5"
+ "@webassemblyjs/utf8" "1.8.5"
+
+"@webassemblyjs/wast-parser@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz#e10eecd542d0e7bd394f6827c49f3df6d4eefb8c"
+ integrity sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/floating-point-hex-parser" "1.8.5"
+ "@webassemblyjs/helper-api-error" "1.8.5"
+ "@webassemblyjs/helper-code-frame" "1.8.5"
+ "@webassemblyjs/helper-fsm" "1.8.5"
+ "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/wast-printer@1.8.5":
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz#114bbc481fd10ca0e23b3560fa812748b0bae5bc"
+ integrity sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/wast-parser" "1.8.5"
+ "@xtuc/long" "4.2.2"
+
+"@xtuc/ieee754@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+ integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+ integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+ integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
+accepts@~1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f"
+ integrity sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=
+ dependencies:
+ mime-types "~2.1.16"
+ negotiator "0.6.1"
+
+acorn-dynamic-import@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948"
+ integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==
+
+acorn@^6.0.5:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f"
+ integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==
+
+"actiontext@file:../..":
+ version "6.0.0-beta2"
+ dependencies:
+ "@rails/activestorage" "^6.0.0-alpha"
+
+ajv-errors@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
+ integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==
+
+ajv-keywords@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be"
+ integrity sha1-rCsnk5xUPpXSwG5/f1wnvkqlQ74=
+
+ajv@^4.9.1:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+ integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=
+ dependencies:
+ co "^4.6.0"
+ json-stable-stringify "^1.0.1"
+
+ajv@^6.1.0:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.1.1.tgz#978d597fbc2b7d0e5a5c3ddeb149a682f2abfa0e"
+ integrity sha1-l41Zf7wrfQ5aXD3esUmmgvKr+g4=
+ dependencies:
+ fast-deep-equal "^1.0.0"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.3.0"
+
+ajv@^6.5.5:
+ version "6.10.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
+ integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
+ dependencies:
+ fast-deep-equal "^2.0.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+alphanum-sort@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
+ integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
+
+amdefine@>=0.0.4:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
+ integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=
+
+ansi-colors@^3.0.0:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf"
+ integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==
+
+ansi-html@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
+ integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4=
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+ integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+ integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+ integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
+
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+anymatch@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+ integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
+ dependencies:
+ micromatch "^3.1.4"
+ normalize-path "^2.1.1"
+
+aproba@^1.0.3, aproba@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+ integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+
+are-we-there-yet@~1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
+ integrity sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+argparse@^1.0.7:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
+ integrity sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=
+ dependencies:
+ sprintf-js "~1.0.2"
+
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+ integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+ integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+ integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
+array-find-index@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
+ integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=
+
+array-flatten@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+ integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
+
+array-flatten@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296"
+ integrity sha1-Qmu52oQJDBg42BLIFQryCoMx4pY=
+
+array-union@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+ integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=
+ 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"
+ integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+ integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
+asn1.js@^4.0.0:
+ version "4.9.2"
+ resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a"
+ integrity sha512-b/OsSjvWEo8Pi8H0zsDd2P6Uqo2TK2pH8gNLSJtNLM2Db0v2QaAZ0pBQJXVjAn4gBuugeVDr7s63ZogpUIwWDg==
+ dependencies:
+ bn.js "^4.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+asn1@~0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+ integrity sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+ integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+assert-plus@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
+ integrity sha1-104bh+ev/A24qttwIfP+SBAasjQ=
+
+assert@^1.1.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
+ integrity sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=
+ dependencies:
+ util "0.10.3"
+
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+ integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+
+async-each@^1.0.0, async-each@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+ integrity sha1-GdOGodntxufByF04iu28xW0zYC0=
+
+async-foreach@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
+ integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=
+
+async@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+ integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+atob@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d"
+ integrity sha1-GcenYEc3dEaPILLS0DNyrX1Mv10=
+
+autoprefixer@^9.4.9:
+ version "9.4.10"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.4.10.tgz#e1be61fc728bacac8f4252ed242711ec0dcc6a7b"
+ integrity sha512-XR8XZ09tUrrSzgSlys4+hy5r2/z4Jp7Ag3pHm31U4g/CTccYPOVe19AkaJ4ey/vRd1sfj+5TtuD6I0PXtutjvQ==
+ dependencies:
+ browserslist "^4.4.2"
+ caniuse-lite "^1.0.30000940"
+ normalize-range "^0.1.2"
+ num2fraction "^1.2.2"
+ postcss "^7.0.14"
+ postcss-value-parser "^3.3.1"
+
+aws-sign2@~0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
+ integrity sha1-FDQt0428yU0OW4fXY81jYSwOeU8=
+
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+ integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.2.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+ integrity sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=
+
+aws4@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
+ integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
+
+babel-loader@^8.0.5:
+ version "8.0.5"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.5.tgz#225322d7509c2157655840bba52e46b6c2f2fe33"
+ integrity sha512-NTnHnVRd2JnRqPC0vW+iOQWU5pchDbYXsG2E6DMXEpMfUcQKclF9gmf3G3ZMhzG7IG9ji4coL0cm+FxeWxDpnw==
+ dependencies:
+ find-cache-dir "^2.0.0"
+ loader-utils "^1.0.2"
+ mkdirp "^0.5.1"
+ util.promisify "^1.0.0"
+
+babel-plugin-dynamic-import-node@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.2.0.tgz#c0adfb07d95f4a4495e9aaac6ec386c4d7c2524e"
+ integrity sha512-fP899ELUnTaBcIzmrW7nniyqqdYWrWuJUyPWHxFa/c7r7hS6KC8FscNfLlBNIoPSc55kYMGEEKjPjJGCLbE1qA==
+ dependencies:
+ object.assign "^4.1.0"
+
+babel-plugin-macros@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-2.5.0.tgz#01f4d3b50ed567a67b80a30b9da066e94f4097b6"
+ integrity sha512-BWw0lD0kVZAXRD3Od1kMrdmfudqzDzYv2qrN3l2ISR1HVp1EgLKfbOrYV9xmY5k3qx3RIu5uPAUZZZHpo0o5Iw==
+ dependencies:
+ cosmiconfig "^5.0.5"
+ resolve "^1.8.1"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+ integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+base64-js@^1.0.2:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
+ integrity sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==
+
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+ dependencies:
+ cache-base "^1.0.1"
+ class-utils "^0.3.5"
+ component-emitter "^1.2.1"
+ define-property "^1.0.0"
+ isobject "^3.0.1"
+ mixin-deep "^1.2.0"
+ pascalcase "^0.1.1"
+
+batch@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
+ integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+ integrity sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=
+ dependencies:
+ tweetnacl "^0.14.3"
+
+big.js@^3.1.3:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
+ integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==
+
+big.js@^5.2.2:
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
+ integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
+
+binary-extensions@^1.0.0:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
+ integrity sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=
+
+block-stream@*:
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+ integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=
+ dependencies:
+ inherits "~2.0.0"
+
+bluebird@^3.5.3:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7"
+ integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
+ integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
+
+body-parser@1.18.2:
+ version "1.18.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
+ integrity sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=
+ dependencies:
+ bytes "3.0.0"
+ content-type "~1.0.4"
+ debug "2.6.9"
+ depd "~1.1.1"
+ http-errors "~1.6.2"
+ iconv-lite "0.4.19"
+ on-finished "~2.3.0"
+ qs "6.5.1"
+ raw-body "2.3.2"
+ type-is "~1.6.15"
+
+bonjour@^3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5"
+ integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU=
+ dependencies:
+ array-flatten "^2.1.0"
+ deep-equal "^1.0.1"
+ dns-equal "^1.0.0"
+ dns-txt "^2.0.2"
+ multicast-dns "^6.0.1"
+ multicast-dns-service-types "^1.1.0"
+
+boolbase@^1.0.0, boolbase@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+ integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
+boom@2.x.x:
+ version "2.10.1"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
+ integrity sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=
+ dependencies:
+ hoek "2.x.x"
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.0.tgz#a46941cb5fb492156b3d6a656e06c35364e3e66e"
+ integrity sha512-P4O8UQRdGiMLWSizsApmXVQDBS6KCt7dSexgLKBmH5Hr1CZq7vsnscFh8oR1sP1ab1Zj0uCHCEzZeV6SfUf3rA==
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
+braces@^2.3.1, braces@^2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+ integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
+brorand@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+ integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f"
+ integrity sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg==
+ dependencies:
+ buffer-xor "^1.0.3"
+ cipher-base "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.3"
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+browserify-cipher@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a"
+ integrity sha1-mYgkSHS/XtTijalWZtzWasj8Njo=
+ dependencies:
+ browserify-aes "^1.0.4"
+ browserify-des "^1.0.0"
+ evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd"
+ integrity sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=
+ dependencies:
+ cipher-base "^1.0.1"
+ des.js "^1.0.0"
+ inherits "^2.0.1"
+
+browserify-rsa@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
+ integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=
+ dependencies:
+ bn.js "^4.1.0"
+ randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
+ integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=
+ dependencies:
+ bn.js "^4.1.1"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.2"
+ elliptic "^6.0.0"
+ inherits "^2.0.1"
+ parse-asn1 "^5.0.0"
+
+browserify-zlib@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+ integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==
+ dependencies:
+ pako "~1.0.5"
+
+browserslist@^4.0.0, browserslist@^4.3.4, browserslist@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.4.2.tgz#6ea8a74d6464bb0bd549105f659b41197d8f0ba2"
+ integrity sha512-ISS/AIAiHERJ3d45Fz0AVYKkgcy+F/eJHzKEvv1j0wwKGKD9T3BrwKr/5g45L+Y4XIK5PlTqefHciRFcfE1Jxg==
+ dependencies:
+ caniuse-lite "^1.0.30000939"
+ electron-to-chromium "^1.3.113"
+ node-releases "^1.1.8"
+
+buffer-from@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+ integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
+buffer-indexof@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c"
+ integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==
+
+buffer-xor@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+ integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
+
+buffer@^4.3.0:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
+ integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+ isarray "^1.0.0"
+
+builtin-modules@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+ integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
+
+builtin-status-codes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+ integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
+
+bytes@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
+ integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
+
+cacache@^11.0.2, cacache@^11.2.0:
+ version "11.3.2"
+ resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.2.tgz#2d81e308e3d258ca38125b676b98b2ac9ce69bfa"
+ integrity sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg==
+ dependencies:
+ bluebird "^3.5.3"
+ chownr "^1.1.1"
+ figgy-pudding "^3.5.1"
+ glob "^7.1.3"
+ graceful-fs "^4.1.15"
+ lru-cache "^5.1.1"
+ mississippi "^3.0.0"
+ mkdirp "^0.5.1"
+ move-concurrently "^1.0.1"
+ promise-inflight "^1.0.1"
+ rimraf "^2.6.2"
+ ssri "^6.0.1"
+ unique-filename "^1.1.1"
+ y18n "^4.0.0"
+
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+ dependencies:
+ collection-visit "^1.0.0"
+ component-emitter "^1.2.1"
+ get-value "^2.0.6"
+ has-value "^1.0.0"
+ isobject "^3.0.1"
+ set-value "^2.0.0"
+ to-object-path "^0.3.0"
+ union-value "^1.0.0"
+ unset-value "^1.0.0"
+
+caller-callsite@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134"
+ integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=
+ dependencies:
+ callsites "^2.0.0"
+
+caller-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4"
+ integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=
+ dependencies:
+ caller-callsite "^2.0.0"
+
+callsites@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
+ integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=
+
+camelcase-keys@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
+ integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc=
+ dependencies:
+ camelcase "^2.0.0"
+ map-obj "^1.0.0"
+
+camelcase@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
+ integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=
+
+camelcase@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
+ integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo=
+
+camelcase@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+ integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
+
+camelcase@^5.0.0, camelcase@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.2.0.tgz#e7522abda5ed94cc0489e1b8466610e88404cf45"
+ integrity sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ==
+
+caniuse-api@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
+ integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+ dependencies:
+ browserslist "^4.0.0"
+ caniuse-lite "^1.0.0"
+ lodash.memoize "^4.1.2"
+ lodash.uniq "^4.5.0"
+
+caniuse-lite@^1.0.0:
+ version "1.0.30000808"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000808.tgz#7d759b5518529ea08b6705a19e70dbf401628ffc"
+ integrity sha512-vT0JLmHdvq1UVbYXioxCXHYdNw55tyvi+IUWyX0Zeh1OFQi2IllYtm38IJnSgHWCv/zUnX1hdhy3vMJvuTNSqw==
+
+caniuse-lite@^1.0.30000939, caniuse-lite@^1.0.30000940:
+ version "1.0.30000942"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000942.tgz#454139b28274bce70bfe1d50c30970df7430c6e4"
+ integrity sha512-wLf+IhZUy2rfz48tc40OH7jHjXjnvDFEYqBHluINs/6MgzoNLPf25zhE4NOVzqxLKndf+hau81sAW0RcGHIaBQ==
+
+case-sensitive-paths-webpack-plugin@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.2.0.tgz#3371ef6365ef9c25fa4b81c16ace0e9c7dc58c3e"
+ integrity sha512-u5ElzokS8A1pm9vM3/iDgTcI3xqHxuCao94Oz8etI3cf0Tio0p8izkDYbTIn09uP3yUUr6+veaE6IkjnTYS46g==
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+ integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
+chalk@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+ integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
+ 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, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
+ integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chokidar@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.1.tgz#6e67e9998fe10e8f651e975ca62460456ff8e297"
+ integrity sha512-rv5iP8ENhpqvDWr677rAXcB+SMoPQ1urd4ch79+PhM4lQwbATdJUQK69t0lJIKNB+VXpqxt5V1gvqs59XEPKnw==
+ dependencies:
+ anymatch "^2.0.0"
+ async-each "^1.0.0"
+ braces "^2.3.0"
+ glob-parent "^3.1.0"
+ inherits "^2.0.1"
+ is-binary-path "^1.0.0"
+ is-glob "^4.0.0"
+ normalize-path "^2.1.1"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.0.0"
+ upath "1.0.0"
+ optionalDependencies:
+ fsevents "^1.0.0"
+
+chokidar@^2.0.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.2.tgz#9c23ea40b01638439e0513864d362aeacc5ad058"
+ integrity sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==
+ dependencies:
+ anymatch "^2.0.0"
+ async-each "^1.0.1"
+ braces "^2.3.2"
+ glob-parent "^3.1.0"
+ inherits "^2.0.3"
+ is-binary-path "^1.0.0"
+ is-glob "^4.0.0"
+ normalize-path "^3.0.0"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.2.1"
+ upath "^1.1.0"
+ optionalDependencies:
+ fsevents "^1.2.7"
+
+chownr@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
+ integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==
+
+chrome-trace-event@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz#45a91bd2c20c9411f0963b5aaeb9a1b95e09cc48"
+ integrity sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==
+ dependencies:
+ tslib "^1.9.0"
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+ integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
+cliui@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+ integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wrap-ansi "^2.0.0"
+
+cliui@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
+ integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==
+ dependencies:
+ string-width "^2.1.1"
+ strip-ansi "^4.0.0"
+ wrap-ansi "^2.0.0"
+
+clone-deep@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713"
+ integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==
+ dependencies:
+ for-own "^1.0.0"
+ is-plain-object "^2.0.4"
+ kind-of "^6.0.0"
+ shallow-clone "^1.0.0"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+ integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+
+coa@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3"
+ integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==
+ dependencies:
+ "@types/q" "^1.5.1"
+ chalk "^2.4.1"
+ q "^1.1.2"
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+ integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+ dependencies:
+ map-visit "^1.0.0"
+ object-visit "^1.0.0"
+
+color-convert@^1.9.0, color-convert@^1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
+ integrity sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==
+ dependencies:
+ color-name "^1.1.1"
+
+color-name@^1.0.0, color-name@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+color-string@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.2.tgz#26e45814bc3c9a7cbd6751648a41434514a773a9"
+ integrity sha1-JuRYFLw8mny9Z1FkikFDRRSnc6k=
+ dependencies:
+ color-name "^1.0.0"
+ simple-swizzle "^0.2.2"
+
+color@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/color/-/color-3.1.0.tgz#d8e9fb096732875774c84bf922815df0308d0ffc"
+ integrity sha512-CwyopLkuRYO5ei2EpzpIh6LqJMt6Mt+jZhO5VI5f/wJLZriXQE32/SSqzmrh+QB+AZT81Cj8yv+7zwToW8ahZg==
+ dependencies:
+ color-convert "^1.9.1"
+ color-string "^1.5.2"
+
+combined-stream@^1.0.5, combined-stream@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
+ integrity sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=
+ dependencies:
+ delayed-stream "~1.0.0"
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
+ integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@~2.17.1:
+ version "2.17.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
+ integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
+
+commondir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+ integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
+
+component-emitter@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+ integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
+
+compressible@~2.0.11:
+ version "2.0.12"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.12.tgz#c59a5c99db76767e9876500e271ef63b3493bd66"
+ integrity sha1-xZpcmdt2dn6YdlAOJx72OzSTvWY=
+ dependencies:
+ mime-db ">= 1.30.0 < 2"
+
+compression-webpack-plugin@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-2.0.0.tgz#46476350c1eb27f783dccc79ac2f709baa2cffbc"
+ integrity sha512-bDgd7oTUZC8EkRx8j0sjyCfeiO+e5sFcfgaFcjVhfQf5lLya7oY2BczxcJ7IUuVjz5m6fy8IECFmVFew3xLk8Q==
+ dependencies:
+ cacache "^11.2.0"
+ find-cache-dir "^2.0.0"
+ neo-async "^2.5.0"
+ schema-utils "^1.0.0"
+ serialize-javascript "^1.4.0"
+ webpack-sources "^1.0.1"
+
+compression@^1.5.2:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.1.tgz#eff2603efc2e22cf86f35d2eb93589f9875373db"
+ integrity sha1-7/JgPvwuIs+G810uuTWJ+YdTc9s=
+ dependencies:
+ accepts "~1.3.4"
+ bytes "3.0.0"
+ compressible "~2.0.11"
+ debug "2.6.9"
+ on-headers "~1.0.1"
+ safe-buffer "5.1.1"
+ vary "~1.1.2"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+concat-stream@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+ integrity sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+connect-history-api-fallback@^1.3.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a"
+ integrity sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=
+
+console-browserify@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
+ integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=
+ dependencies:
+ date-now "^0.1.4"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+ integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
+
+constants-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+ integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=
+
+content-disposition@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
+ integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ=
+
+content-type@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+ integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+
+convert-source-map@^1.1.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
+ integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==
+ dependencies:
+ safe-buffer "~5.1.1"
+
+cookie-signature@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
+ integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
+
+cookie@0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
+ integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
+
+copy-concurrently@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
+ integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==
+ dependencies:
+ aproba "^1.1.1"
+ fs-write-stream-atomic "^1.0.8"
+ iferr "^0.1.5"
+ mkdirp "^0.5.1"
+ rimraf "^2.5.4"
+ run-queue "^1.0.0"
+
+copy-descriptor@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+ integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+
+core-js@^2.5.7:
+ version "2.6.5"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895"
+ integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==
+
+core-util-is@1.0.2, 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"
+ integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+cosmiconfig@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-4.0.0.tgz#760391549580bbd2df1e562bc177b13c290972dc"
+ integrity sha512-6e5vDdrXZD+t5v0L8CrurPeybg4Fmf+FCSYxXKYVAqLUtyCSbuyqE059d0kDthTNRzKVjL7QMgNpEUlsoYH3iQ==
+ dependencies:
+ is-directory "^0.3.1"
+ js-yaml "^3.9.0"
+ parse-json "^4.0.0"
+ require-from-string "^2.0.1"
+
+cosmiconfig@^5.0.0, cosmiconfig@^5.0.5:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.1.0.tgz#6c5c35e97f37f985061cdf653f114784231185cf"
+ integrity sha512-kCNPvthka8gvLtzAxQXvWo4FxqRB+ftRZyPZNuab5ngvM9Y7yw7hbEysglptLgpkGX9nAOKTBVkHUAe8xtYR6Q==
+ dependencies:
+ import-fresh "^2.0.0"
+ is-directory "^0.3.1"
+ js-yaml "^3.9.0"
+ lodash.get "^4.4.2"
+ parse-json "^4.0.0"
+
+create-ecdh@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
+ integrity sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=
+ dependencies:
+ bn.js "^4.1.0"
+ elliptic "^6.0.0"
+
+create-hash@^1.1.0, create-hash@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd"
+ integrity sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=
+ dependencies:
+ cipher-base "^1.0.1"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06"
+ integrity sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=
+ dependencies:
+ cipher-base "^1.0.3"
+ create-hash "^1.1.0"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+cross-spawn@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
+ integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI=
+ dependencies:
+ lru-cache "^4.0.1"
+ which "^1.2.9"
+
+cross-spawn@^6.0.0, cross-spawn@^6.0.5:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+cryptiles@2.x.x:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
+ integrity sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=
+ dependencies:
+ boom "2.x.x"
+
+crypto-browserify@^3.11.0:
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
+ integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==
+ dependencies:
+ browserify-cipher "^1.0.0"
+ browserify-sign "^4.0.0"
+ create-ecdh "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.0"
+ diffie-hellman "^5.0.0"
+ inherits "^2.0.1"
+ pbkdf2 "^3.0.3"
+ public-encrypt "^4.0.0"
+ randombytes "^2.0.0"
+ randomfill "^1.0.3"
+
+css-blank-pseudo@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5"
+ integrity sha512-LHz35Hr83dnFeipc7oqFDmsjHdljj3TQtxGGiNWSOsTLIAubSm4TEz8qCaKFpk7idaQ1GfWscF4E6mgpBysA1w==
+ dependencies:
+ postcss "^7.0.5"
+
+css-color-names@0.0.4, css-color-names@^0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
+ integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=
+
+css-declaration-sorter@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22"
+ integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==
+ dependencies:
+ postcss "^7.0.1"
+ timsort "^0.3.0"
+
+css-has-pseudo@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-0.10.0.tgz#3c642ab34ca242c59c41a125df9105841f6966ee"
+ integrity sha512-Z8hnfsZu4o/kt+AuFzeGpLVhFOGO9mluyHBaA2bA8aCGTwah5sT3WV/fTHH8UNZUytOIImuGPrl/prlb4oX4qQ==
+ dependencies:
+ postcss "^7.0.6"
+ postcss-selector-parser "^5.0.0-rc.4"
+
+css-loader@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea"
+ integrity sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w==
+ dependencies:
+ camelcase "^5.2.0"
+ icss-utils "^4.1.0"
+ loader-utils "^1.2.3"
+ normalize-path "^3.0.0"
+ postcss "^7.0.14"
+ postcss-modules-extract-imports "^2.0.0"
+ postcss-modules-local-by-default "^2.0.6"
+ postcss-modules-scope "^2.1.0"
+ postcss-modules-values "^2.0.0"
+ postcss-value-parser "^3.3.0"
+ schema-utils "^1.0.0"
+
+css-prefers-color-scheme@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz#6f830a2714199d4f0d0d0bb8a27916ed65cff1f4"
+ integrity sha512-MTu6+tMs9S3EUqzmqLXEcgNRbNkkD/TGFvowpeoWJn5Vfq7FMgsmRQs9X5NXAURiOBmOxm/lLjsDNXDE6k9bhg==
+ dependencies:
+ postcss "^7.0.5"
+
+css-select-base-adapter@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
+ integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
+
+css-select@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.0.2.tgz#ab4386cec9e1f668855564b17c3733b43b2a5ede"
+ integrity sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ==
+ dependencies:
+ boolbase "^1.0.0"
+ css-what "^2.1.2"
+ domutils "^1.7.0"
+ nth-check "^1.0.2"
+
+css-tree@1.0.0-alpha.28:
+ version "1.0.0-alpha.28"
+ resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.28.tgz#8e8968190d886c9477bc8d61e96f61af3f7ffa7f"
+ integrity sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w==
+ dependencies:
+ mdn-data "~1.1.0"
+ source-map "^0.5.3"
+
+css-tree@1.0.0-alpha.29:
+ version "1.0.0-alpha.29"
+ resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39"
+ integrity sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==
+ dependencies:
+ mdn-data "~1.1.0"
+ source-map "^0.5.3"
+
+css-unit-converter@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996"
+ integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY=
+
+css-url-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/css-url-regex/-/css-url-regex-1.1.0.tgz#83834230cc9f74c457de59eebd1543feeb83b7ec"
+ integrity sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w=
+
+css-what@^2.1.2:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2"
+ integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==
+
+cssdb@^4.3.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0"
+ integrity sha512-LsTAR1JPEM9TpGhl/0p3nQecC2LJ0kD8X5YARu1hk/9I1gril5vDtMZyNxcEpxxDj34YNck/ucjuoUd66K03oQ==
+
+cssesc@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703"
+ integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==
+
+cssesc@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+ integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+cssnano-preset-default@^4.0.7:
+ version "4.0.7"
+ resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76"
+ integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA==
+ dependencies:
+ css-declaration-sorter "^4.0.1"
+ cssnano-util-raw-cache "^4.0.1"
+ postcss "^7.0.0"
+ postcss-calc "^7.0.1"
+ postcss-colormin "^4.0.3"
+ postcss-convert-values "^4.0.1"
+ postcss-discard-comments "^4.0.2"
+ postcss-discard-duplicates "^4.0.2"
+ postcss-discard-empty "^4.0.1"
+ postcss-discard-overridden "^4.0.1"
+ postcss-merge-longhand "^4.0.11"
+ postcss-merge-rules "^4.0.3"
+ postcss-minify-font-values "^4.0.2"
+ postcss-minify-gradients "^4.0.2"
+ postcss-minify-params "^4.0.2"
+ postcss-minify-selectors "^4.0.2"
+ postcss-normalize-charset "^4.0.1"
+ postcss-normalize-display-values "^4.0.2"
+ postcss-normalize-positions "^4.0.2"
+ postcss-normalize-repeat-style "^4.0.2"
+ postcss-normalize-string "^4.0.2"
+ postcss-normalize-timing-functions "^4.0.2"
+ postcss-normalize-unicode "^4.0.1"
+ postcss-normalize-url "^4.0.1"
+ postcss-normalize-whitespace "^4.0.2"
+ postcss-ordered-values "^4.1.2"
+ postcss-reduce-initial "^4.0.3"
+ postcss-reduce-transforms "^4.0.2"
+ postcss-svgo "^4.0.2"
+ postcss-unique-selectors "^4.0.1"
+
+cssnano-util-get-arguments@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f"
+ integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=
+
+cssnano-util-get-match@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d"
+ integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=
+
+cssnano-util-raw-cache@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282"
+ integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==
+ dependencies:
+ postcss "^7.0.0"
+
+cssnano-util-same-parent@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3"
+ integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==
+
+cssnano@^4.1.0:
+ version "4.1.10"
+ resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2"
+ integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ==
+ dependencies:
+ cosmiconfig "^5.0.0"
+ cssnano-preset-default "^4.0.7"
+ is-resolvable "^1.0.0"
+ postcss "^7.0.0"
+
+csso@^3.5.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b"
+ integrity sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg==
+ dependencies:
+ css-tree "1.0.0-alpha.29"
+
+currently-unhandled@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
+ integrity sha1-mI3zP+qxke95mmE2nddsF635V+o=
+ dependencies:
+ array-find-index "^1.0.1"
+
+cyclist@~0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
+ integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+ dependencies:
+ assert-plus "^1.0.0"
+
+date-now@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+ integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=
+
+debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+debug@^3.2.5, debug@^3.2.6:
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+ integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+ dependencies:
+ ms "^2.1.1"
+
+debug@^4.1.0, debug@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+ integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+ dependencies:
+ ms "^2.1.1"
+
+decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+ integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
+
+decamelize@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-2.0.0.tgz#656d7bbc8094c4c788ea53c5840908c9c7d063c7"
+ integrity sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==
+ dependencies:
+ xregexp "4.0.0"
+
+decode-uri-component@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+ integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+
+deep-equal@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
+ integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
+
+deep-extend@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+ integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
+deep-extend@~0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
+ integrity sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=
+
+default-gateway@^4.0.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b"
+ integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==
+ dependencies:
+ execa "^1.0.0"
+ ip-regex "^2.1.0"
+
+define-properties@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94"
+ integrity sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=
+ dependencies:
+ foreach "^2.0.5"
+ object-keys "^1.0.8"
+
+define-properties@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
+ integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
+ dependencies:
+ object-keys "^1.0.12"
+
+define-property@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+ integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+ dependencies:
+ is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+ integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+ dependencies:
+ is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+ integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+ dependencies:
+ is-descriptor "^1.0.2"
+ isobject "^3.0.1"
+
+del@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5"
+ integrity sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=
+ dependencies:
+ globby "^6.1.0"
+ is-path-cwd "^1.0.0"
+ is-path-in-cwd "^1.0.0"
+ p-map "^1.1.1"
+ pify "^3.0.0"
+ rimraf "^2.2.8"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+ integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
+
+depd@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
+ integrity sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=
+
+depd@~1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+ integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+
+des.js@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
+ integrity sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=
+ dependencies:
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+destroy@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
+ integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+
+detect-file@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
+ integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
+
+detect-libc@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+ integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
+
+detect-node@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
+ integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
+
+diffie-hellman@^5.0.0:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
+ integrity sha1-tYNXOScM/ias9jIJn97SoH8gnl4=
+ dependencies:
+ bn.js "^4.1.0"
+ miller-rabin "^4.0.0"
+ randombytes "^2.0.0"
+
+dns-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
+ integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0=
+
+dns-packet@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a"
+ integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==
+ dependencies:
+ ip "^1.1.0"
+ safe-buffer "^5.0.1"
+
+dns-txt@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6"
+ integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=
+ dependencies:
+ buffer-indexof "^1.0.0"
+
+dom-serializer@0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
+ integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
+ dependencies:
+ domelementtype "^1.3.0"
+ entities "^1.1.1"
+
+domain-browser@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
+ integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
+
+domelementtype@1, domelementtype@^1.3.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
+ integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
+
+domutils@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
+ integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+dot-prop@^4.1.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
+ integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==
+ dependencies:
+ is-obj "^1.0.0"
+
+duplexify@^3.4.2, duplexify@^3.5.3:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.3.tgz#8b5818800df92fd0125b27ab896491912858243e"
+ integrity sha512-g8ID9OroF9hKt2POf8YLayy+9594PzmM3scI00/uBXocX3TWNgoB67hjzkFe9ITAbQOne/lLdBxHXvYUM4ZgGA==
+ dependencies:
+ end-of-stream "^1.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+ stream-shift "^1.0.0"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
+ integrity sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=
+ dependencies:
+ jsbn "~0.1.0"
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+ integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+
+electron-to-chromium@^1.3.113:
+ version "1.3.113"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz#b1ccf619df7295aea17bc6951dc689632629e4a9"
+ integrity sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g==
+
+elliptic@^6.0.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
+ integrity sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=
+ dependencies:
+ bn.js "^4.4.0"
+ brorand "^1.0.1"
+ hash.js "^1.0.0"
+ hmac-drbg "^1.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.0"
+
+emojis-list@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
+ integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
+
+encodeurl@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+ integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
+ integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==
+ dependencies:
+ once "^1.4.0"
+
+enhanced-resolve@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f"
+ integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==
+ dependencies:
+ graceful-fs "^4.1.2"
+ memory-fs "^0.4.0"
+ tapable "^1.0.0"
+
+entities@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
+ integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
+
+errno@^0.1.3, errno@^0.1.4:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.6.tgz#c386ce8a6283f14fc09563b71560908c9bf53026"
+ integrity sha512-IsORQDpaaSwcDP4ZZnHxgE85werpo34VYn1Ud3mq+eUsF593faR8oCZNXrROVkpFu2TsbrNhHin0aUrTsQ9vNw==
+ dependencies:
+ prr "~1.0.1"
+
+error-ex@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ integrity sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=
+ dependencies:
+ is-arrayish "^0.2.1"
+
+error-ex@^1.3.1:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+ integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es-abstract@^1.12.0, es-abstract@^1.5.1:
+ version "1.13.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
+ integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==
+ dependencies:
+ es-to-primitive "^1.2.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ is-callable "^1.1.4"
+ is-regex "^1.0.4"
+ object-keys "^1.0.12"
+
+es-to-primitive@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377"
+ integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==
+ dependencies:
+ is-callable "^1.1.4"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.2"
+
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+ integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+
+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"
+ integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+eslint-scope@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.2.tgz#5f10cd6cabb1965bf479fa65745673439e21cb0e"
+ integrity sha512-5q1+B/ogmHl8+paxtOKx38Z8LtWkVGuNt3+GQNErqwLl6ViNp/gdJGMCjZNxZ8j/VYjDNZ2Fo+eQc1TAVPIzbg==
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+esprima@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+ integrity sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==
+
+esrecurse@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
+ integrity sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=
+ dependencies:
+ estraverse "^4.1.0"
+ object-assign "^4.0.1"
+
+estraverse@^4.1.0, estraverse@^4.1.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+ integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+ integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=
+
+etag@~1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
+ integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+
+eventemitter3@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
+ integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==
+
+events@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+ integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
+
+eventsource@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0"
+ integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==
+ dependencies:
+ original "^1.0.0"
+
+evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+ integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==
+ dependencies:
+ md5.js "^1.3.4"
+ safe-buffer "^5.1.1"
+
+execa@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+ integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+ dependencies:
+ cross-spawn "^6.0.0"
+ get-stream "^4.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+ dependencies:
+ debug "^2.3.3"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ posix-character-classes "^0.1.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+expand-tilde@^2.0.0, expand-tilde@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
+ integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=
+ dependencies:
+ homedir-polyfill "^1.0.1"
+
+express@^4.16.2:
+ version "4.16.2"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c"
+ integrity sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=
+ dependencies:
+ accepts "~1.3.4"
+ array-flatten "1.1.1"
+ body-parser "1.18.2"
+ content-disposition "0.5.2"
+ content-type "~1.0.4"
+ cookie "0.3.1"
+ cookie-signature "1.0.6"
+ debug "2.6.9"
+ depd "~1.1.1"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ finalhandler "1.1.0"
+ fresh "0.5.2"
+ merge-descriptors "1.0.1"
+ methods "~1.1.2"
+ on-finished "~2.3.0"
+ parseurl "~1.3.2"
+ path-to-regexp "0.1.7"
+ proxy-addr "~2.0.2"
+ qs "6.5.1"
+ range-parser "~1.2.0"
+ safe-buffer "5.1.1"
+ send "0.16.1"
+ serve-static "1.13.1"
+ setprototypeof "1.1.0"
+ statuses "~1.3.1"
+ type-is "~1.6.15"
+ utils-merge "1.0.1"
+ vary "~1.1.2"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+ integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+ dependencies:
+ assign-symbols "^1.0.0"
+ is-extendable "^1.0.1"
+
+extend@~3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
+ integrity sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=
+
+extend@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+ integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+extglob@^2.0.2, extglob@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+ dependencies:
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ expand-brackets "^2.1.4"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+extsprintf@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+ integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+ integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+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"
+ integrity sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=
+
+fast-deep-equal@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
+ integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
+
+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"
+ integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
+
+faye-websocket@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"
+ integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=
+ dependencies:
+ websocket-driver ">=0.5.1"
+
+faye-websocket@~0.11.1:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38"
+ integrity sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=
+ dependencies:
+ websocket-driver ">=0.5.1"
+
+figgy-pudding@^3.5.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
+ integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==
+
+file-loader@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa"
+ integrity sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw==
+ dependencies:
+ loader-utils "^1.0.2"
+ schema-utils "^1.0.0"
+
+fill-range@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+ integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
+finalhandler@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5"
+ integrity sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ parseurl "~1.3.2"
+ statuses "~1.3.1"
+ unpipe "~1.0.0"
+
+find-cache-dir@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d"
+ integrity sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA==
+ dependencies:
+ commondir "^1.0.1"
+ make-dir "^1.0.0"
+ pkg-dir "^3.0.0"
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=
+ dependencies:
+ path-exists "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+find-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+ integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+ dependencies:
+ locate-path "^3.0.0"
+
+findup-sync@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc"
+ integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=
+ dependencies:
+ detect-file "^1.0.0"
+ is-glob "^3.1.0"
+ micromatch "^3.0.4"
+ resolve-dir "^1.0.1"
+
+flatted@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916"
+ integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==
+
+flatten@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
+ integrity sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=
+
+flush-write-stream@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417"
+ integrity sha1-yBuQ2HRnZvGmCaRoCZRsRd2K5Bc=
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.0.4"
+
+follow-redirects@^1.0.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76"
+ integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==
+ dependencies:
+ debug "^3.2.6"
+
+for-in@^0.1.3:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
+ integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=
+
+for-in@^1.0.1, for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+ integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+
+for-own@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b"
+ integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=
+ dependencies:
+ for-in "^1.0.1"
+
+foreach@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
+ integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+ integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@~2.1.1:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
+ integrity sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.5"
+ mime-types "^2.1.12"
+
+form-data@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+ integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.6"
+ mime-types "^2.1.12"
+
+forwarded@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
+ integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
+
+fragment-cache@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+ integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+ dependencies:
+ map-cache "^0.2.2"
+
+fresh@0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
+ integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+
+from2@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
+ integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+
+fs-minipass@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
+ integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==
+ dependencies:
+ minipass "^2.2.1"
+
+fs-write-stream-atomic@^1.0.8:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
+ integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=
+ dependencies:
+ graceful-fs "^4.1.2"
+ iferr "^0.1.5"
+ imurmurhash "^0.1.4"
+ readable-stream "1 || 2"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@^1.0.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8"
+ integrity sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==
+ dependencies:
+ nan "^2.3.0"
+ node-pre-gyp "^0.6.39"
+
+fsevents@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.7.tgz#4851b664a3783e52003b3c66eb0eee1074933aa4"
+ integrity sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==
+ dependencies:
+ nan "^2.9.2"
+ node-pre-gyp "^0.10.0"
+
+fstream-ignore@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
+ integrity sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=
+ dependencies:
+ fstream "^1.0.0"
+ inherits "2"
+ minimatch "^3.0.0"
+
+fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
+ integrity sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=
+ dependencies:
+ graceful-fs "^4.1.2"
+ inherits "~2.0.0"
+ mkdirp ">=0.5 0"
+ rimraf "2"
+
+function-bind@^1.0.2, function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+gaze@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105"
+ integrity sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=
+ dependencies:
+ globule "^1.0.0"
+
+get-caller-file@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+ integrity sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=
+
+get-stdin@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
+ integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
+
+get-stream@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+ integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+ dependencies:
+ pump "^3.0.0"
+
+get-value@^2.0.3, get-value@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+ integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob-parent@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+ integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=
+ dependencies:
+ is-glob "^3.1.0"
+ path-dirname "^1.0.0"
+
+glob@^6.0.4:
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
+ integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=
+ dependencies:
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "2 || 3"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.1.1:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==
+ 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"
+
+glob@^7.1.3:
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
+ integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
+ 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"
+
+global-modules@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
+ integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==
+ dependencies:
+ global-prefix "^1.0.1"
+ is-windows "^1.0.1"
+ resolve-dir "^1.0.0"
+
+global-prefix@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe"
+ integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=
+ dependencies:
+ expand-tilde "^2.0.2"
+ homedir-polyfill "^1.0.1"
+ ini "^1.3.4"
+ is-windows "^1.0.1"
+ which "^1.2.14"
+
+globals@^11.1.0:
+ version "11.11.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.11.0.tgz#dcf93757fa2de5486fbeed7118538adf789e9c2e"
+ integrity sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw==
+
+globby@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
+ integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=
+ dependencies:
+ array-union "^1.0.1"
+ glob "^7.0.3"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+globule@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09"
+ integrity sha1-HcScaCLdnoovoAuiopUAboZkvQk=
+ dependencies:
+ glob "~7.1.1"
+ lodash "~4.17.4"
+ minimatch "~3.0.2"
+
+graceful-fs@^4.1.11, graceful-fs@^4.1.2:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+ integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=
+
+graceful-fs@^4.1.15:
+ version "4.1.15"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
+ integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
+
+handle-thing@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754"
+ integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ==
+
+har-schema@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
+ integrity sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+ integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
+ integrity sha1-M0gdDxu/9gDdID11gSpqX7oALio=
+ dependencies:
+ ajv "^4.9.1"
+ har-schema "^1.0.5"
+
+har-validator@~5.1.0:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+ integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+ dependencies:
+ ajv "^6.5.5"
+ har-schema "^2.0.0"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+ integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+ integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+ integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
+
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+ dependencies:
+ get-value "^2.0.3"
+ has-values "^0.1.4"
+ isobject "^2.0.0"
+
+has-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+ integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+ dependencies:
+ get-value "^2.0.6"
+ has-values "^1.0.0"
+ isobject "^3.0.0"
+
+has-values@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+ integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+has@^1.0.0, has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+has@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
+ integrity sha1-hGFzP1OLCDfJNh45qauelwTcLyg=
+ dependencies:
+ function-bind "^1.0.2"
+
+hash-base@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1"
+ integrity sha1-ZuodhW206KVHDK32/OI65SRO8uE=
+ dependencies:
+ inherits "^2.0.1"
+
+hash-base@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918"
+ integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
+ integrity sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.0"
+
+hawk@3.1.3, hawk@~3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
+ integrity sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=
+ dependencies:
+ boom "2.x.x"
+ cryptiles "2.x.x"
+ hoek "2.x.x"
+ sntp "1.x.x"
+
+hex-color-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
+ integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
+
+hmac-drbg@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+ integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
+ dependencies:
+ hash.js "^1.0.3"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.1"
+
+hoek@2.x.x:
+ version "2.16.3"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
+ integrity sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=
+
+homedir-polyfill@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"
+ integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==
+ dependencies:
+ parse-passwd "^1.0.0"
+
+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"
+ integrity sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==
+
+hpack.js@^2.1.6:
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2"
+ integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=
+ dependencies:
+ inherits "^2.0.1"
+ obuf "^1.0.0"
+ readable-stream "^2.0.1"
+ wbuf "^1.1.0"
+
+hsl-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e"
+ integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=
+
+hsla-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38"
+ integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg=
+
+html-comment-regex@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
+ integrity sha1-ZouTd26q5V696POtRkswekljYl4=
+
+html-entities@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
+ integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=
+
+http-deceiver@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
+ integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=
+
+http-errors@1.6.2, http-errors@~1.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
+ integrity sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=
+ dependencies:
+ depd "1.1.1"
+ inherits "2.0.3"
+ setprototypeof "1.0.3"
+ statuses ">= 1.3.1 < 2"
+
+http-parser-js@>=0.4.0:
+ version "0.4.10"
+ resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4"
+ integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=
+
+http-proxy-middleware@^0.19.1:
+ version "0.19.1"
+ resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a"
+ integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==
+ dependencies:
+ http-proxy "^1.17.0"
+ is-glob "^4.0.0"
+ lodash "^4.17.11"
+ micromatch "^3.1.10"
+
+http-proxy@^1.17.0:
+ version "1.17.0"
+ resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a"
+ integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==
+ dependencies:
+ eventemitter3 "^3.0.0"
+ follow-redirects "^1.0.0"
+ requires-port "^1.0.0"
+
+http-signature@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
+ integrity sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=
+ dependencies:
+ assert-plus "^0.2.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+https-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
+ integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
+
+iconv-lite@0.4.19:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+ integrity sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==
+
+iconv-lite@^0.4.4:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+icss-replace-symbols@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
+ integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=
+
+icss-utils@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.0.tgz#339dbbffb9f8729a243b701e1c29d4cc58c52f0e"
+ integrity sha512-3DEun4VOeMvSczifM3F2cKQrDQ5Pj6WKhkOq6HD4QTnDUAq8MQRxy5TX6Sy1iY6WPBe4gQ3p5vTECjbIkglkkQ==
+ dependencies:
+ postcss "^7.0.14"
+
+ieee754@^1.1.4:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
+ integrity sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=
+
+iferr@^0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
+ integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
+
+ignore-walk@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
+ integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==
+ dependencies:
+ minimatch "^3.0.4"
+
+import-cwd@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
+ integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk=
+ dependencies:
+ import-from "^2.1.0"
+
+import-fresh@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
+ integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY=
+ dependencies:
+ caller-path "^2.0.0"
+ resolve-from "^3.0.0"
+
+import-from@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1"
+ integrity sha1-M1238qev/VOqpHHUuAId7ja387E=
+ dependencies:
+ resolve-from "^3.0.0"
+
+import-local@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
+ integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==
+ dependencies:
+ pkg-dir "^3.0.0"
+ resolve-cwd "^2.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+ integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
+
+in-publish@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51"
+ integrity sha1-4g/146KvwmkDILbcVSaCqcf631E=
+
+indent-string@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
+ integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=
+ dependencies:
+ repeating "^2.0.0"
+
+indexes-of@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+ integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
+
+indexof@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
+ integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+ integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
+inherits@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+ integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
+
+ini@^1.3.4, ini@~1.3.0:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
+ integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+
+internal-ip@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.2.0.tgz#46e81b638d84c338e5c67e42b1a17db67d0814fa"
+ integrity sha512-ZY8Rk+hlvFeuMmG5uH1MXhhdeMntmIaxaInvAmzMq/SHV8rv4Kh+6GiQNNDQd0wZFrcO+FiTBo8lui/osKOyJw==
+ dependencies:
+ default-gateway "^4.0.1"
+ ipaddr.js "^1.9.0"
+
+interpret@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
+ integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==
+
+invariant@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
+ integrity sha1-nh9WrArNtr8wMwbzOL47IErmA2A=
+ dependencies:
+ loose-envify "^1.0.0"
+
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+ integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
+
+invert-kv@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
+ integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
+
+ip-regex@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
+ integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
+
+ip@^1.1.0, ip@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
+ integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
+
+ipaddr.js@1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0"
+ integrity sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=
+
+ipaddr.js@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
+ integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==
+
+is-absolute-url@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
+ integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=
+
+is-accessor-descriptor@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+ integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+ integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+ dependencies:
+ kind-of "^6.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"
+ integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
+
+is-arrayish@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.1.tgz#c2dfc386abaa0c3e33c48db3fe87059e69065efd"
+ integrity sha1-wt/DhquqDD4zxI2z/ocFnmkGXv0=
+
+is-binary-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+ integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=
+ dependencies:
+ binary-extensions "^1.0.0"
+
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+ integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
+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"
+ integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74=
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-callable@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
+ integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==
+
+is-color-stop@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345"
+ integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=
+ dependencies:
+ css-color-names "^0.0.4"
+ hex-color-regex "^1.1.0"
+ hsl-regex "^1.0.0"
+ hsla-regex "^1.0.0"
+ rgb-regex "^1.0.1"
+ rgba-regex "^1.0.0"
+
+is-data-descriptor@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+ integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+ integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+ dependencies:
+ kind-of "^6.0.0"
+
+is-date-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+ integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=
+
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+ dependencies:
+ is-accessor-descriptor "^0.1.6"
+ is-data-descriptor "^0.1.4"
+ kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+ integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+ dependencies:
+ is-accessor-descriptor "^1.0.0"
+ is-data-descriptor "^1.0.0"
+ kind-of "^6.0.2"
+
+is-directory@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
+ integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+ integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+ dependencies:
+ is-plain-object "^2.0.4"
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
+ 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"
+ integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+is-glob@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+ integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
+ dependencies:
+ is-extglob "^2.1.0"
+
+is-glob@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0"
+ integrity sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-obj@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
+ integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
+
+is-odd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-1.0.0.tgz#3b8a932eb028b3775c39bb09e91767accdb69088"
+ integrity sha1-O4qTLrAos3dcObsJ6RdnrM22kIg=
+ dependencies:
+ is-number "^3.0.0"
+
+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"
+ integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=
+
+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"
+ integrity sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=
+ 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"
+ integrity sha1-jvW33lBDej/cprToZe96pVy0gDY=
+ dependencies:
+ path-is-inside "^1.0.1"
+
+is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ dependencies:
+ isobject "^3.0.1"
+
+is-regex@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
+ integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=
+ dependencies:
+ has "^1.0.1"
+
+is-resolvable@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
+ integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
+
+is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+ integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+
+is-svg@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75"
+ integrity sha512-gi4iHK53LR2ujhLVVj+37Ykh9GLqYHX6JOVXbLAucaG/Cqw9xwdFOjDM2qeifLs1sF1npXXFvDu0r5HNgCMrzQ==
+ dependencies:
+ html-comment-regex "^1.1.0"
+
+is-symbol@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38"
+ integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==
+ dependencies:
+ has-symbols "^1.0.0"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+ integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-utf8@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+ integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
+
+is-windows@^1.0.1, is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+ integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+is-wsl@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+ integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+
+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"
+ integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+ dependencies:
+ isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+ integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+js-base64@^2.1.8:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582"
+ integrity sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw==
+
+js-levenshtein@^1.1.3:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
+ integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
+
+js-tokens@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+ integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
+
+js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@^3.12.0, js-yaml@^3.12.2, js-yaml@^3.9.0:
+ version "3.12.2"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.2.tgz#ef1d067c5a9d9cb65bd72f285b5d8105c77f14fc"
+ integrity sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q==
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+ integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+jsesc@^2.5.1:
+ version "2.5.2"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
+ integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
+
+jsesc@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+ integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
+
+json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+ integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+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"
+ integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+ integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+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"
+ integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=
+ dependencies:
+ jsonify "~0.0.0"
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+ integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+json3@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
+ integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=
+
+json5@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+ integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=
+
+json5@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
+ integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
+ dependencies:
+ minimist "^1.2.0"
+
+json5@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850"
+ integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==
+ dependencies:
+ minimist "^1.2.0"
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+ integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+killable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b"
+ integrity sha1-2ouEvUfeU5WHj5XWTQLyRJ/gXms=
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+ 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"
+ integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0, kind-of@^5.0.2:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+ integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+ integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
+
+last-call-webpack-plugin@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555"
+ integrity sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==
+ dependencies:
+ lodash "^4.17.5"
+ webpack-sources "^1.1.0"
+
+lazy-cache@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264"
+ integrity sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=
+ dependencies:
+ set-getter "^0.1.0"
+
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=
+ dependencies:
+ invert-kv "^1.0.0"
+
+lcid@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
+ integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==
+ dependencies:
+ invert-kv "^2.0.0"
+
+load-json-file@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+ integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+
+loader-runner@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
+ integrity sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=
+
+loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
+ integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=
+ dependencies:
+ big.js "^3.1.3"
+ emojis-list "^2.0.0"
+ json5 "^0.5.0"
+
+loader-utils@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
+ integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
+ dependencies:
+ big.js "^5.2.2"
+ emojis-list "^2.0.0"
+ json5 "^1.0.1"
+
+locate-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+ integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+ dependencies:
+ p-locate "^3.0.0"
+ path-exists "^3.0.0"
+
+lodash._reinterpolate@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
+ integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
+
+lodash.assign@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
+ integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=
+
+lodash.clonedeep@^4.3.2:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+ integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
+
+lodash.get@^4.0, lodash.get@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+ integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
+
+lodash.has@^4.0:
+ version "4.5.2"
+ resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862"
+ integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=
+
+lodash.memoize@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+ integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
+lodash.mergewith@^4.6.0:
+ version "4.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
+ integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==
+
+lodash.tail@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664"
+ integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=
+
+lodash.template@^4.2.4:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0"
+ integrity sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=
+ dependencies:
+ lodash._reinterpolate "~3.0.0"
+ lodash.templatesettings "^4.0.0"
+
+lodash.templatesettings@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316"
+ integrity sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=
+ dependencies:
+ lodash._reinterpolate "~3.0.0"
+
+lodash.uniq@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+ integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+
+lodash@3.x:
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
+ integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
+
+lodash@^4.0.0, lodash@~4.17.4:
+ version "4.17.5"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
+ integrity sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==
+
+lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5:
+ version "4.17.11"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
+ integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
+
+loglevel@^1.4.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa"
+ integrity sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=
+
+loose-envify@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+ integrity sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=
+ dependencies:
+ js-tokens "^3.0.0"
+
+loud-rejection@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
+ integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=
+ dependencies:
+ currently-unhandled "^0.4.1"
+ signal-exit "^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"
+ integrity sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+lru-cache@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
+make-dir@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51"
+ integrity sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA==
+ dependencies:
+ pify "^3.0.0"
+
+mamacro@^0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4"
+ integrity sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==
+
+map-age-cleaner@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
+ integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
+ dependencies:
+ p-defer "^1.0.0"
+
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+ integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+
+map-obj@^1.0.0, map-obj@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+ integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
+
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+ dependencies:
+ object-visit "^1.0.0"
+
+md5.js@^1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d"
+ integrity sha1-6b296UogpawYsENA/Fdk1bCdkB0=
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
+mdn-data@~1.1.0:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01"
+ integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+ integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+
+mem@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-4.1.0.tgz#aeb9be2d21f47e78af29e4ac5978e8afa2ca5b8a"
+ integrity sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==
+ dependencies:
+ map-age-cleaner "^0.1.1"
+ mimic-fn "^1.0.0"
+ p-is-promise "^2.0.0"
+
+memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
+ integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=
+ dependencies:
+ errno "^0.1.3"
+ readable-stream "^2.0.1"
+
+meow@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
+ integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=
+ dependencies:
+ camelcase-keys "^2.0.0"
+ decamelize "^1.1.2"
+ loud-rejection "^1.0.0"
+ map-obj "^1.0.1"
+ minimist "^1.1.3"
+ normalize-package-data "^2.3.4"
+ object-assign "^4.0.1"
+ read-pkg-up "^1.0.1"
+ redent "^1.0.0"
+ trim-newlines "^1.0.0"
+
+merge-descriptors@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
+ integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
+
+methods@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
+ integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
+
+micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.8:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
+micromatch@^3.1.4:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.5.tgz#d05e168c206472dfbca985bfef4f57797b4cd4ba"
+ integrity sha512-ykttrLPQrz1PUJcXjwsTUjGoPJ64StIGNE2lGVD1c9CuguJ+L7/navsE8IcDNndOoCMvYV0qc/exfVbMHkUhvA==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.0"
+ define-property "^1.0.0"
+ extend-shallow "^2.0.1"
+ extglob "^2.0.2"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.0"
+ nanomatch "^1.2.5"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+miller-rabin@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+ integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==
+ dependencies:
+ bn.js "^4.0.0"
+ brorand "^1.0.1"
+
+"mime-db@>= 1.30.0 < 2":
+ version "1.32.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.32.0.tgz#485b3848b01a3cda5f968b4882c0771e58e09414"
+ integrity sha512-+ZWo/xZN40Tt6S+HyakUxnSOgff+JEdaneLWIm0Z6LmpCn5DMcZntLyUY5c/rTDog28LhXLKOUZKoTxTCAdBVw==
+
+mime-db@~1.30.0:
+ version "1.30.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
+ integrity sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=
+
+mime-db@~1.38.0:
+ version "1.38.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad"
+ integrity sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==
+
+mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7:
+ version "2.1.17"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
+ integrity sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=
+ dependencies:
+ mime-db "~1.30.0"
+
+mime-types@~2.1.19:
+ version "2.1.22"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd"
+ integrity sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==
+ dependencies:
+ mime-db "~1.38.0"
+
+mime@1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
+ integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
+
+mime@^2.3.1:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.0.tgz#e051fd881358585f3279df333fe694da0bcffdd6"
+ integrity sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==
+
+mimic-fn@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
+ integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
+
+mini-css-extract-plugin@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0"
+ integrity sha512-IuaLjruM0vMKhUUT51fQdQzBYTX49dLj8w68ALEAe2A4iYNpIC4eMac67mt3NzycvjOlf07/kYxJDc0RTl1Wqw==
+ dependencies:
+ loader-utils "^1.1.0"
+ schema-utils "^1.0.0"
+ webpack-sources "^1.1.0"
+
+minimalistic-assert@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
+ integrity sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=
+
+minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+ integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
+
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+ 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"
+ integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
+
+minimist@^1.1.3, minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+ integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
+
+minipass@^2.2.1, minipass@^2.3.4:
+ version "2.3.5"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
+ integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==
+ dependencies:
+ safe-buffer "^5.1.2"
+ yallist "^3.0.0"
+
+minizlib@^1.1.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614"
+ integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==
+ dependencies:
+ minipass "^2.2.1"
+
+mississippi@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
+ integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==
+ dependencies:
+ concat-stream "^1.5.0"
+ duplexify "^3.4.2"
+ end-of-stream "^1.1.0"
+ flush-write-stream "^1.0.0"
+ from2 "^2.1.0"
+ parallel-transform "^1.1.0"
+ pump "^3.0.0"
+ pumpify "^1.3.3"
+ stream-each "^1.1.0"
+ through2 "^2.0.0"
+
+mixin-deep@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
+ integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
+mixin-object@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e"
+ integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=
+ dependencies:
+ for-in "^0.1.3"
+ is-extendable "^0.1.1"
+
+mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
+ dependencies:
+ minimist "0.0.8"
+
+move-concurrently@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
+ integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=
+ dependencies:
+ aproba "^1.1.1"
+ copy-concurrently "^1.0.0"
+ fs-write-stream-atomic "^1.0.8"
+ mkdirp "^0.5.1"
+ rimraf "^2.5.4"
+ run-queue "^1.0.3"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+ integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+
+multicast-dns-service-types@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
+ integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=
+
+multicast-dns@^6.0.1:
+ version "6.2.3"
+ resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229"
+ integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==
+ dependencies:
+ dns-packet "^1.3.1"
+ thunky "^1.0.2"
+
+nan@^2.10.0, nan@^2.9.2:
+ version "2.12.1"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
+ integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
+
+nan@^2.3.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
+ integrity sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=
+
+nanomatch@^1.2.5:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79"
+ integrity sha512-/5ldsnyurvEw7wNpxLFgjVvBLMta43niEYOy0CJ4ntcYSbx6bugRUTQeFb4BR/WanEL1o3aQgHuVLHQaB6tOqg==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ is-odd "^1.0.0"
+ kind-of "^5.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+nanomatch@^1.2.9:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+ integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ fragment-cache "^0.2.1"
+ is-windows "^1.0.2"
+ kind-of "^6.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+needle@^2.2.1:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e"
+ integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==
+ dependencies:
+ debug "^2.1.2"
+ iconv-lite "^0.4.4"
+ sax "^1.2.4"
+
+negotiator@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
+ integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=
+
+neo-async@^2.5.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835"
+ integrity sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==
+
+nice-try@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+ integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+node-forge@0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300"
+ integrity sha1-naYR6giYL0uUIGs760zJZl8gwwA=
+
+node-gyp@^3.8.0:
+ version "3.8.0"
+ resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
+ integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==
+ dependencies:
+ fstream "^1.0.0"
+ glob "^7.0.3"
+ graceful-fs "^4.1.2"
+ mkdirp "^0.5.0"
+ nopt "2 || 3"
+ npmlog "0 || 1 || 2 || 3 || 4"
+ osenv "0"
+ request "^2.87.0"
+ rimraf "2"
+ semver "~5.3.0"
+ tar "^2.0.0"
+ which "1"
+
+node-libs-browser@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
+ integrity sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==
+ dependencies:
+ assert "^1.1.1"
+ browserify-zlib "^0.2.0"
+ buffer "^4.3.0"
+ console-browserify "^1.1.0"
+ constants-browserify "^1.0.0"
+ crypto-browserify "^3.11.0"
+ domain-browser "^1.1.1"
+ events "^1.0.0"
+ https-browserify "^1.0.0"
+ os-browserify "^0.3.0"
+ path-browserify "0.0.0"
+ process "^0.11.10"
+ punycode "^1.2.4"
+ querystring-es3 "^0.2.0"
+ readable-stream "^2.3.3"
+ stream-browserify "^2.0.1"
+ stream-http "^2.7.2"
+ string_decoder "^1.0.0"
+ timers-browserify "^2.0.4"
+ tty-browserify "0.0.0"
+ url "^0.11.0"
+ util "^0.10.3"
+ vm-browserify "0.0.4"
+
+node-pre-gyp@^0.10.0:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
+ integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==
+ dependencies:
+ detect-libc "^1.0.2"
+ mkdirp "^0.5.1"
+ needle "^2.2.1"
+ nopt "^4.0.1"
+ npm-packlist "^1.1.6"
+ npmlog "^4.0.2"
+ rc "^1.2.7"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^4"
+
+node-pre-gyp@^0.6.39:
+ version "0.6.39"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649"
+ integrity sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==
+ dependencies:
+ detect-libc "^1.0.2"
+ hawk "3.1.3"
+ mkdirp "^0.5.1"
+ nopt "^4.0.1"
+ npmlog "^4.0.2"
+ rc "^1.1.7"
+ request "2.81.0"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^2.2.1"
+ tar-pack "^3.4.0"
+
+node-releases@^1.1.8:
+ version "1.1.9"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.9.tgz#70d0985ec4bf7de9f08fc481f5dae111889ca482"
+ integrity sha512-oic3GT4OtbWWKfRolz5Syw0Xus0KRFxeorLNj0s93ofX6PWyuzKjsiGxsCtWktBwwmTF6DdRRf2KreGqeOk5KA==
+ dependencies:
+ semver "^5.3.0"
+
+node-sass@^4.11.0:
+ version "4.11.0"
+ resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a"
+ integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==
+ dependencies:
+ async-foreach "^0.1.3"
+ chalk "^1.1.1"
+ cross-spawn "^3.0.0"
+ gaze "^1.0.0"
+ get-stdin "^4.0.1"
+ glob "^7.0.3"
+ in-publish "^2.0.0"
+ lodash.assign "^4.2.0"
+ lodash.clonedeep "^4.3.2"
+ lodash.mergewith "^4.6.0"
+ meow "^3.7.0"
+ mkdirp "^0.5.1"
+ nan "^2.10.0"
+ node-gyp "^3.8.0"
+ npmlog "^4.0.0"
+ request "^2.88.0"
+ sass-graph "^2.2.4"
+ stdout-stream "^1.4.0"
+ "true-case-path" "^1.0.2"
+
+"nopt@2 || 3":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
+ integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k=
+ dependencies:
+ abbrev "1"
+
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==
+ 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.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+normalize-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+normalize-range@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+ integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
+
+normalize-url@^3.0.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
+ integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==
+
+npm-bundled@^1.0.1:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd"
+ integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==
+
+npm-packlist@^1.1.6:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.1.tgz#19064cdf988da80ea3cee45533879d90192bbfbc"
+ integrity sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==
+ dependencies:
+ ignore-walk "^3.0.1"
+ npm-bundled "^1.0.1"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
+ dependencies:
+ path-key "^2.0.0"
+
+"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
+nth-check@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
+ integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
+ dependencies:
+ boolbase "~1.0.0"
+
+num2fraction@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+ integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=
+
+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"
+ integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
+
+oauth-sign@~0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
+ integrity sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=
+
+oauth-sign@~0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+ integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
+object-assign@^4.0.1, object-assign@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+ integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
+object-keys@^1.0.11, object-keys@^1.0.12:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.0.tgz#11bd22348dd2e096a045ab06f6c85bcc340fa032"
+ integrity sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==
+
+object-keys@^1.0.8:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
+ integrity sha1-xUYBd4rVYPEULODgG8yotW0TQm0=
+
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+ dependencies:
+ isobject "^3.0.0"
+
+object.assign@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+ integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ object-keys "^1.0.11"
+
+object.getownpropertydescriptors@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
+ integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.1"
+
+object.pick@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+ integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+ dependencies:
+ isobject "^3.0.1"
+
+object.values@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9"
+ integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==
+ dependencies:
+ define-properties "^1.1.3"
+ es-abstract "^1.12.0"
+ function-bind "^1.1.1"
+ has "^1.0.3"
+
+obuf@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e"
+ integrity sha1-EEEktsYCxnlogaBCVB0220OlJk4=
+
+obuf@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
+ integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+ integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+ dependencies:
+ ee-first "1.1.1"
+
+on-headers@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
+ integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=
+
+once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+ dependencies:
+ wrappy "1"
+
+opn@^5.1.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225"
+ integrity sha512-Jd/GpzPyHF4P2/aNOVmS3lfMSWV9J7cOhCG1s08XCEAsPkB7lp6ddiU0J7XzyQRDUh8BqJ7PchfINjR8jyofRQ==
+ dependencies:
+ is-wsl "^1.1.0"
+
+optimize-css-assets-webpack-plugin@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.1.tgz#9eb500711d35165b45e7fd60ba2df40cb3eb9159"
+ integrity sha512-Rqm6sSjWtx9FchdP0uzTQDc7GXDKnwVEGoSxjezPkzMewx7gEWE9IMUYKmigTRC4U3RaNSwYVnUDLuIdtTpm0A==
+ dependencies:
+ cssnano "^4.1.0"
+ last-call-webpack-plugin "^3.0.0"
+
+original@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f"
+ integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==
+ dependencies:
+ url-parse "^1.4.3"
+
+os-browserify@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
+ integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+ integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
+
+os-locale@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
+ integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=
+ dependencies:
+ lcid "^1.0.0"
+
+os-locale@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
+ integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
+ dependencies:
+ execa "^1.0.0"
+ lcid "^2.0.0"
+ mem "^4.0.0"
+
+os-tmpdir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+ integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+
+osenv@0, osenv@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
+ integrity sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
+p-defer@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
+ integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+ integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
+
+p-is-promise@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.0.0.tgz#7554e3d572109a87e1f3f53f6a7d85d1b194f4c5"
+ integrity sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==
+
+p-limit@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2"
+ integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==
+ dependencies:
+ p-try "^2.0.0"
+
+p-locate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+ integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+ dependencies:
+ p-limit "^2.0.0"
+
+p-map@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
+ integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==
+
+p-try@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
+ integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==
+
+pako@~1.0.5:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
+ integrity sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==
+
+parallel-transform@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06"
+ integrity sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=
+ dependencies:
+ cyclist "~0.2.2"
+ inherits "^2.0.3"
+ readable-stream "^2.1.5"
+
+parse-asn1@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712"
+ integrity sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=
+ dependencies:
+ asn1.js "^4.0.0"
+ browserify-aes "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.0"
+ pbkdf2 "^3.0.3"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=
+ dependencies:
+ error-ex "^1.2.0"
+
+parse-json@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+ integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
+ dependencies:
+ error-ex "^1.3.1"
+ json-parse-better-errors "^1.0.1"
+
+parse-passwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
+ integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
+
+parseurl@~1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
+ integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+ integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+
+path-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
+ integrity sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=
+
+path-complete-extname@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/path-complete-extname/-/path-complete-extname-1.0.0.tgz#f889985dc91000c815515c0bfed06c5acda0752b"
+ integrity sha512-CVjiWcMRdGU8ubs08YQVzhutOR5DEfO97ipRIlOGMK5Bek5nQySknBpuxVAVJ36hseTNs+vdIcv57ZrWxH7zvg==
+
+path-dirname@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+ integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
+
+path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=
+ 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"
+ integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+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"
+ integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-is-inside@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+ integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
+
+path-key@^2.0.0, path-key@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+ integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
+path-parse@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+ integrity sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=
+
+path-parse@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+ integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+
+path-to-regexp@0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
+ integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+
+path-type@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+ integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+pbkdf2@^3.0.3:
+ version "3.0.14"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade"
+ integrity sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==
+ dependencies:
+ create-hash "^1.1.2"
+ create-hmac "^1.1.4"
+ ripemd160 "^2.0.1"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+performance-now@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+ integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+ integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+pify@^2.0.0, pify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+ integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+ integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o=
+ 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"
+ integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
+
+pkg-dir@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
+ integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==
+ dependencies:
+ find-up "^3.0.0"
+
+pnp-webpack-plugin@^1.3.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/pnp-webpack-plugin/-/pnp-webpack-plugin-1.4.1.tgz#e8f8c683b496a71c0d200e664c4bb399a9c9585e"
+ integrity sha512-S4kz+5rvWvD0w1O63eTJeXIxW4JHK0wPRMO7GmPhbZXJnTePcfrWZlni4BoglIf7pLSY18xtqo3MSnVkoAFXKg==
+ dependencies:
+ ts-pnp "^1.0.0"
+
+portfinder@^1.0.9:
+ version "1.0.13"
+ resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
+ integrity sha1-uzLs2HwnEErm7kS1o8y/Drsa7ek=
+ dependencies:
+ async "^1.5.2"
+ debug "^2.2.0"
+ mkdirp "0.5.x"
+
+posix-character-classes@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+ integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+
+postcss-attribute-case-insensitive@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-4.0.1.tgz#b2a721a0d279c2f9103a36331c88981526428cc7"
+ integrity sha512-L2YKB3vF4PetdTIthQVeT+7YiSzMoNMLLYxPXXppOOP7NoazEAy45sh2LvJ8leCQjfBcfkYQs8TtCcQjeZTp8A==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-selector-parser "^5.0.0"
+
+postcss-calc@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.1.tgz#36d77bab023b0ecbb9789d84dcb23c4941145436"
+ integrity sha512-oXqx0m6tb4N3JGdmeMSc/i91KppbYsFZKdH0xMOqK8V1rJlzrKlTdokz8ozUXLVejydRN6u2IddxpcijRj2FqQ==
+ dependencies:
+ css-unit-converter "^1.1.1"
+ postcss "^7.0.5"
+ postcss-selector-parser "^5.0.0-rc.4"
+ postcss-value-parser "^3.3.1"
+
+postcss-color-functional-notation@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-2.0.1.tgz#5efd37a88fbabeb00a2966d1e53d98ced93f74e0"
+ integrity sha512-ZBARCypjEDofW4P6IdPVTLhDNXPRn8T2s1zHbZidW6rPaaZvcnCS2soYFIQJrMZSxiePJ2XIYTlcb2ztr/eT2g==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-color-gray@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-5.0.0.tgz#532a31eb909f8da898ceffe296fdc1f864be8547"
+ integrity sha512-q6BuRnAGKM/ZRpfDascZlIZPjvwsRye7UDNalqVz3s7GDxMtqPY6+Q871liNxsonUw8oC61OG+PSaysYpl1bnw==
+ dependencies:
+ "@csstools/convert-colors" "^1.4.0"
+ postcss "^7.0.5"
+ postcss-values-parser "^2.0.0"
+
+postcss-color-hex-alpha@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-5.0.2.tgz#e9b1886bb038daed33f6394168c210b40bb4fdb6"
+ integrity sha512-8bIOzQMGdZVifoBQUJdw+yIY00omBd2EwkJXepQo9cjp1UOHHHoeRDeSzTP6vakEpaRc6GAIOfvcQR7jBYaG5Q==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-color-mod-function@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-color-mod-function/-/postcss-color-mod-function-3.0.3.tgz#816ba145ac11cc3cb6baa905a75a49f903e4d31d"
+ integrity sha512-YP4VG+xufxaVtzV6ZmhEtc+/aTXH3d0JLpnYfxqTvwZPbJhWqp8bSY3nfNzNRFLgB4XSaBA82OE4VjOOKpCdVQ==
+ dependencies:
+ "@csstools/convert-colors" "^1.4.0"
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-color-rebeccapurple@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-4.0.1.tgz#c7a89be872bb74e45b1e3022bfe5748823e6de77"
+ integrity sha512-aAe3OhkS6qJXBbqzvZth2Au4V3KieR5sRQ4ptb2b2O8wgvB3SJBsdG+jsn2BZbbwekDG8nTfcCNKcSfe/lEy8g==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-colormin@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381"
+ integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==
+ dependencies:
+ browserslist "^4.0.0"
+ color "^3.0.0"
+ has "^1.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-convert-values@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f"
+ integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==
+ dependencies:
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-custom-media@^7.0.7:
+ version "7.0.7"
+ resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-7.0.7.tgz#bbc698ed3089ded61aad0f5bfb1fb48bf6969e73"
+ integrity sha512-bWPCdZKdH60wKOTG4HKEgxWnZVjAIVNOJDvi3lkuTa90xo/K0YHa2ZnlKLC5e2qF8qCcMQXt0yzQITBp8d0OFA==
+ dependencies:
+ postcss "^7.0.5"
+
+postcss-custom-properties@^8.0.9:
+ version "8.0.9"
+ resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-8.0.9.tgz#8943870528a6eae4c8e8d285b6ccc9fd1f97e69c"
+ integrity sha512-/Lbn5GP2JkKhgUO2elMs4NnbUJcvHX4AaF5nuJDaNkd2chYW1KA5qtOGGgdkBEWcXtKSQfHXzT7C6grEVyb13w==
+ dependencies:
+ postcss "^7.0.5"
+ postcss-values-parser "^2.0.0"
+
+postcss-custom-selectors@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-5.1.2.tgz#64858c6eb2ecff2fb41d0b28c9dd7b3db4de7fba"
+ integrity sha512-DSGDhqinCqXqlS4R7KGxL1OSycd1lydugJ1ky4iRXPHdBRiozyMHrdu0H3o7qNOCiZwySZTUI5MV0T8QhCLu+w==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-selector-parser "^5.0.0-rc.3"
+
+postcss-dir-pseudo-class@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-5.0.0.tgz#6e3a4177d0edb3abcc85fdb6fbb1c26dabaeaba2"
+ integrity sha512-3pm4oq8HYWMZePJY+5ANriPs3P07q+LW6FAdTlkFH2XqDdP4HeeJYMOzn0HYLhRSjBO3fhiqSwwU9xEULSrPgw==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-selector-parser "^5.0.0-rc.3"
+
+postcss-discard-comments@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033"
+ integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-discard-duplicates@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb"
+ integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-discard-empty@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765"
+ integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-discard-overridden@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57"
+ integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-double-position-gradients@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-1.0.0.tgz#fc927d52fddc896cb3a2812ebc5df147e110522e"
+ integrity sha512-G+nV8EnQq25fOI8CH/B6krEohGWnF5+3A6H/+JEpOncu5dCnkS1QQ6+ct3Jkaepw1NGVqqOZH6lqrm244mCftA==
+ dependencies:
+ postcss "^7.0.5"
+ postcss-values-parser "^2.0.0"
+
+postcss-env-function@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-env-function/-/postcss-env-function-2.0.2.tgz#0f3e3d3c57f094a92c2baf4b6241f0b0da5365d7"
+ integrity sha512-rwac4BuZlITeUbiBq60h/xbLzXY43qOsIErngWa4l7Mt+RaSkT7QBjXVGTcBHupykkblHMDrBFh30zchYPaOUw==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-flexbugs-fixes@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.1.0.tgz#e094a9df1783e2200b7b19f875dcad3b3aff8b20"
+ integrity sha512-jr1LHxQvStNNAHlgco6PzY308zvLklh7SJVYuWUwyUQncofaAlD2l+P/gxKHOdqWKe7xJSkVLFF/2Tp+JqMSZA==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-focus-visible@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-4.0.0.tgz#477d107113ade6024b14128317ade2bd1e17046e"
+ integrity sha512-Z5CkWBw0+idJHSV6+Bgf2peDOFf/x4o+vX/pwcNYrWpXFrSfTkQ3JQ1ojrq9yS+upnAlNRHeg8uEwFTgorjI8g==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-focus-within@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-3.0.0.tgz#763b8788596cee9b874c999201cdde80659ef680"
+ integrity sha512-W0APui8jQeBKbCGZudW37EeMCjDeVxKgiYfIIEo8Bdh5SpB9sxds/Iq8SEuzS0Q4YFOlG7EPFulbbxujpkrV2w==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-font-variant@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-4.0.0.tgz#71dd3c6c10a0d846c5eda07803439617bbbabacc"
+ integrity sha512-M8BFYKOvCrI2aITzDad7kWuXXTm0YhGdP9Q8HanmN4EF1Hmcgs1KK5rSHylt/lUJe8yLxiSwWAHdScoEiIxztg==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-gap-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-2.0.0.tgz#431c192ab3ed96a3c3d09f2ff615960f902c1715"
+ integrity sha512-QZSqDaMgXCHuHTEzMsS2KfVDOq7ZFiknSpkrPJY6jmxbugUPTuSzs/vuE5I3zv0WAS+3vhrlqhijiprnuQfzmg==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-image-set-function@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-3.0.1.tgz#28920a2f29945bed4c3198d7df6496d410d3f288"
+ integrity sha512-oPTcFFip5LZy8Y/whto91L9xdRHCWEMs3e1MdJxhgt4jy2WYXfhkng59fH5qLXSCPN8k4n94p1Czrfe5IOkKUw==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-import@^12.0.1:
+ version "12.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153"
+ integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw==
+ dependencies:
+ postcss "^7.0.1"
+ postcss-value-parser "^3.2.3"
+ read-cache "^1.0.0"
+ resolve "^1.1.7"
+
+postcss-initial@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-3.0.0.tgz#1772512faf11421b791fb2ca6879df5f68aa0517"
+ integrity sha512-WzrqZ5nG9R9fUtrA+we92R4jhVvEB32IIRTzfIG/PLL8UV4CvbF1ugTEHEFX6vWxl41Xt5RTCJPEZkuWzrOM+Q==
+ dependencies:
+ lodash.template "^4.2.4"
+ postcss "^7.0.2"
+
+postcss-lab-function@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-2.0.1.tgz#bb51a6856cd12289ab4ae20db1e3821ef13d7d2e"
+ integrity sha512-whLy1IeZKY+3fYdqQFuDBf8Auw+qFuVnChWjmxm/UhHWqNHZx+B99EwxTvGYmUBqe3Fjxs4L1BoZTJmPu6usVg==
+ dependencies:
+ "@csstools/convert-colors" "^1.4.0"
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-load-config@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.0.0.tgz#f1312ddbf5912cd747177083c5ef7a19d62ee484"
+ integrity sha512-V5JBLzw406BB8UIfsAWSK2KSwIJ5yoEIVFb4gVkXci0QdKgA24jLmHZ/ghe/GgX0lJ0/D1uUK1ejhzEY94MChQ==
+ dependencies:
+ cosmiconfig "^4.0.0"
+ import-cwd "^2.0.0"
+
+postcss-loader@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d"
+ integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==
+ dependencies:
+ loader-utils "^1.1.0"
+ postcss "^7.0.0"
+ postcss-load-config "^2.0.0"
+ schema-utils "^1.0.0"
+
+postcss-logical@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-3.0.0.tgz#2495d0f8b82e9f262725f75f9401b34e7b45d5b5"
+ integrity sha512-1SUKdJc2vuMOmeItqGuNaC+N8MzBWFWEkAnRnLpFYj1tGGa7NqyVBujfRtgNa2gXR+6RkGUiB2O5Vmh7E2RmiA==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-media-minmax@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-4.0.0.tgz#b75bb6cbc217c8ac49433e12f22048814a4f5ed5"
+ integrity sha512-fo9moya6qyxsjbFAYl97qKO9gyre3qvbMnkOZeZwlsW6XYFsvs2DMGDlchVLfAd8LHPZDxivu/+qW2SMQeTHBw==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-merge-longhand@^4.0.11:
+ version "4.0.11"
+ resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24"
+ integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==
+ dependencies:
+ css-color-names "0.0.4"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+ stylehacks "^4.0.0"
+
+postcss-merge-rules@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650"
+ integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==
+ dependencies:
+ browserslist "^4.0.0"
+ caniuse-api "^3.0.0"
+ cssnano-util-same-parent "^4.0.0"
+ postcss "^7.0.0"
+ postcss-selector-parser "^3.0.0"
+ vendors "^1.0.0"
+
+postcss-minify-font-values@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6"
+ integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==
+ dependencies:
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-minify-gradients@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471"
+ integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==
+ dependencies:
+ cssnano-util-get-arguments "^4.0.0"
+ is-color-stop "^1.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-minify-params@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874"
+ integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==
+ dependencies:
+ alphanum-sort "^1.0.0"
+ browserslist "^4.0.0"
+ cssnano-util-get-arguments "^4.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+ uniqs "^2.0.0"
+
+postcss-minify-selectors@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8"
+ integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==
+ dependencies:
+ alphanum-sort "^1.0.0"
+ has "^1.0.0"
+ postcss "^7.0.0"
+ postcss-selector-parser "^3.0.0"
+
+postcss-modules-extract-imports@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e"
+ integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==
+ dependencies:
+ postcss "^7.0.5"
+
+postcss-modules-local-by-default@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz#dd9953f6dd476b5fd1ef2d8830c8929760b56e63"
+ integrity sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA==
+ dependencies:
+ postcss "^7.0.6"
+ postcss-selector-parser "^6.0.0"
+ postcss-value-parser "^3.3.1"
+
+postcss-modules-scope@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.1.0.tgz#ad3f5bf7856114f6fcab901b0502e2a2bc39d4eb"
+ integrity sha512-91Rjps0JnmtUB0cujlc8KIKCsJXWjzuxGeT/+Q2i2HXKZ7nBUeF9YQTZZTNvHVoNYj1AthsjnGLtqDUE0Op79A==
+ dependencies:
+ postcss "^7.0.6"
+ postcss-selector-parser "^6.0.0"
+
+postcss-modules-values@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz#479b46dc0c5ca3dc7fa5270851836b9ec7152f64"
+ integrity sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w==
+ dependencies:
+ icss-replace-symbols "^1.1.0"
+ postcss "^7.0.6"
+
+postcss-nesting@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-7.0.0.tgz#6e26a770a0c8fcba33782a6b6f350845e1a448f6"
+ integrity sha512-WSsbVd5Ampi3Y0nk/SKr5+K34n52PqMqEfswu6RtU4r7wA8vSD+gM8/D9qq4aJkHImwn1+9iEFTbjoWsQeqtaQ==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-normalize-charset@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4"
+ integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-normalize-display-values@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a"
+ integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==
+ dependencies:
+ cssnano-util-get-match "^4.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-positions@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f"
+ integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==
+ dependencies:
+ cssnano-util-get-arguments "^4.0.0"
+ has "^1.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-repeat-style@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c"
+ integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==
+ dependencies:
+ cssnano-util-get-arguments "^4.0.0"
+ cssnano-util-get-match "^4.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-string@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c"
+ integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==
+ dependencies:
+ has "^1.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-timing-functions@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9"
+ integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==
+ dependencies:
+ cssnano-util-get-match "^4.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-unicode@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb"
+ integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==
+ dependencies:
+ browserslist "^4.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-url@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1"
+ integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==
+ dependencies:
+ is-absolute-url "^2.0.0"
+ normalize-url "^3.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-normalize-whitespace@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82"
+ integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==
+ dependencies:
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-ordered-values@^4.1.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee"
+ integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==
+ dependencies:
+ cssnano-util-get-arguments "^4.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-overflow-shorthand@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-2.0.0.tgz#31ecf350e9c6f6ddc250a78f0c3e111f32dd4c30"
+ integrity sha512-aK0fHc9CBNx8jbzMYhshZcEv8LtYnBIRYQD5i7w/K/wS9c2+0NSR6B3OVMu5y0hBHYLcMGjfU+dmWYNKH0I85g==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-page-break@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-2.0.0.tgz#add52d0e0a528cabe6afee8b46e2abb277df46bf"
+ integrity sha512-tkpTSrLpfLfD9HvgOlJuigLuk39wVTbbd8RKcy8/ugV2bNBUW3xU+AIqyxhDrQr1VUj1RmyJrBn1YWrqUm9zAQ==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-place@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-4.0.1.tgz#e9f39d33d2dc584e46ee1db45adb77ca9d1dcc62"
+ integrity sha512-Zb6byCSLkgRKLODj/5mQugyuj9bvAAw9LqJJjgwz5cYryGeXfFZfSXoP1UfveccFmeq0b/2xxwcTEVScnqGxBg==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-values-parser "^2.0.0"
+
+postcss-preset-env@^6.6.0:
+ version "6.6.0"
+ resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-6.6.0.tgz#642e7d962e2bdc2e355db117c1eb63952690ed5b"
+ integrity sha512-I3zAiycfqXpPIFD6HXhLfWXIewAWO8emOKz+QSsxaUZb9Dp8HbF5kUf+4Wy/AxR33o+LRoO8blEWCHth0ZsCLA==
+ dependencies:
+ autoprefixer "^9.4.9"
+ browserslist "^4.4.2"
+ caniuse-lite "^1.0.30000939"
+ css-blank-pseudo "^0.1.4"
+ css-has-pseudo "^0.10.0"
+ css-prefers-color-scheme "^3.1.1"
+ cssdb "^4.3.0"
+ postcss "^7.0.14"
+ postcss-attribute-case-insensitive "^4.0.1"
+ postcss-color-functional-notation "^2.0.1"
+ postcss-color-gray "^5.0.0"
+ postcss-color-hex-alpha "^5.0.2"
+ postcss-color-mod-function "^3.0.3"
+ postcss-color-rebeccapurple "^4.0.1"
+ postcss-custom-media "^7.0.7"
+ postcss-custom-properties "^8.0.9"
+ postcss-custom-selectors "^5.1.2"
+ postcss-dir-pseudo-class "^5.0.0"
+ postcss-double-position-gradients "^1.0.0"
+ postcss-env-function "^2.0.2"
+ postcss-focus-visible "^4.0.0"
+ postcss-focus-within "^3.0.0"
+ postcss-font-variant "^4.0.0"
+ postcss-gap-properties "^2.0.0"
+ postcss-image-set-function "^3.0.1"
+ postcss-initial "^3.0.0"
+ postcss-lab-function "^2.0.1"
+ postcss-logical "^3.0.0"
+ postcss-media-minmax "^4.0.0"
+ postcss-nesting "^7.0.0"
+ postcss-overflow-shorthand "^2.0.0"
+ postcss-page-break "^2.0.0"
+ postcss-place "^4.0.1"
+ postcss-pseudo-class-any-link "^6.0.0"
+ postcss-replace-overflow-wrap "^3.0.0"
+ postcss-selector-matches "^4.0.0"
+ postcss-selector-not "^4.0.0"
+
+postcss-pseudo-class-any-link@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-6.0.0.tgz#2ed3eed393b3702879dec4a87032b210daeb04d1"
+ integrity sha512-lgXW9sYJdLqtmw23otOzrtbDXofUdfYzNm4PIpNE322/swES3VU9XlXHeJS46zT2onFO7V1QFdD4Q9LiZj8mew==
+ dependencies:
+ postcss "^7.0.2"
+ postcss-selector-parser "^5.0.0-rc.3"
+
+postcss-reduce-initial@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df"
+ integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==
+ dependencies:
+ browserslist "^4.0.0"
+ caniuse-api "^3.0.0"
+ has "^1.0.0"
+ postcss "^7.0.0"
+
+postcss-reduce-transforms@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29"
+ integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==
+ dependencies:
+ cssnano-util-get-match "^4.0.0"
+ has "^1.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+
+postcss-replace-overflow-wrap@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-3.0.0.tgz#61b360ffdaedca84c7c918d2b0f0d0ea559ab01c"
+ integrity sha512-2T5hcEHArDT6X9+9dVSPQdo7QHzG4XKclFT8rU5TzJPDN7RIRTbO9c4drUISOVemLj03aezStHCR2AIcr8XLpw==
+ dependencies:
+ postcss "^7.0.2"
+
+postcss-safe-parser@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-4.0.1.tgz#8756d9e4c36fdce2c72b091bbc8ca176ab1fcdea"
+ integrity sha512-xZsFA3uX8MO3yAda03QrG3/Eg1LN3EPfjjf07vke/46HERLZyHrTsQ9E1r1w1W//fWEhtYNndo2hQplN2cVpCQ==
+ dependencies:
+ postcss "^7.0.0"
+
+postcss-selector-matches@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-4.0.0.tgz#71c8248f917ba2cc93037c9637ee09c64436fcff"
+ integrity sha512-LgsHwQR/EsRYSqlwdGzeaPKVT0Ml7LAT6E75T8W8xLJY62CE4S/l03BWIt3jT8Taq22kXP08s2SfTSzaraoPww==
+ dependencies:
+ balanced-match "^1.0.0"
+ postcss "^7.0.2"
+
+postcss-selector-not@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-4.0.0.tgz#c68ff7ba96527499e832724a2674d65603b645c0"
+ integrity sha512-W+bkBZRhqJaYN8XAnbbZPLWMvZD1wKTu0UxtFKdhtGjWYmxhkUneoeOhRJKdAE5V7ZTlnbHfCR+6bNwK9e1dTQ==
+ dependencies:
+ balanced-match "^1.0.0"
+ postcss "^7.0.2"
+
+postcss-selector-parser@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865"
+ integrity sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=
+ dependencies:
+ dot-prop "^4.1.1"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
+postcss-selector-parser@^5.0.0, postcss-selector-parser@^5.0.0-rc.3, postcss-selector-parser@^5.0.0-rc.4:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c"
+ integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==
+ dependencies:
+ cssesc "^2.0.0"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
+postcss-selector-parser@^6.0.0:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
+ integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
+ dependencies:
+ cssesc "^3.0.0"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
+postcss-svgo@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-4.0.2.tgz#17b997bc711b333bab143aaed3b8d3d6e3d38258"
+ integrity sha512-C6wyjo3VwFm0QgBy+Fu7gCYOkCmgmClghO+pjcxvrcBKtiKt0uCF+hvbMO1fyv5BMImRK90SMb+dwUnfbGd+jw==
+ dependencies:
+ is-svg "^3.0.0"
+ postcss "^7.0.0"
+ postcss-value-parser "^3.0.0"
+ svgo "^1.0.0"
+
+postcss-unique-selectors@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac"
+ integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==
+ dependencies:
+ alphanum-sort "^1.0.0"
+ postcss "^7.0.0"
+ uniqs "^2.0.0"
+
+postcss-value-parser@^3.0.0, postcss-value-parser@^3.3.1:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281"
+ integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==
+
+postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
+ integrity sha1-h/OPnxj3dKSrTIojL1xc6IcqnRU=
+
+postcss-values-parser@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/postcss-values-parser/-/postcss-values-parser-2.0.1.tgz#da8b472d901da1e205b47bdc98637b9e9e550e5f"
+ integrity sha512-2tLuBsA6P4rYTNKCXYG/71C7j1pU6pK503suYOmn4xYrQIzW+opD+7FAFNuGSdZC/3Qfy334QbeMu7MEb8gOxg==
+ dependencies:
+ flatten "^1.0.2"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
+postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.2, postcss@^7.0.5, postcss@^7.0.6:
+ version "7.0.14"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.14.tgz#4527ed6b1ca0d82c53ce5ec1a2041c2346bbd6e5"
+ integrity sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg==
+ dependencies:
+ chalk "^2.4.2"
+ source-map "^0.6.1"
+ supports-color "^6.1.0"
+
+private@^0.1.6:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+ integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
+
+process-nextick-args@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
+ integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
+
+process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+ integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
+
+promise-inflight@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
+ integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+
+proxy-addr@~2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec"
+ integrity sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=
+ dependencies:
+ forwarded "~0.1.2"
+ ipaddr.js "1.5.2"
+
+prr@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+ integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+ integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
+
+psl@^1.1.24:
+ version "1.1.31"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
+ integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
+
+public-encrypt@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
+ integrity sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=
+ dependencies:
+ bn.js "^4.1.0"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ parse-asn1 "^5.0.0"
+ randombytes "^2.0.1"
+
+pump@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
+ integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+pump@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+ integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+pumpify@^1.3.3:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb"
+ integrity sha512-2kmNR9ry+Pf45opRVirpNuIFotsxUGLaYqxIwuR77AYrYRMuFCz9eryHBS52L360O+NcR383CL4QYlMKPq4zYA==
+ dependencies:
+ duplexify "^3.5.3"
+ inherits "^2.0.3"
+ pump "^2.0.0"
+
+punycode@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+ integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
+
+punycode@^1.2.4, punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+ integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+
+punycode@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+ integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+q@^1.1.2:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
+ integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
+
+qs@6.5.1:
+ version "6.5.1"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+ integrity sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==
+
+qs@~6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+ integrity sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=
+
+qs@~6.5.2:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+ integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+
+querystring-es3@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+ integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
+
+querystring@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+ integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
+
+querystringify@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.0.tgz#7ded8dfbf7879dcc60d0a644ac6754b283ad17ef"
+ integrity sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg==
+
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"
+ integrity sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==
+ dependencies:
+ safe-buffer "^5.1.0"
+
+randomfill@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.3.tgz#b96b7df587f01dd91726c418f30553b1418e3d62"
+ integrity sha512-YL6GrhrWoic0Eq8rXVbMptH7dAxCs0J+mh5Y0euNekPPYaxEmdVGim6GdoxoRzKW2yJoU8tueifS7mYxvcFDEQ==
+ dependencies:
+ randombytes "^2.0.5"
+ safe-buffer "^5.1.0"
+
+range-parser@^1.0.3, range-parser@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
+ integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=
+
+raw-body@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"
+ integrity sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=
+ dependencies:
+ bytes "3.0.0"
+ http-errors "1.6.2"
+ iconv-lite "0.4.19"
+ unpipe "1.0.0"
+
+rc@^1.1.7:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd"
+ integrity sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0=
+ dependencies:
+ deep-extend "~0.4.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+rc@^1.2.7:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+ integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+ dependencies:
+ deep-extend "^0.6.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+read-cache@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
+ integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=
+ dependencies:
+ pify "^2.3.0"
+
+read-pkg-up@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+ integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=
+ dependencies:
+ find-up "^1.0.0"
+ read-pkg "^1.0.0"
+
+read-pkg@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+ integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=
+ dependencies:
+ load-json-file "^1.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^1.0.0"
+
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3:
+ version "2.3.4"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071"
+ integrity sha512-vuYxeWYM+fde14+rajzqgeohAI7YoJcHE7kXDAc4Nk0EbuKnJfqtY9YtRkLo/tqkuF7MsBQRhPnPeyjYITp3ZQ==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.0.3"
+ util-deprecate "~1.0.1"
+
+readable-stream@^3.0.6:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d"
+ integrity sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+readdirp@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
+ integrity sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=
+ dependencies:
+ graceful-fs "^4.1.2"
+ minimatch "^3.0.2"
+ readable-stream "^2.0.2"
+ set-immediate-shim "^1.0.1"
+
+readdirp@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
+ integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==
+ dependencies:
+ graceful-fs "^4.1.11"
+ micromatch "^3.1.10"
+ readable-stream "^2.0.2"
+
+redent@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
+ integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=
+ dependencies:
+ indent-string "^2.1.0"
+ strip-indent "^1.0.1"
+
+regenerate-unicode-properties@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.0.1.tgz#58a4a74e736380a7ab3c5f7e03f303a941b31289"
+ integrity sha512-HTjMafphaH5d5QDHuwW8Me6Hbc/GhXg8luNqTkPVwZ/oCZhnoifjWhGYsu2BzepMELTlbnoVcXvV0f+2uDDvoQ==
+ dependencies:
+ regenerate "^1.4.0"
+
+regenerate@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
+ integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
+
+regenerator-runtime@^0.12.0:
+ version "0.12.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
+ integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
+
+regenerator-transform@^0.13.4:
+ version "0.13.4"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.13.4.tgz#18f6763cf1382c69c36df76c6ce122cc694284fb"
+ integrity sha512-T0QMBjK3J0MtxjPmdIMXm72Wvj2Abb0Bd4HADdfijwMdoIsyQZ6fWC7kDFhk2YinBBEMZDL7Y7wh0J1sGx3S4A==
+ dependencies:
+ private "^0.1.6"
+
+regex-not@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.0.tgz#42f83e39771622df826b02af176525d6a5f157f9"
+ integrity sha1-Qvg+OXcWIt+CawKvF2Ul1qXxV/k=
+ dependencies:
+ extend-shallow "^2.0.1"
+
+regex-not@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+ integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+ dependencies:
+ extend-shallow "^3.0.2"
+ safe-regex "^1.1.0"
+
+regexp-tree@^0.1.0:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.5.tgz#7cd71fca17198d04b4176efd79713f2998009397"
+ integrity sha512-nUmxvfJyAODw+0B13hj8CFVAxhe7fDEAgJgaotBu3nnR+IgGgZq59YedJP5VYTlkEfqjuK6TuRpnymKdatLZfQ==
+
+regexpu-core@^4.1.3, regexpu-core@^4.2.0:
+ version "4.5.3"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.3.tgz#72f572e03bb8b9f4f4d895a0ccc57e707f4af2e4"
+ integrity sha512-LON8666bTAlViVEPXMv65ZqiaR3rMNLz36PIaQ7D+er5snu93k0peR7FSvO0QteYbZ3GOkvfHKbGr/B1xDu9FA==
+ dependencies:
+ regenerate "^1.4.0"
+ regenerate-unicode-properties "^8.0.1"
+ regjsgen "^0.5.0"
+ regjsparser "^0.6.0"
+ unicode-match-property-ecmascript "^1.0.4"
+ unicode-match-property-value-ecmascript "^1.1.0"
+
+regjsgen@^0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd"
+ integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==
+
+regjsparser@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.0.tgz#f1e6ae8b7da2bae96c99399b868cd6c933a2ba9c"
+ integrity sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==
+ 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"
+ integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
+
+repeat-element@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
+ integrity sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=
+
+repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+ integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=
+ dependencies:
+ is-finite "^1.0.0"
+
+request@2.81.0:
+ version "2.81.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
+ integrity sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=
+ dependencies:
+ aws-sign2 "~0.6.0"
+ aws4 "^1.2.1"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.0"
+ forever-agent "~0.6.1"
+ form-data "~2.1.1"
+ har-validator "~4.2.1"
+ hawk "~3.1.3"
+ http-signature "~1.1.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.7"
+ oauth-sign "~0.8.1"
+ performance-now "^0.2.0"
+ qs "~6.4.0"
+ safe-buffer "^5.0.1"
+ stringstream "~0.0.4"
+ tough-cookie "~2.3.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.0.0"
+
+request@^2.87.0, request@^2.88.0:
+ version "2.88.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+ integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.0"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.4.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+ integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-from-string@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+ integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+ integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
+
+requires-port@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+ integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+
+resolve-cwd@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
+ integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=
+ dependencies:
+ resolve-from "^3.0.0"
+
+resolve-dir@^1.0.0, resolve-dir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
+ integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=
+ dependencies:
+ expand-tilde "^2.0.0"
+ global-modules "^1.0.0"
+
+resolve-from@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+ integrity sha1-six699nWiBvItuZTM17rywoYh0g=
+
+resolve-url@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+ integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+
+resolve@^1.1.7:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
+ integrity sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==
+ dependencies:
+ path-parse "^1.0.5"
+
+resolve@^1.3.2, resolve@^1.8.1:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba"
+ integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==
+ dependencies:
+ path-parse "^1.0.6"
+
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+ integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
+rgb-regex@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
+ integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE=
+
+rgba-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
+ integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
+
+rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
+ integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==
+ dependencies:
+ glob "^7.0.5"
+
+rimraf@^2.6.2:
+ version "2.6.3"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
+ integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
+ dependencies:
+ glob "^7.1.3"
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
+ integrity sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=
+ dependencies:
+ hash-base "^2.0.0"
+ inherits "^2.0.1"
+
+run-queue@^1.0.0, run-queue@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
+ integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=
+ dependencies:
+ aproba "^1.1.1"
+
+safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+ integrity sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==
+
+safe-buffer@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+ integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+ dependencies:
+ ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3":
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sass-graph@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
+ integrity sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=
+ dependencies:
+ glob "^7.0.0"
+ lodash "^4.0.0"
+ scss-tokenizer "^0.2.3"
+ yargs "^7.0.0"
+
+sass-loader@^7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.1.0.tgz#16fd5138cb8b424bf8a759528a1972d72aad069d"
+ integrity sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==
+ dependencies:
+ clone-deep "^2.0.1"
+ loader-utils "^1.0.1"
+ lodash.tail "^4.1.1"
+ neo-async "^2.5.0"
+ pify "^3.0.0"
+ semver "^5.5.0"
+
+sax@^1.2.4, sax@~1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+ integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+schema-utils@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
+ integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==
+ dependencies:
+ ajv "^6.1.0"
+ ajv-errors "^1.0.0"
+ ajv-keywords "^3.1.0"
+
+scss-tokenizer@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
+ integrity sha1-jrBtualyMzOCTT9VMGQRSYR85dE=
+ dependencies:
+ js-base64 "^2.1.8"
+ source-map "^0.4.2"
+
+select-hose@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
+ integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
+
+selfsigned@^1.9.1:
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.2.tgz#b4449580d99929b65b10a48389301a6592088758"
+ integrity sha1-tESVgNmZKbZbEKSDiTAaZZIIh1g=
+ dependencies:
+ node-forge "0.7.1"
+
+"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"
+ integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==
+
+semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0:
+ version "5.6.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
+ integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
+
+semver@~5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+ integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8=
+
+send@0.16.1:
+ version "0.16.1"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3"
+ integrity sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==
+ dependencies:
+ debug "2.6.9"
+ depd "~1.1.1"
+ destroy "~1.0.4"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ etag "~1.8.1"
+ fresh "0.5.2"
+ http-errors "~1.6.2"
+ mime "1.4.1"
+ ms "2.0.0"
+ on-finished "~2.3.0"
+ range-parser "~1.2.0"
+ statuses "~1.3.1"
+
+serialize-javascript@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.4.0.tgz#7c958514db6ac2443a8abc062dc9f7886a7f6005"
+ integrity sha1-fJWFFNtqwkQ6irwGLcn3iGp/YAU=
+
+serve-index@^1.7.2:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"
+ integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=
+ dependencies:
+ accepts "~1.3.4"
+ batch "0.6.1"
+ debug "2.6.9"
+ escape-html "~1.0.3"
+ http-errors "~1.6.2"
+ mime-types "~2.1.17"
+ parseurl "~1.3.2"
+
+serve-static@1.13.1:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719"
+ integrity sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==
+ dependencies:
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ parseurl "~1.3.2"
+ send "0.16.1"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+ integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+set-getter@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376"
+ integrity sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=
+ dependencies:
+ to-object-path "^0.3.0"
+
+set-immediate-shim@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
+ integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=
+
+set-value@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
+ integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE=
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.1"
+ to-object-path "^0.3.0"
+
+set-value@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274"
+ integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
+setimmediate@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+ integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+setprototypeof@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
+ integrity sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=
+
+setprototypeof@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
+ integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+ version "2.4.10"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.10.tgz#b1fde5cd7d11a5626638a07c604ab909cfa31f9b"
+ integrity sha512-vnwmrFDlOExK4Nm16J2KMWHLrp14lBrjxMxBJpu++EnsuBmpiYaM/MEs46Vxxm/4FvdP5yTwuCTO9it5FSjrqA==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+shallow-clone@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571"
+ integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==
+ dependencies:
+ is-extendable "^0.1.1"
+ kind-of "^5.0.0"
+ mixin-object "^2.0.1"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+ 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"
+ integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
+signal-exit@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+ integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
+
+simple-swizzle@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
+ integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=
+ dependencies:
+ is-arrayish "^0.3.1"
+
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+ dependencies:
+ define-property "^1.0.0"
+ isobject "^3.0.0"
+ snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+ integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+ dependencies:
+ kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.1.tgz#e12b5487faded3e3dea0ac91e9400bf75b401370"
+ integrity sha1-4StUh/re0+PeoKyR6UAL91tAE3A=
+ dependencies:
+ base "^0.11.1"
+ debug "^2.2.0"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ map-cache "^0.2.2"
+ source-map "^0.5.6"
+ source-map-resolve "^0.5.0"
+ use "^2.0.0"
+
+sntp@1.x.x:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
+ integrity sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=
+ dependencies:
+ hoek "2.x.x"
+
+sockjs-client@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177"
+ integrity sha512-R9jxEzhnnrdxLCNln0xg5uGHqMnkhPSTzUZH2eXcR03S/On9Yvoq2wyUZILRUhZCNVu2PmwWVoyuiPz8th8zbg==
+ dependencies:
+ debug "^3.2.5"
+ eventsource "^1.0.7"
+ faye-websocket "~0.11.1"
+ inherits "^2.0.3"
+ json3 "^3.3.2"
+ url-parse "^1.4.3"
+
+sockjs@0.3.19:
+ version "0.3.19"
+ resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d"
+ integrity sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==
+ dependencies:
+ faye-websocket "^0.10.0"
+ uuid "^3.0.1"
+
+source-list-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
+ integrity sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==
+
+source-map-resolve@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a"
+ integrity sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==
+ dependencies:
+ atob "^2.0.0"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.0"
+
+source-map-support@~0.5.9:
+ version "0.5.10"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.10.tgz#2214080bc9d51832511ee2bab96e3c2f9353120c"
+ integrity sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map-url@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+ integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
+
+source-map@^0.4.2:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
+ integrity sha1-66T12pwNyZneaAMti092FzZSA2s=
+ dependencies:
+ amdefine ">=0.0.4"
+
+source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+ integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+spark-md5@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.0.tgz#3722227c54e2faf24b1dc6d933cc144e6f71bfef"
+ integrity sha1-NyIifFTi+vJLHcbZM8wUTm9xv+8=
+
+spdx-correct@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
+ integrity sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=
+ 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"
+ integrity sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=
+
+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"
+ integrity sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=
+
+spdy-transport@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31"
+ integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==
+ dependencies:
+ debug "^4.1.0"
+ detect-node "^2.0.4"
+ hpack.js "^2.1.6"
+ obuf "^1.1.2"
+ readable-stream "^3.0.6"
+ wbuf "^1.7.3"
+
+spdy@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.0.tgz#81f222b5a743a329aa12cea6a390e60e9b613c52"
+ integrity sha512-ot0oEGT/PGUpzf/6uk4AWLqkq+irlqHXkrdbk51oWONh3bxQmBuljxPNl66zlRRcIJStWq0QkLUCPOPjgjvU0Q==
+ dependencies:
+ debug "^4.1.0"
+ handle-thing "^2.0.0"
+ http-deceiver "^1.2.7"
+ select-hose "^2.0.0"
+ spdy-transport "^3.0.0"
+
+split-string@^3.0.1, split-string@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+ integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+ dependencies:
+ extend-shallow "^3.0.0"
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+ integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+
+sshpk@^1.7.0:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
+ integrity sha1-US322mKHFEMW3EwY/hzx2UBzm+M=
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ dashdash "^1.12.0"
+ getpass "^0.1.1"
+ optionalDependencies:
+ bcrypt-pbkdf "^1.0.0"
+ ecc-jsbn "~0.1.1"
+ jsbn "~0.1.0"
+ tweetnacl "~0.14.0"
+
+ssri@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
+ integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
+ dependencies:
+ figgy-pudding "^3.5.1"
+
+stable@^0.1.8:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
+ integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+ dependencies:
+ define-property "^0.2.5"
+ object-copy "^0.1.0"
+
+"statuses@>= 1.3.1 < 2":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
+ integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==
+
+statuses@~1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
+ integrity sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=
+
+stdout-stream@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b"
+ integrity sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=
+ dependencies:
+ readable-stream "^2.0.1"
+
+stream-browserify@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
+ integrity sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "^2.0.2"
+
+stream-each@^1.1.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd"
+ integrity sha512-mc1dbFhGBxvTM3bIWmAAINbqiuAk9TATcfIQC8P+/+HJefgaiTlMn2dHvkX8qlI12KeYKSQ1Ua9RrIqrn1VPoA==
+ dependencies:
+ end-of-stream "^1.1.0"
+ stream-shift "^1.0.0"
+
+stream-http@^2.7.2:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10"
+ integrity sha512-sZOFxI/5xw058XIRHl4dU3dZ+TTOIGJR78Dvo0oEAejIt4ou27k+3ne1zYmCV+v7UucbxIFQuOgnkTVHh8YPnw==
+ dependencies:
+ builtin-status-codes "^3.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.3.3"
+ to-arraybuffer "^1.0.0"
+ xtend "^4.0.0"
+
+stream-shift@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
+ integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
+
+string-width@^1.0.1, string-width@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+string-width@^2.0.0, string-width@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string_decoder@^1.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"
+ integrity sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+string_decoder@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
+ integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+stringstream@~0.0.4:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
+ integrity sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
+ 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"
+ integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-bom@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
+ integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=
+ dependencies:
+ is-utf8 "^0.2.0"
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+ integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
+
+strip-indent@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
+ integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=
+ dependencies:
+ get-stdin "^4.0.1"
+
+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"
+ integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
+
+style-loader@^0.23.1:
+ version "0.23.1"
+ resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925"
+ integrity sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg==
+ dependencies:
+ loader-utils "^1.1.0"
+ schema-utils "^1.0.0"
+
+stylehacks@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5"
+ integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==
+ dependencies:
+ browserslist "^4.0.0"
+ postcss "^7.0.0"
+ postcss-selector-parser "^3.0.0"
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+ integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
+
+supports-color@^5.3.0, supports-color@^5.5.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+supports-color@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
+ integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
+ dependencies:
+ has-flag "^3.0.0"
+
+svgo@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.2.0.tgz#305a8fc0f4f9710828c65039bb93d5793225ffc3"
+ integrity sha512-xBfxJxfk4UeVN8asec9jNxHiv3UAMv/ujwBWGYvQhhMb2u3YTGKkiybPcLFDLq7GLLWE9wa73e0/m8L5nTzQbw==
+ dependencies:
+ chalk "^2.4.1"
+ coa "^2.0.2"
+ css-select "^2.0.0"
+ css-select-base-adapter "^0.1.1"
+ css-tree "1.0.0-alpha.28"
+ css-url-regex "^1.1.0"
+ csso "^3.5.1"
+ js-yaml "^3.12.0"
+ mkdirp "~0.5.1"
+ object.values "^1.1.0"
+ sax "~1.2.4"
+ stable "^0.1.8"
+ unquote "~1.1.1"
+ util.promisify "~1.0.0"
+
+tapable@^1.0.0, tapable@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.1.tgz#4d297923c5a72a42360de2ab52dadfaaec00018e"
+ integrity sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==
+
+tar-pack@^3.4.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f"
+ integrity sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==
+ dependencies:
+ debug "^2.2.0"
+ fstream "^1.0.10"
+ fstream-ignore "^1.0.5"
+ once "^1.3.3"
+ readable-stream "^2.1.4"
+ rimraf "^2.5.1"
+ tar "^2.2.1"
+ uid-number "^0.0.6"
+
+tar@^2.0.0, tar@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
+ integrity sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=
+ dependencies:
+ block-stream "*"
+ fstream "^1.0.2"
+ inherits "2"
+
+tar@^4:
+ version "4.4.8"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d"
+ integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==
+ dependencies:
+ chownr "^1.1.1"
+ fs-minipass "^1.2.5"
+ minipass "^2.3.4"
+ minizlib "^1.1.1"
+ mkdirp "^0.5.0"
+ safe-buffer "^5.1.2"
+ yallist "^3.0.2"
+
+terser-webpack-plugin@^1.1.0, terser-webpack-plugin@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz#3f98bc902fac3e5d0de730869f50668561262ec8"
+ integrity sha512-GOK7q85oAb/5kE12fMuLdn2btOS9OBZn4VsecpHDywoUC/jLhSAKOiYo0ezx7ss2EXPMzyEWFoE0s1WLE+4+oA==
+ dependencies:
+ cacache "^11.0.2"
+ find-cache-dir "^2.0.0"
+ schema-utils "^1.0.0"
+ serialize-javascript "^1.4.0"
+ source-map "^0.6.1"
+ terser "^3.16.1"
+ webpack-sources "^1.1.0"
+ worker-farm "^1.5.2"
+
+terser@^3.16.1:
+ version "3.16.1"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-3.16.1.tgz#5b0dd4fa1ffd0b0b43c2493b2c364fd179160493"
+ integrity sha512-JDJjgleBROeek2iBcSNzOHLKsB/MdDf+E/BOAJ0Tk9r7p9/fVobfv7LMJ/g/k3v9SXdmjZnIlFd5nfn/Rt0Xow==
+ dependencies:
+ commander "~2.17.1"
+ source-map "~0.6.1"
+ source-map-support "~0.5.9"
+
+through2@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
+ integrity sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=
+ dependencies:
+ readable-stream "^2.1.5"
+ xtend "~4.0.1"
+
+thunky@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.2.tgz#a862e018e3fb1ea2ec3fce5d55605cf57f247371"
+ integrity sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E=
+
+timers-browserify@^2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.6.tgz#241e76927d9ca05f4d959819022f5b3664b64bae"
+ integrity sha512-HQ3nbYRAowdVd0ckGFvmJPPCOH/CHleFN/Y0YQCX1DVaB7t+KFvisuyN09fuP8Jtp1CpfSh8O8bMkHbdbPe6Pw==
+ dependencies:
+ setimmediate "^1.0.4"
+
+timsort@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
+ integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+
+to-arraybuffer@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+ integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
+
+to-fast-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+ integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
+
+to-object-path@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+ integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+ dependencies:
+ kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+ integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
+to-regex@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.1.tgz#15358bee4a2c83bd76377ba1dc049d0f18837aae"
+ integrity sha1-FTWL7kosg712N3uh3ASdDxiDeq4=
+ dependencies:
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ regex-not "^1.0.0"
+
+to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
+tough-cookie@~2.3.0:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
+ integrity sha1-C2GKVWW23qkL80JdBNVe3EdadWE=
+ dependencies:
+ punycode "^1.4.1"
+
+tough-cookie@~2.4.3:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
+ integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
+ dependencies:
+ psl "^1.1.24"
+ punycode "^1.4.1"
+
+trim-newlines@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
+ integrity sha1-WIeWa7WCpFA6QetST301ARgVphM=
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+ integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
+
+"true-case-path@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62"
+ integrity sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=
+ dependencies:
+ glob "^6.0.4"
+
+ts-pnp@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.0.1.tgz#fde74a6371676a167abaeda1ffc0fdb423520098"
+ integrity sha512-Zzg9XH0anaqhNSlDRibNC8Kp+B9KNM0uRIpLpGkGyrgRIttA7zZBhotTSEoEyuDrz3QW2LGtu2dxuk34HzIGnQ==
+
+tslib@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
+ integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
+
+tty-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+ integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+ integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+type-is@~1.6.15:
+ version "1.6.15"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
+ integrity sha1-yrEPtJCeRByChC6v4a1kbIGARBA=
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.15"
+
+typedarray@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+ integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+
+uid-number@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+ integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=
+
+underscore.string@2.3.x:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-2.3.3.tgz#71c08bf6b428b1133f37e78fa3a21c82f7329b0d"
+ integrity sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=
+
+unicode-canonical-property-names-ecmascript@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
+ integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==
+
+unicode-match-property-ecmascript@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"
+ integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==
+ dependencies:
+ unicode-canonical-property-names-ecmascript "^1.0.4"
+ unicode-property-aliases-ecmascript "^1.0.4"
+
+unicode-match-property-value-ecmascript@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277"
+ integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==
+
+unicode-property-aliases-ecmascript@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57"
+ integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==
+
+union-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
+ integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^0.4.3"
+
+uniq@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
+ integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=
+
+uniqs@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
+ integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI=
+
+unique-filename@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
+ integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
+ dependencies:
+ unique-slug "^2.0.0"
+
+unique-slug@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab"
+ integrity sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=
+ dependencies:
+ imurmurhash "^0.1.4"
+
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+ integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
+
+unquote@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544"
+ integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=
+
+unset-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+ integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+ dependencies:
+ has-value "^0.3.1"
+ isobject "^3.0.0"
+
+upath@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.0.tgz#b4706b9461ca8473adf89133d235689ca17f3656"
+ integrity sha1-tHBrlGHKhHOt+JEz0jVonKF/NlY=
+ dependencies:
+ lodash "3.x"
+ underscore.string "2.3.x"
+
+upath@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.1.tgz#497f7c1090b0818f310bbfb06783586a68d28014"
+ integrity sha512-D0yetkpIOKiZQquxjM2Syvy48Y1DbZ0SWxgsZiwd9GCWRpc75vN8ytzem14WDSg+oiX6+Qt31FpiS/ExODCrLg==
+
+uri-js@^4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+ integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+ dependencies:
+ punycode "^2.1.0"
+
+urix@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+ integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+
+url-parse@^1.4.3:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.4.tgz#cac1556e95faa0303691fec5cf9d5a1bc34648f8"
+ integrity sha512-/92DTTorg4JjktLNLe6GPS2/RvAd/RGr6LuktmWSMLEOa6rjnlrFXNgSbSmkNvCoL2T028A0a1JaJLzRMlFoHg==
+ dependencies:
+ querystringify "^2.0.0"
+ requires-port "^1.0.0"
+
+url@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+ integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
+ dependencies:
+ punycode "1.3.2"
+ querystring "0.2.0"
+
+use@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8"
+ integrity sha1-riig1y+TvyJCKhii43mZMRLeyOg=
+ dependencies:
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ lazy-cache "^2.0.2"
+
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util.promisify@^1.0.0, util.promisify@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
+ integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==
+ dependencies:
+ define-properties "^1.1.2"
+ object.getownpropertydescriptors "^2.0.3"
+
+util@0.10.3, util@^0.10.3:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+ integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk=
+ dependencies:
+ inherits "2.0.1"
+
+utils-merge@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+ integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
+
+uuid@^3.0.0, uuid@^3.0.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
+ integrity sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==
+
+uuid@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+ integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
+
+v8-compile-cache@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c"
+ integrity sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw==
+
+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"
+ integrity sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=
+ dependencies:
+ spdx-correct "~1.0.0"
+ spdx-expression-parse "~1.0.0"
+
+vary@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
+ integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
+
+vendors@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22"
+ integrity sha1-N61zyO5Bf7PVgOeFMSMH0nSEfyI=
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+vm-browserify@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
+ integrity sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=
+ dependencies:
+ indexof "0.0.1"
+
+watchpack@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
+ integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==
+ dependencies:
+ chokidar "^2.0.2"
+ graceful-fs "^4.1.2"
+ neo-async "^2.5.0"
+
+wbuf@^1.1.0:
+ version "1.7.2"
+ resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe"
+ integrity sha1-1pe5nx9ZUS3ydRvkJ2nBWAtYAf4=
+ dependencies:
+ minimalistic-assert "^1.0.0"
+
+wbuf@^1.7.3:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
+ integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==
+ dependencies:
+ minimalistic-assert "^1.0.0"
+
+webpack-assets-manifest@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/webpack-assets-manifest/-/webpack-assets-manifest-3.1.1.tgz#39bbc3bf2ee57fcd8ba07cda51c9ba4a3c6ae1de"
+ integrity sha512-JV9V2QKc5wEWQptdIjvXDUL1ucbPLH2f27toAY3SNdGZp+xSaStAgpoMcvMZmqtFrBc9a5pTS1058vxyMPOzRQ==
+ dependencies:
+ chalk "^2.0"
+ lodash.get "^4.0"
+ lodash.has "^4.0"
+ mkdirp "^0.5"
+ schema-utils "^1.0.0"
+ tapable "^1.0.0"
+ webpack-sources "^1.0.0"
+
+webpack-cli@^3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.2.3.tgz#13653549adfd8ccd920ad7be1ef868bacc22e346"
+ integrity sha512-Ik3SjV6uJtWIAN5jp5ZuBMWEAaP5E4V78XJ2nI+paFPh8v4HPSwo/myN0r29Xc/6ZKnd2IdrAlpSgNOu2CDQ6Q==
+ dependencies:
+ chalk "^2.4.1"
+ cross-spawn "^6.0.5"
+ enhanced-resolve "^4.1.0"
+ findup-sync "^2.0.0"
+ global-modules "^1.0.0"
+ import-local "^2.0.0"
+ interpret "^1.1.0"
+ loader-utils "^1.1.0"
+ supports-color "^5.5.0"
+ v8-compile-cache "^2.0.2"
+ yargs "^12.0.4"
+
+webpack-dev-middleware@^3.5.1:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.6.1.tgz#91f2531218a633a99189f7de36045a331a4b9cd4"
+ integrity sha512-XQmemun8QJexMEvNFbD2BIg4eSKrmSIMrTfnl2nql2Sc6OGAYFyb8rwuYrCjl/IiEYYuyTEiimMscu7EXji/Dw==
+ dependencies:
+ memory-fs "^0.4.1"
+ mime "^2.3.1"
+ range-parser "^1.0.3"
+ webpack-log "^2.0.0"
+
+webpack-dev-server@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.2.1.tgz#1b45ce3ecfc55b6ebe5e36dab2777c02bc508c4e"
+ integrity sha512-sjuE4mnmx6JOh9kvSbPYw3u/6uxCLHNWfhWaIPwcXWsvWOPN+nc5baq4i9jui3oOBRXGonK9+OI0jVkaz6/rCw==
+ dependencies:
+ ansi-html "0.0.7"
+ bonjour "^3.5.0"
+ chokidar "^2.0.0"
+ compression "^1.5.2"
+ connect-history-api-fallback "^1.3.0"
+ debug "^4.1.1"
+ del "^3.0.0"
+ express "^4.16.2"
+ html-entities "^1.2.0"
+ http-proxy-middleware "^0.19.1"
+ import-local "^2.0.0"
+ internal-ip "^4.2.0"
+ ip "^1.1.5"
+ killable "^1.0.0"
+ loglevel "^1.4.1"
+ opn "^5.1.0"
+ portfinder "^1.0.9"
+ schema-utils "^1.0.0"
+ selfsigned "^1.9.1"
+ semver "^5.6.0"
+ serve-index "^1.7.2"
+ sockjs "0.3.19"
+ sockjs-client "1.3.0"
+ spdy "^4.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^6.1.0"
+ url "^0.11.0"
+ webpack-dev-middleware "^3.5.1"
+ webpack-log "^2.0.0"
+ yargs "12.0.2"
+
+webpack-log@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f"
+ integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==
+ dependencies:
+ ansi-colors "^3.0.0"
+ uuid "^3.3.2"
+
+webpack-sources@^1.0.0, webpack-sources@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85"
+ integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.6.1"
+
+webpack-sources@^1.0.1, webpack-sources@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
+ integrity sha512-aqYp18kPphgoO5c/+NaUvEeACtZjMESmDChuD3NBciVpah3XpMEU9VAAtIaB1BsfJWWTSdv8Vv1m3T0aRk2dUw==
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.6.1"
+
+webpack@^4.29.6:
+ version "4.29.6"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.6.tgz#66bf0ec8beee4d469f8b598d3988ff9d8d90e955"
+ integrity sha512-MwBwpiE1BQpMDkbnUUaW6K8RFZjljJHArC6tWQJoFm0oQtfoSebtg4Y7/QHnJ/SddtjYLHaKGX64CFjG5rehJw==
+ dependencies:
+ "@webassemblyjs/ast" "1.8.5"
+ "@webassemblyjs/helper-module-context" "1.8.5"
+ "@webassemblyjs/wasm-edit" "1.8.5"
+ "@webassemblyjs/wasm-parser" "1.8.5"
+ acorn "^6.0.5"
+ acorn-dynamic-import "^4.0.0"
+ ajv "^6.1.0"
+ ajv-keywords "^3.1.0"
+ chrome-trace-event "^1.0.0"
+ enhanced-resolve "^4.1.0"
+ eslint-scope "^4.0.0"
+ json-parse-better-errors "^1.0.2"
+ loader-runner "^2.3.0"
+ loader-utils "^1.1.0"
+ memory-fs "~0.4.1"
+ micromatch "^3.1.8"
+ mkdirp "~0.5.0"
+ neo-async "^2.5.0"
+ node-libs-browser "^2.0.0"
+ schema-utils "^1.0.0"
+ tapable "^1.1.0"
+ terser-webpack-plugin "^1.1.0"
+ watchpack "^1.5.0"
+ webpack-sources "^1.3.0"
+
+websocket-driver@>=0.5.1:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb"
+ integrity sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=
+ dependencies:
+ http-parser-js ">=0.4.0"
+ websocket-extensions ">=0.1.1"
+
+websocket-extensions@>=0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
+ integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==
+
+which-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
+ integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+ integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
+
+which@1, which@^1.2.9:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+ integrity sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==
+ dependencies:
+ isexe "^2.0.0"
+
+which@^1.2.14:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
+ integrity sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==
+ dependencies:
+ string-width "^1.0.2"
+
+worker-farm@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae"
+ integrity sha512-XxiQ9kZN5n6mmnW+mFJ+wXjNNI/Nx4DIdaAKLX1Bn6LYBWlN/zaBhu34DQYPZ1AJobQuu67S2OfDdNSVULvXkQ==
+ dependencies:
+ errno "^0.1.4"
+ xtend "^4.0.1"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+xregexp@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020"
+ integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==
+
+xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+ integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
+
+y18n@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+ integrity sha1-bRX7qITAhnnA136I53WegR4H+kE=
+
+"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+ integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+ integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
+
+yallist@^3.0.0, yallist@^3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"
+ integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==
+
+yargs-parser@^10.1.0:
+ version "10.1.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8"
+ integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==
+ dependencies:
+ camelcase "^4.1.0"
+
+yargs-parser@^11.1.1:
+ version "11.1.1"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
+ integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==
+ dependencies:
+ camelcase "^5.0.0"
+ decamelize "^1.2.0"
+
+yargs-parser@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
+ integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=
+ dependencies:
+ camelcase "^3.0.0"
+
+yargs@12.0.2:
+ version "12.0.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.2.tgz#fe58234369392af33ecbef53819171eff0f5aadc"
+ integrity sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==
+ dependencies:
+ cliui "^4.0.0"
+ decamelize "^2.0.0"
+ find-up "^3.0.0"
+ get-caller-file "^1.0.1"
+ os-locale "^3.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1 || ^4.0.0"
+ yargs-parser "^10.1.0"
+
+yargs@^12.0.4:
+ version "12.0.5"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
+ integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
+ dependencies:
+ cliui "^4.0.0"
+ decamelize "^1.2.0"
+ find-up "^3.0.0"
+ get-caller-file "^1.0.1"
+ os-locale "^3.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1 || ^4.0.0"
+ yargs-parser "^11.1.1"
+
+yargs@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
+ integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=
+ dependencies:
+ camelcase "^3.0.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^1.4.0"
+ read-pkg-up "^1.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^1.0.2"
+ which-module "^1.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^5.0.0"
diff --git a/actiontext/test/fixtures/files/racecar.jpg b/actiontext/test/fixtures/files/racecar.jpg
new file mode 100644
index 0000000000..934b4caa22
--- /dev/null
+++ b/actiontext/test/fixtures/files/racecar.jpg
Binary files differ
diff --git a/actiontext/test/template/form_helper_test.rb b/actiontext/test/template/form_helper_test.rb
new file mode 100644
index 0000000000..cf7e4c0c69
--- /dev/null
+++ b/actiontext/test/template/form_helper_test.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionText::FormHelperTest < ActionView::TestCase
+ tests ActionText::TagHelper
+
+ def form_with(*)
+ @output_buffer = super
+ end
+
+ teardown do
+ I18n.backend.reload!
+ end
+
+ setup do
+ I18n.backend.store_translations("placeholder",
+ activerecord: {
+ attributes: {
+ message: {
+ title: "Story title"
+ }
+ }
+ }
+ )
+ end
+
+ test "form with rich text area" do
+ form_with model: Message.new, scope: :message do |form|
+ form.rich_text_area :content
+ end
+
+ assert_dom_equal \
+ '<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">' \
+ '<input type="hidden" name="message[content]" id="message_content_trix_input_message" />' \
+ '<trix-editor id="message_content" input="message_content_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/:signed_id/:filename">' \
+ "</trix-editor>" \
+ "</form>",
+ output_buffer
+ end
+
+ test "form with rich text area having class" do
+ form_with model: Message.new, scope: :message do |form|
+ form.rich_text_area :content, class: "custom-class"
+ end
+
+ assert_dom_equal \
+ '<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">' \
+ '<input type="hidden" name="message[content]" id="message_content_trix_input_message" />' \
+ '<trix-editor id="message_content" input="message_content_trix_input_message" class="custom-class" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/:signed_id/:filename">' \
+ "</trix-editor>" \
+ "</form>",
+ output_buffer
+ end
+
+ test "form with rich text area for non-attribute" do
+ form_with model: Message.new, scope: :message do |form|
+ form.rich_text_area :not_an_attribute
+ end
+
+ assert_dom_equal \
+ '<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">' \
+ '<input type="hidden" name="message[not_an_attribute]" id="message_not_an_attribute_trix_input_message" />' \
+ '<trix-editor id="message_not_an_attribute" input="message_not_an_attribute_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/:signed_id/:filename">' \
+ "</trix-editor>" \
+ "</form>",
+ output_buffer
+ end
+
+ test "modelless form with rich text area" do
+ form_with url: "/messages", scope: :message do |form|
+ form.rich_text_area :content
+ end
+
+ assert_dom_equal \
+ '<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">' \
+ '<input type="hidden" name="message[content]" id="trix_input_1" />' \
+ '<trix-editor id="message_content" input="trix_input_1" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/:signed_id/:filename">' \
+ "</trix-editor>" \
+ "</form>",
+ output_buffer
+ end
+
+ test "form with rich text area having placeholder without locale" do
+ form_with model: Message.new, scope: :message do |form|
+ form.rich_text_area :content, placeholder: true
+ end
+
+ assert_dom_equal \
+ '<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">' \
+ '<input type="hidden" name="message[content]" id="message_content_trix_input_message" />' \
+ '<trix-editor placeholder="Content" id="message_content" input="message_content_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/:signed_id/:filename">' \
+ "</trix-editor>" \
+ "</form>",
+ output_buffer
+ end
+
+ test "form with rich text area having placeholder with locale" do
+ I18n.with_locale :placeholder do
+ form_with model: Message.new, scope: :message do |form|
+ form.rich_text_area :title, placeholder: true
+ end
+ end
+
+ assert_dom_equal \
+ '<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">' \
+ '<input type="hidden" name="message[title]" id="message_title_trix_input_message" />' \
+ '<trix-editor placeholder="Story title" id="message_title" input="message_title_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/:signed_id/:filename">' \
+ "</trix-editor>" \
+ "</form>",
+ output_buffer
+ end
+end
diff --git a/actiontext/test/test_helper.rb b/actiontext/test/test_helper.rb
new file mode 100644
index 0000000000..196fba8c99
--- /dev/null
+++ b/actiontext/test/test_helper.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+# Configure Rails Environment
+ENV["RAILS_ENV"] = "test"
+
+require_relative "../test/dummy/config/environment"
+ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
+require "rails/test_help"
+
+# Filter out Minitest backtrace while allowing backtrace from other libraries
+# to be shown.
+Minitest.backtrace_filter = Minitest::BacktraceFilter.new
+
+require "rails/test_unit/reporter"
+Rails::TestUnitReporter.executable = "bin/test"
+
+# Disable available locale checks to allow to add locale after initialized.
+I18n.enforce_available_locales = false
+
+# 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
+
+class ActiveSupport::TestCase
+ private
+ def create_file_blob(filename:, content_type:, metadata: nil)
+ ActiveStorage::Blob.create_after_upload! io: file_fixture(filename).open, filename: filename, content_type: content_type, metadata: metadata
+ end
+end
diff --git a/actiontext/test/unit/attachment_test.rb b/actiontext/test/unit/attachment_test.rb
new file mode 100644
index 0000000000..026078dcec
--- /dev/null
+++ b/actiontext/test/unit/attachment_test.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionText::AttachmentTest < ActiveSupport::TestCase
+ test "from_attachable" do
+ attachment = ActionText::Attachment.from_attachable(attachable, caption: "Captioned")
+ assert_equal attachable, attachment.attachable
+ assert_equal "Captioned", attachment.caption
+ end
+
+ test "proxies missing methods to attachable" do
+ attachable.instance_eval { def proxied; "proxied"; end }
+ attachment = ActionText::Attachment.from_attachable(attachable)
+ assert_equal "proxied", attachment.proxied
+ end
+
+ test "proxies #to_param to attachable" do
+ attachment = ActionText::Attachment.from_attachable(attachable)
+ assert_equal attachable.to_param, attachment.to_param
+ end
+
+ test "converts to TrixAttachment" do
+ attachment = attachment_from_html(%Q(<action-text-attachment sgid="#{attachable.attachable_sgid}" caption="Captioned"></action-text-attachment>))
+
+ trix_attachment = attachment.to_trix_attachment
+ assert_kind_of ActionText::TrixAttachment, trix_attachment
+
+ assert_equal attachable.attachable_sgid, trix_attachment.attributes["sgid"]
+ assert_equal attachable.attachable_content_type, trix_attachment.attributes["contentType"]
+ assert_equal attachable.filename.to_s, trix_attachment.attributes["filename"]
+ assert_equal attachable.byte_size, trix_attachment.attributes["filesize"]
+ assert_equal "Captioned", trix_attachment.attributes["caption"]
+
+ assert_not_nil attachable.to_trix_content_attachment_partial_path
+ assert_not_nil trix_attachment.attributes["content"]
+ end
+
+ test "converts to TrixAttachment with content" do
+ attachable = Person.create! name: "Javan"
+ attachment = attachment_from_html(%Q(<action-text-attachment sgid="#{attachable.attachable_sgid}"></action-text-attachment>))
+
+ trix_attachment = attachment.to_trix_attachment
+ assert_kind_of ActionText::TrixAttachment, trix_attachment
+
+ assert_equal attachable.attachable_sgid, trix_attachment.attributes["sgid"]
+ assert_equal attachable.attachable_content_type, trix_attachment.attributes["contentType"]
+
+ assert_not_nil attachable.to_trix_content_attachment_partial_path
+ assert_not_nil trix_attachment.attributes["content"]
+ end
+
+ test "defaults trix partial to model partial" do
+ attachable = Page.create! title: "Homepage"
+ assert_equal "pages/page", attachable.to_trix_content_attachment_partial_path
+ end
+
+ private
+ def attachment_from_html(html)
+ ActionText::Content.new(html).attachments.first
+ end
+
+ def attachable
+ @attachment ||= create_file_blob(filename: "racecar.jpg", content_type: "image/jpg")
+ end
+end
diff --git a/actiontext/test/unit/content_test.rb b/actiontext/test/unit/content_test.rb
new file mode 100644
index 0000000000..f77f48df14
--- /dev/null
+++ b/actiontext/test/unit/content_test.rb
@@ -0,0 +1,116 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionText::ContentTest < ActiveSupport::TestCase
+ test "equality" do
+ html = "<div>test</div>"
+ content = content_from_html(html)
+ assert_equal content, content_from_html(html)
+ assert_not_equal content, html
+ end
+
+ test "marshal serialization" do
+ content = content_from_html("Hello!")
+ assert_equal content, Marshal.load(Marshal.dump(content))
+ end
+
+ test "roundtrips HTML without additional newlines" do
+ html = "<div>a<br></div>"
+ content = content_from_html(html)
+ assert_equal html, content.to_html
+ end
+
+ test "extracts links" do
+ html = '<a href="http://example.com/1">1</a><br><a href="http://example.com/1">1</a>'
+ content = content_from_html(html)
+ assert_equal ["http://example.com/1"], content.links
+ end
+
+ test "extracts attachables" do
+ attachable = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg")
+ html = %Q(<action-text-attachment sgid="#{attachable.attachable_sgid}" caption="Captioned"></action-text-attachment>)
+
+ content = content_from_html(html)
+ assert_equal 1, content.attachments.size
+
+ attachment = content.attachments.first
+ assert_equal "Captioned", attachment.caption
+ assert_equal attachable, attachment.attachable
+ end
+
+ test "extracts remote image attachables" do
+ html = '<action-text-attachment content-type="image" url="http://example.com/cat.jpg" width="100" height="100" caption="Captioned"></action-text-attachment>'
+
+ content = content_from_html(html)
+ assert_equal 1, content.attachments.size
+
+ attachment = content.attachments.first
+ assert_equal "Captioned", attachment.caption
+
+ attachable = attachment.attachable
+ assert_kind_of ActionText::Attachables::RemoteImage, attachable
+ assert_equal "http://example.com/cat.jpg", attachable.url
+ assert_equal "100", attachable.width
+ assert_equal "100", attachable.height
+ end
+
+ test "identifies destroyed attachables as missing" do
+ attachable = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg")
+ html = %Q(<action-text-attachment sgid="#{attachable.attachable_sgid}"></action-text-attachment>)
+ attachable.destroy!
+ content = content_from_html(html)
+ assert_equal 1, content.attachments.size
+ assert_equal ActionText::Attachables::MissingAttachable, content.attachments.first.attachable
+ end
+
+ test "extracts missing attachables" do
+ html = '<action-text-attachment sgid="missing"></action-text-attachment>'
+ content = content_from_html(html)
+ assert_equal 1, content.attachments.size
+ assert_equal ActionText::Attachables::MissingAttachable, content.attachments.first.attachable
+ end
+
+ test "converts Trix-formatted attachments" do
+ html = %Q(<figure data-trix-attachment='{"sgid":"123","contentType":"text/plain","width":100,"height":100}' data-trix-attributes='{"caption":"Captioned"}'></figure>)
+ content = content_from_html(html)
+ assert_equal 1, content.attachments.size
+ assert_equal '<action-text-attachment sgid="123" content-type="text/plain" width="100" height="100" caption="Captioned"></action-text-attachment>', content.to_html
+ end
+
+ test "ignores Trix-formatted attachments with malformed JSON" do
+ html = %Q(<div data-trix-attachment='{"sgid":"garbage...'></div>)
+ content = content_from_html(html)
+ assert_equal 0, content.attachments.size
+ end
+
+ test "minifies attachment markup" do
+ html = '<action-text-attachment sgid="123"><div>HTML</div></action-text-attachment>'
+ assert_equal '<action-text-attachment sgid="123"></action-text-attachment>', content_from_html(html).to_html
+ end
+
+ test "canonicalizes attachment gallery markup" do
+ attachment_html = '<action-text-attachment sgid="1" presentation="gallery"></action-text-attachment><action-text-attachment sgid="2" presentation="gallery"></action-text-attachment>'
+ html = %Q(<div class="attachment-gallery attachment-gallery--2">#{attachment_html}</div>)
+ assert_equal "<div>#{attachment_html}</div>", content_from_html(html).to_html
+ end
+
+ test "canonicalizes attachment gallery markup with whitespace" do
+ attachment_html = %Q(\n <action-text-attachment sgid="1" presentation="gallery"></action-text-attachment>\n <action-text-attachment sgid="2" presentation="gallery"></action-text-attachment>\n)
+ html = %Q(<div class="attachment-gallery attachment-gallery--2">#{attachment_html}</div>)
+ assert_equal "<div>#{attachment_html}</div>", content_from_html(html).to_html
+ end
+
+ test "canonicalizes nested attachment gallery markup" do
+ attachment_html = '<action-text-attachment sgid="1" presentation="gallery"></action-text-attachment><action-text-attachment sgid="2" presentation="gallery"></action-text-attachment>'
+ html = %Q(<blockquote><div class="attachment-gallery attachment-gallery--2">#{attachment_html}</div></blockquote>)
+ assert_equal "<blockquote><div>#{attachment_html}</div></blockquote>", content_from_html(html).to_html
+ end
+
+ private
+ def content_from_html(html)
+ ActionText::Content.new(html).tap do |content|
+ assert_nothing_raised { content.to_s }
+ end
+ end
+end
diff --git a/actiontext/test/unit/model_test.rb b/actiontext/test/unit/model_test.rb
new file mode 100644
index 0000000000..af53f88caa
--- /dev/null
+++ b/actiontext/test/unit/model_test.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionText::ModelTest < ActiveSupport::TestCase
+ test "html conversion" do
+ message = Message.new(subject: "Greetings", content: "<h1>Hello world</h1>")
+ assert_equal %Q(<div class="trix-content">\n <h1>Hello world</h1>\n</div>\n), "#{message.content}"
+ end
+
+ test "plain text conversion" do
+ message = Message.new(subject: "Greetings", content: "<h1>Hello world</h1>")
+ assert_equal "Hello world", message.content.to_plain_text
+ end
+
+ test "without content" do
+ message = Message.create!(subject: "Greetings")
+ assert message.content.nil?
+ assert message.content.blank?
+ assert message.content.empty?
+ assert_not message.content.present?
+ end
+
+ test "with blank content" do
+ message = Message.create!(subject: "Greetings", content: "")
+ assert_not message.content.nil?
+ assert message.content.blank?
+ assert message.content.empty?
+ assert_not message.content.present?
+ end
+
+ test "embed extraction" do
+ blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg")
+ message = Message.create!(subject: "Greetings", content: ActionText::Content.new("Hello world").append_attachables(blob))
+ assert_equal "racecar.jpg", message.content.embeds.first.filename.to_s
+ end
+
+ test "embed extraction only extracts file attachments" do
+ remote_image_html = '<action-text-attachment content-type="image" url="http://example.com/cat.jpg"></action-text-attachment>'
+ blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg")
+ content = ActionText::Content.new(remote_image_html).append_attachables(blob)
+ message = Message.create!(subject: "Greetings", content: content)
+ assert_equal [ActionText::Attachables::RemoteImage, ActiveStorage::Blob], message.content.body.attachables.map(&:class)
+ assert_equal [ActiveStorage::Attachment], message.content.embeds.map(&:class)
+ end
+
+ test "saving content" do
+ message = Message.create!(subject: "Greetings", content: "<h1>Hello world</h1>")
+ assert_equal "Hello world", message.content.to_plain_text
+ end
+
+ test "saving body" do
+ message = Message.create(subject: "Greetings", body: "<h1>Hello world</h1>")
+ assert_equal "Hello world", message.body.to_plain_text
+ end
+
+ test "saving content via nested attributes" do
+ message = Message.create! subject: "Greetings", content: "<h1>Hello world</h1>",
+ review_attributes: { author_name: "Marcia", content: "Nice work!" }
+ assert_equal "Nice work!", message.review.content.to_plain_text
+ end
+
+ test "updating content via nested attributes" do
+ message = Message.create! subject: "Greetings", content: "<h1>Hello world</h1>",
+ review_attributes: { author_name: "Marcia", content: "Nice work!" }
+
+ message.update! review_attributes: { id: message.review.id, content: "Great work!" }
+ assert_equal "Great work!", message.review.reload.content.to_plain_text
+ end
+
+ test "building content lazily on existing record" do
+ message = Message.create!(subject: "Greetings")
+
+ assert_no_difference -> { ActionText::RichText.count } do
+ assert_kind_of ActionText::RichText, message.content
+ end
+ end
+end
diff --git a/actiontext/test/unit/plain_text_conversion_test.rb b/actiontext/test/unit/plain_text_conversion_test.rb
new file mode 100644
index 0000000000..53a1029caf
--- /dev/null
+++ b/actiontext/test/unit/plain_text_conversion_test.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionText::PlainTextConversionTest < ActiveSupport::TestCase
+ test "<p> tags are separated by two new lines" do
+ assert_converted_to(
+ "Hello world!\n\nHow are you?",
+ "<p>Hello world!</p><p>How are you?</p>"
+ )
+ end
+
+ test "<blockquote> tags are separated by two new lines" do
+ assert_converted_to(
+ "“Hello world!â€\n\n“How are you?â€",
+ "<blockquote>Hello world!</blockquote><blockquote>How are you?</blockquote>"
+ )
+ end
+
+ test "<ol> tags are separated by two new lines" do
+ assert_converted_to(
+ "Hello world!\n\n1. list1\n\n1. list2\n\nHow are you?",
+ "<p>Hello world!</p><ol><li>list1</li></ol><ol><li>list2</li></ol><p>How are you?</p>"
+ )
+ end
+
+ test "<ul> tags are separated by two new lines" do
+ assert_converted_to(
+ "Hello world!\n\n• list1\n\n• list2\n\nHow are you?",
+ "<p>Hello world!</p><ul><li>list1</li></ul><ul><li>list2</li></ul><p>How are you?</p>"
+ )
+ end
+
+ test "<h1> tags are separated by two new lines" do
+ assert_converted_to(
+ "Hello world!\n\nHow are you?",
+ "<h1>Hello world!</h1><div>How are you?</div>"
+ )
+ end
+
+ test "<li> tags are separated by one new line" do
+ assert_converted_to(
+ "• one\n• two\n• three",
+ "<ul><li>one</li><li>two</li><li>three</li></ul>"
+ )
+ end
+
+ test "<li> tags without a parent list" do
+ assert_converted_to(
+ "• one\n• two\n• three",
+ "<li>one</li><li>two</li><li>three</li>"
+ )
+ end
+
+ test "<br> tags are separated by one new line" do
+ assert_converted_to(
+ "Hello world!\none\ntwo\nthree",
+ "<p>Hello world!<br>one<br>two<br>three</p>"
+ )
+ end
+
+ test "<div> tags are separated by one new line" do
+ assert_converted_to(
+ "Hello world!\nHow are you?",
+ "<div>Hello world!</div><div>How are you?</div>"
+ )
+ end
+
+ test "<action-text-attachment> tags are converted to their plain-text representation" do
+ assert_converted_to(
+ "Hello world! [Cat]",
+ 'Hello world! <action-text-attachment url="http://example.com/cat.jpg" content-type="image" caption="Cat"></action-text-attachment>'
+ )
+ end
+
+ test "preserves non-linebreak whitespace after text" do
+ assert_converted_to(
+ "Hello world!",
+ "<div><strong>Hello </strong>world!</div>"
+ )
+ end
+
+ test "preserves trailing linebreaks after text" do
+ assert_converted_to(
+ "Hello\nHow are you?",
+ "<strong>Hello<br></strong>How are you?"
+ )
+ end
+
+ private
+ def assert_converted_to(plain_text, html)
+ assert_equal plain_text, ActionText::Content.new(html).to_plain_text
+ end
+end
diff --git a/actiontext/test/unit/trix_attachment_test.rb b/actiontext/test/unit/trix_attachment_test.rb
new file mode 100644
index 0000000000..81d015750e
--- /dev/null
+++ b/actiontext/test/unit/trix_attachment_test.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class ActionText::TrixAttachmentTest < ActiveSupport::TestCase
+ test "from_attributes" do
+ attributes = {
+ "data-trix-attachment" => {
+ "sgid" => "123",
+ "contentType" => "text/plain",
+ "href" => "http://example.com/",
+ "filename" => "example.txt",
+ "filesize" => 12345,
+ "previewable" => true
+ },
+ "data-trix-attributes" => {
+ "caption" => "hello"
+ }
+ }
+
+ attachment = attachment(
+ sgid: "123",
+ content_type: "text/plain",
+ href: "http://example.com/",
+ filename: "example.txt",
+ filesize: "12345",
+ previewable: "true",
+ caption: "hello"
+ )
+
+ assert_attachment_json_attributes(attachment, attributes)
+ end
+
+ test "previewable is typecast" do
+ assert_attachment_attribute(attachment(previewable: ""), "previewable", false)
+ assert_attachment_attribute(attachment(previewable: false), "previewable", false)
+ assert_attachment_attribute(attachment(previewable: "false"), "previewable", false)
+ assert_attachment_attribute(attachment(previewable: "garbage"), "previewable", false)
+ assert_attachment_attribute(attachment(previewable: true), "previewable", true)
+ assert_attachment_attribute(attachment(previewable: "true"), "previewable", true)
+ end
+
+ test "filesize is typecast when integer-like" do
+ assert_attachment_attribute(attachment(filesize: 123), "filesize", 123)
+ assert_attachment_attribute(attachment(filesize: "123"), "filesize", 123)
+ assert_attachment_attribute(attachment(filesize: "3.5 MB"), "filesize", "3.5 MB")
+ assert_attachment_attribute(attachment(filesize: nil), "filesize", nil)
+ assert_attachment_attribute(attachment(filesize: ""), "filesize", "")
+ end
+
+ test "#attributes strips unmappable attributes" do
+ attributes = {
+ "sgid" => "123",
+ "caption" => "hello"
+ }
+
+ attachment = attachment(sgid: "123", caption: "hello", nonexistent: "garbage")
+ assert_attachment_attributes(attachment, attributes)
+ end
+
+ def assert_attachment_attribute(attachment, name, value)
+ if value.nil?
+ assert_nil(attachment.attributes[name])
+ else
+ assert_equal(value, attachment.attributes[name])
+ end
+ end
+
+ def assert_attachment_attributes(attachment, attributes)
+ assert_equal(attributes, attachment.attributes)
+ end
+
+ def assert_attachment_json_attributes(attachment, attributes)
+ attributes.each do |name, expected|
+ actual = JSON.parse(attachment.node[name])
+ assert_equal(expected, actual)
+ end
+ end
+
+ def attachment(**attributes)
+ ActionText::TrixAttachment.from_attributes(attributes)
+ end
+end
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 a1901e8a17..abb97804e1 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,299 +1,240 @@
-* Deprecate `datetime_field` and `datetime_field_tag` helpers.
- Datetime input type was removed from HTML specification.
- One can use `datetime_local_field` and `datetime_local_field_tag` instead.
+* Only clear ActionView cache in development on file changes
- *Wojciech Wnętrzak*
+ To speed up development mode, view caches are only cleared when files in
+ the view paths have changed. Applications which have implemented custom
+ `ActionView::Resolver` subclasses may need to add their own cache clearing.
-* Added log "Rendering ...", when starting to render a template to log that
- we have started rendering something. This helps to easily identify the origin
- of queries in the log whether they came from controller or views.
+ *John Hawthorn*
- *Vipul A M and Prem Sichanugrist*
-## Rails 5.0.0.beta3 (February 24, 2016) ##
+## Rails 6.0.0.beta3 (March 11, 2019) ##
-* Collection rendering can cache and fetch multiple partials at once.
+* Only accept formats from registered mime types
- Collections rendered as:
+ A lack of filtering on mime types could allow an attacker to read
+ arbitrary files on the target server or to perform a denial of service
+ attack.
- ```ruby
- <%= render partial: 'notifications/notification', collection: @notifications, as: :notification, cached: true %>
- ```
-
- will read several partials from cache at once. The templates in the collection
- that haven't been cached already will automatically be written to cache. Works
- great alongside individual template fragment caching. For instance if the
- template the collection renders is cached like:
-
- ```ruby
- # notifications/_notification.html.erb
- <% cache notification do %>
- <%# ... %>
- <% end %>
- ```
-
- Then any collection renders shares that cache when attempting to read multiple
- ones at once.
-
- *Kasper Timm Hansen*
-
-* Add support for nested hashes/arrays to `:params` option of `button_to` helper.
-
- *James Coleman*
-
-## Rails 5.0.0.beta2 (February 01, 2016) ##
-
-* Fix stripping the digest from the automatically generated img tag alt
- attribute when assets are handled by Sprockets >=3.0.
-
- *Bart de Water*
-
-* Create a new `ActiveSupport::SafeBuffer` instance when `content_for` is flushed.
-
- Fixes #19890.
-
- *Yoong Kang Lim*
-
-* Fix `collection_radio_buttons` hidden_field name and make it appear
- before the actual input radio tags to make the real value override
- the hidden when passed.
-
- Fixes #22773.
-
- *Santiago Pastorino*
-
-* `ActionView::TestCase::Controller#params` returns an instance of
- `ActionController::Parameters`.
-
- *Justin Coyne*
-
-* Fix regression in `submit_tag` when a symbol is used as label argument.
-
- *Yuuji Yaginuma*
+ Fixes CVE-2019-5418
+ Fixes CVE-2019-5419
+ *John Hawthorn*, *Eileen M. Uchitelle*, *Aaron Patterson*
-## Rails 5.0.0.beta1 (December 18, 2015) ##
-* `I18n.translate` helper will wrap the missing translation keys
- in a <span> tag only if `debug_missing_translation` configuration
- be true. Default value is `true`. For example in `application.rb`:
+## Rails 6.0.0.beta2 (February 25, 2019) ##
- # in order to turn off missing key wrapping
- config.action_view.debug_missing_translation = false
+* `ActionView::Template.finalize_compiled_template_methods` is deprecated with
+ no replacement.
- *Sameer Rahmani*
+ *tenderlove*
-* Respect value of `:object` if `:object` is false when rendering.
+* `config.action_view.finalize_compiled_template_methods` is deprecated with
+ no replacement.
- Fixes #22260.
+ *tenderlove*
- *Yuichiro Kaneko*
+* Ensure unique DOM IDs for collection inputs with float values.
-* Generate `week_field` input values using a 1-based index and not a 0-based index
- as per the W3 spec: http://www.w3.org/TR/html-markup/datatypes.html#form.data.week
+ Fixes #34974.
- *Christoph Geschwind*
+ *Mark Edmondson*
-* Allow `host` option in `javascript_include_tag` and `stylesheet_link_tag` helpers
- *Grzegorz Witek*
+## Rails 6.0.0.beta1 (January 18, 2019) ##
-* Restrict `url_for :back` to valid, non-JavaScript URLs. GH#14444
+* [Rename npm package](https://github.com/rails/rails/pull/34905) from
+ [`rails-ujs`](https://www.npmjs.com/package/rails-ujs) to
+ [`@rails/ujs`](https://www.npmjs.com/package/@rails/ujs).
- *Damien Burke*
+ *Javan Makhmali*
-* Allow `date_select` helper selected option to accept hash like the default options.
+* Remove deprecated `image_alt` helper.
- *Lecky Lao*
-
-* Collection input propagates input's `id` to the label's `for` attribute when
- using html options as the last element of collection.
-
- *Vasiliy Ermolovich*
-
-* Add a `hidden_field` on the `collection_radio_buttons` to avoid raising an error
- when the only input on the form is the `collection_radio_buttons`.
-
- *Mauro George*
+ *Rafael Mendonça França*
-* `url_for` does not modify its arguments when generating polymorphic URLs.
+* Fix the need of `#protect_against_forgery?` method defined in
+ `ActionView::Base` subclasses. This prevents the use of forms and buttons.
- *Bernerd Schaefer*
+ *Genadi Samokovarov*
-* `number_to_currency` and `number_with_delimiter` now accept a custom `delimiter_pattern` option
- to handle placement of delimiter, to support currency formats like INR.
+* Fix UJS permanently showing disabled text in a[data-remote][data-disable-with] elements within forms.
- Example:
+ Fixes #33889.
- number_to_currency(1230000, delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/, unit: '₹', format: "%u %n")
- # => '₹ 12,30,000.00'
+ *Wolfgang Hobmaier*
- *Vipul A M*
+* Prevent non-primary mouse keys from triggering Rails UJS click handlers.
+ Firefox fires click events even if the click was triggered by non-primary mouse keys such as right- or scroll-wheel-clicks.
+ For example, right-clicking a link such as the one described below (with an underlying ajax request registered on click) should not cause that request to occur.
-* Make `disable_with` the default behavior for submit tags. Disables the
- button on submit to prevent double submits.
-
- *Justin Schiff*
+ ```
+ <%= link_to 'Remote', remote_path, class: 'remote', remote: true, data: { type: :json } %>
+ ```
-* Add a break_sequence option to word_wrap so you can specify a custom break.
+ Fixes #34541.
- *Mauricio Gomez*
+ *Wolfgang Hobmaier*
-* Add wildcard matching to explicit dependencies.
+* Prevent `ActionView::TextHelper#word_wrap` from unexpectedly stripping white space from the _left_ side of lines.
- Turns:
+ For example, given input like this:
- ```erb
- <% # Template Dependency: recordings/threads/events/subscribers_changed %>
- <% # Template Dependency: recordings/threads/events/completed %>
- <% # Template Dependency: recordings/threads/events/uncompleted %>
```
-
- Into:
-
- ```erb
- <% # Template Dependency: recordings/threads/events/* %>
+ 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"
```
- *Kasper Timm Hansen*
+ Calling `word_wrap` should not trim the indents on the first and last lines.
-* Allow defining explicit collection caching using a `# Template Collection: ...`
- directive inside templates.
+ Fixes #34487.
- *Dov Murik*
+ *Lyle Mullican*
-* Asset helpers raise `ArgumentError` when `nil` is passed as a source.
+* Add allocations to template rendering instrumentation.
- *Anton Kolomiychuk*
+ Adds the allocations for template and partial rendering to the server output on render.
-* 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.
-
- *Roque Pinel*
+ ```
+ 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)
+ ```
-* Improve detection of partial templates eligible for collection caching,
- now allowing multi-line comments at the beginning of the template file.
+ *Eileen M. Uchitelle*, *Aaron Patterson*
- *Dov Murik*
+* Respect the `only_path` option passed to `url_for` when the options are passed in as an array
-* Raise an `ArgumentError` when a false value for `include_blank` is passed to a
- required select field (to comply with the HTML5 spec).
+ Fixes #33237.
- *Grey Baker*
+ *Joel Ambass*
-* Do not put partial name to `local_assigns` when rendering without
- an object or a collection.
+* Deprecate calling private model methods from view helpers.
- *Henrik Nygren*
+ For example, in methods like `options_from_collection_for_select`
+ and `collection_select` it is possible to call private methods from
+ the objects used.
-* Remove `:rescue_format` option for `translate` helper since it's no longer
- supported by I18n.
+ Fixes #33546.
- *Bernard Potocki*
+ *Ana María Martínez Gómez*
-* `translate` should handle `raise` flag correctly in case of both main and default
- translation is missing.
+* Fix issue with `button_to`'s `to_form_params`
- Fixes #19967.
+ `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.
- *Bernard Potocki*
+ The issue is fixed by turning all keys to strings inside
+ `to_form_params` before comparing them.
-* Load the `default_form_builder` from the controller on initialization, which overrides
- the global config if it is present.
+ *Georgi Georgiev*
- *Kevin McPhillips*
+* Mark arrays of translations as trusted safe by using the `_html` suffix.
-* Accept lambda as `child_index` option in `fields_for` method.
+ Example:
- *Karol Galanciak*
+ en:
+ foo_html:
+ - "One"
+ - "<strong>Two</strong>"
+ - "Three &#128075; &#128578;"
-* `translate` allows `default: [[]]` again for a default value of `[]`.
+ *Juan Broullon*
- Fixes #19640.
+* 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.
- *Adam Prescott*
+ Example:
-* `translate` should accept nils as members of the `:default`
- parameter without raising a translation missing error.
+ date_select('user_birthday', '', start_year: 1998, end_year: 2000, year_format: ->year { "Heisei #{year - 1988}" })
- Fixes #19419.
+ The HTML produced:
- *Justin Coyne*
+ <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 */
-* `number_to_percentage` does not crash with `Float::NAN` or `Float::INFINITY`
- as input when `precision: 0` is used.
+ *Koki Ryu*
- Fixes #19227.
+* Fix JavaScript views rendering does not work with Firefox when using
+ Content Security Policy.
- *Yves Senn*
+ Fixes #32577.
-* Fixed the translation helper method to accept different default values types
- besides String.
+ *Yuji Yaginuma*
- *Ulisses Almeida*
+* 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 raising an 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.5.0 or newer.
-* Allow entries without a link tag in `AtomFeedHelper`.
+ *Jeremy Daer*, *Kasper Timm Hansen*
- *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 8573eb1225..ab7c27c209 100644
--- a/actionview/MIT-LICENSE
+++ b/actionview/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2016 David Heinemeier Hansson
+Copyright (c) 2004-2019 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 7a3e5e31d0..ba50c67024 100644
--- a/actionview/README.rdoc
+++ b/actionview/README.rdoc
@@ -5,13 +5,15 @@ view helpers that assist when building HTML forms, Atom feeds and more.
Template formats that Action View handles are ERB (embedded Ruby, typically
used to inline short Ruby snippets inside HTML), and XML Builder.
+You can read more about Action View in the {Action View Overview}[https://edgeguides.rubyonrails.org/action_view_overview.html] guide.
+
== Download and installation
The latest version of Action View can be installed with RubyGems:
$ gem install actionview
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/actionview
@@ -20,16 +22,16 @@ 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
API documentation is at
-* http://api.rubyonrails.org
+* https://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/actionview/RUNNING_UJS_TESTS.rdoc b/actionview/RUNNING_UJS_TESTS.rdoc
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 d41030c650..237e458b6f 100644
--- a/actionview/Rakefile
+++ b/actionview/Rakefile
@@ -1,44 +1,95 @@
-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
-task "package:clean"
+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
+ system("npm run lint")
+ exit $?.exitstatus unless $?.success?
+
+ begin
+ listen_host = "localhost"
+ listen_port = "4567"
+
+ runner_command = %w(ruby ../ci/qunit-selenium-runner.rb)
+ if ENV["SELENIUM_DRIVER_URL"]
+ require "socket"
+ runner_command += %W(http://#{Socket.gethostname}:#{listen_port}/ #{ENV["SELENIUM_DRIVER_URL"]})
+ listen_host = "0.0.0.0"
+ else
+ runner_command += %W(http://localhost:#{listen_port}/)
+ end
+
+ Dir.mkdir("log")
+ pid = File.open("log/test.log", "w") do |f|
+ spawn(*%W(rackup test/ujs/config.ru -o #{listen_host} -p #{listen_port} -s puma), out: f, err: f, pgroup: true)
+ end
+
+ start_time = Time.now
+
+ loop do
+ break if system("lsof -i :4567", 1 => File::NULL)
+
+ if Time.now - start_time > 5
+ puts "Failed to start puma after 5 seconds"
+ puts
+ puts File.read("log/test.log")
+ exit 1
+ end
+
+ sleep 0.2
+ end
+
+ system(*runner_command)
+ 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
@@ -47,8 +98,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 8b0e031dee..200c775dcc 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.5.0"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "https://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://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..03319ea365
--- /dev/null
+++ b/actionview/app/assets/javascripts/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2007-2019 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..aa167004b6
--- /dev/null
+++ b/actionview/app/assets/javascripts/README.md
@@ -0,0 +1,56 @@
+# 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
+require("@rails/ujs").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]: https://validator.w3.org/
+[csrf]: https://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..4cfaead078
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/features/disable.coffee
@@ -0,0 +1,93 @@
+#= 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) ->
+ return if getData(element, 'ujs:disabled')
+ 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) ->
+ return if getData(element, 'ujs:disabled')
+ 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..a5b61220bb
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/features/remote.coffee
@@ -0,0 +1,93 @@
+#= 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.preventInsignificantClick = (e) ->
+ link = this
+ method = (link.getAttribute('data-method') or 'GET').toUpperCase()
+ data = link.getAttribute('data-params')
+ metaClick = e.metaKey or e.ctrlKey
+ insignificantMetaClick = metaClick and method is 'GET' and not data
+ primaryMouseKey = e.button is 0
+ e.stopImmediatePropagation() if not primaryMouseKey or insignificantMetaClick
+
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..0347058195
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/start.coffee
@@ -0,0 +1,75 @@
+{
+ fire, delegate
+ getData, $
+ refreshCSRFTokens, CSRFProtection
+ loadCSPNonce
+ enableElement, disableElement, handleDisabledElement
+ handleConfirm, preventInsignificantClick
+ handleRemote, formSubmitButtonClick,
+ 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', preventInsignificantClick
+ delegate document, Rails.linkClickSelector, 'click', handleDisabledElement
+ delegate document, Rails.linkClickSelector, 'click', handleConfirm
+ delegate document, Rails.linkClickSelector, 'click', disableElement
+ delegate document, Rails.linkClickSelector, 'click', handleRemote
+ delegate document, Rails.linkClickSelector, 'click', handleMethod
+
+ delegate document, Rails.buttonClickSelector, 'click', preventInsignificantClick
+ 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', preventInsignificantClick
+ delegate document, Rails.formInputClickSelector, 'click', handleDisabledElement
+ delegate document, Rails.formInputClickSelector, 'click', handleConfirm
+ delegate document, Rails.formInputClickSelector, 'click', formSubmitButtonClick
+
+ document.addEventListener('DOMContentLoaded', refreshCSRFTokens)
+ document.addEventListener('DOMContentLoaded', loadCSPNonce)
+ 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..5b223d50f6
--- /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(/\b(xml|html|svg)\b/)
+ parser = new DOMParser()
+ type = type.replace(/;.+/, '') # remove something like ';charset=utf-8'
+ try response = parser.parseFromString(response, type)
+ response
+
+# Default way to get an element's href. May be overridden at Rails.href.
+Rails.href = (element) -> element.href
+
+# Determines if the request is a cross domain request.
+Rails.isCrossDomain = (url) ->
+ originAnchor = document.createElement('a')
+ originAnchor.href = location.href
+ urlAnchor = document.createElement('a')
+ try
+ urlAnchor.href = url
+ # If URL protocol is false or is a string containing a single colon
+ # *and* host are false, assume it is not a cross-domain request
+ # (should only be the case for IE7 and IE compatibility mode).
+ # Otherwise, evaluate protocol and host of the URL against the origin
+ # protocol and host.
+ !(((!urlAnchor.protocol || urlAnchor.protocol == ':') && !urlAnchor.host) ||
+ (originAnchor.protocol + '//' + originAnchor.host == urlAnchor.protocol + '//' + urlAnchor.host))
+ catch e
+ # If there is an error parsing the URL, assume it is crossDomain.
+ true
diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee
new file mode 100644
index 0000000000..a33f531375
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee
@@ -0,0 +1,8 @@
+nonce = null
+
+Rails.loadCSPNonce = ->
+ nonce = document.querySelector("meta[name=csp-nonce]")?.content
+
+# Returns the Content-Security-Policy nonce for inline scripts.
+Rails.cspNonce = ->
+ nonce ? Rails.loadCSPNonce()
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..768d9683d4
--- /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 corresponds 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 0a87500a52..f398ba0ff0 100644
--- a/actionview/lib/action_view.rb
+++ b/actionview/lib/action_view.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2019 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
@@ -33,7 +35,6 @@ module ActionView
eager_autoload do
autoload :Base
autoload :Context
- autoload :CompiledTemplates, "action_view/context"
autoload :Digestor
autoload :Helpers
autoload :LookupContext
@@ -43,6 +44,7 @@ module ActionView
autoload :Rendering
autoload :RoutingUrlFor
autoload :Template
+ autoload :FileTemplate
autoload :ViewPaths
autoload_under "renderer" do
@@ -74,12 +76,12 @@ module ActionView
autoload :MissingTemplate
autoload :ActionViewError
autoload :EncodingError
- autoload :MissingRequestError
autoload :TemplateError
autoload :WrongEncodingError
end
end
+ autoload :CacheExpiry
autoload :TestCase
def self.eager_load!
@@ -89,8 +91,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 ad1cb1a4be..5253ef7b0c 100644
--- a/actionview/lib/action_view/base.rb
+++ b/actionview/lib/action_view/base.rb
@@ -1,23 +1,26 @@
-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 "active_support/deprecation"
+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 +28,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 +36,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
#
@@ -110,7 +113,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,36 +143,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
- @@automatically_disable_submit_tag = true
+ 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?
@@ -182,36 +180,133 @@ module ActionView #:nodoc:
def xss_safe? #:nodoc:
true
end
+
+ def with_empty_template_cache # :nodoc:
+ subclass = Class.new(self) {
+ # We can't implement these as self.class because subclasses will
+ # share the same template cache as superclasses, so "changed?" won't work
+ # correctly.
+ define_method(:compiled_method_container) { subclass }
+ define_singleton_method(:compiled_method_container) { subclass }
+ }
+ end
+
+ def changed?(other) # :nodoc:
+ compiled_method_container != other.compiled_method_container
+ end
end
- attr_accessor :view_renderer
+ attr_reader :view_renderer, :lookup_context
attr_internal :config, :assigns
- delegate :lookup_context, :to => :view_renderer
- delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, :to => :lookup_context
+ 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) }
end
- def initialize(context = nil, assigns = {}, controller = nil, formats = nil) #:nodoc:
+ # :stopdoc:
+
+ def self.build_lookup_context(context)
+ case context
+ when ActionView::Renderer
+ context.lookup_context
+ when Array
+ ActionView::LookupContext.new(context)
+ when ActionView::PathSet
+ ActionView::LookupContext.new(context)
+ when nil
+ ActionView::LookupContext.new([])
+ else
+ raise NotImplementedError, context.class.name
+ end
+ end
+
+ def self.empty
+ with_view_paths([])
+ end
+
+ def self.with_view_paths(view_paths, assigns = {}, controller = nil)
+ with_context ActionView::LookupContext.new(view_paths), assigns, controller
+ end
+
+ def self.with_context(context, assigns = {}, controller = nil)
+ new context, assigns, controller
+ end
+
+ NULL = Object.new
+
+ # :startdoc:
+
+ def initialize(lookup_context = nil, assigns = {}, controller = nil, formats = NULL) #:nodoc:
@_config = ActiveSupport::InheritableOptions.new
- if context.is_a?(ActionView::Renderer)
- @view_renderer = context
+ unless formats == NULL
+ ActiveSupport::Deprecation.warn <<~eowarn.squish
+ Passing formats to ActionView::Base.new is deprecated
+ eowarn
+ end
+
+ case lookup_context
+ when ActionView::LookupContext
+ @lookup_context = lookup_context
else
- lookup_context = context.is_a?(ActionView::LookupContext) ?
- context : ActionView::LookupContext.new(context)
- lookup_context.formats = formats if formats
- lookup_context.prefixes = controller._prefixes if controller
- @view_renderer = ActionView::Renderer.new(lookup_context)
+ ActiveSupport::Deprecation.warn <<~eowarn.squish
+ ActionView::Base instances should be constructed with a lookup context,
+ assignments, and a controller.
+ eowarn
+ @lookup_context = self.class.build_lookup_context(lookup_context)
end
+ @view_renderer = ActionView::Renderer.new @lookup_context
+ @current_template = nil
+
+ @cache_hit = {}
assign(assigns)
assign_controller(controller)
_prepare_context
end
+ def _run(method, template, locals, buffer, &block)
+ _old_output_buffer, _old_virtual_path, _old_template = @output_buffer, @virtual_path, @current_template
+ @current_template = template
+ @output_buffer = buffer
+ send(method, locals, buffer, &block)
+ ensure
+ @output_buffer, @virtual_path, @current_template = _old_output_buffer, _old_virtual_path, _old_template
+ end
+
+ def compiled_method_container
+ if self.class == ActionView::Base
+ ActiveSupport::Deprecation.warn <<~eowarn.squish
+ ActionView::Base instances must implement `compiled_method_container`
+ or use the class method `with_empty_template_cache` for constructing
+ an ActionView::Base instances that has an empty cache.
+ eowarn
+ end
+
+ self.class
+ end
+
+ def in_rendering_context(options)
+ old_view_renderer = @view_renderer
+ old_lookup_context = @lookup_context
+
+ if !lookup_context.html_fallback_for_js && options[:formats]
+ formats = Array(options[:formats])
+ if formats == [:js]
+ formats << :html
+ end
+ @lookup_context = lookup_context.with_prepended_formats(formats)
+ @view_renderer = ActionView::Renderer.new @lookup_context
+ end
+
+ yield @view_renderer
+ ensure
+ @view_renderer = old_view_renderer
+ @lookup_context = old_lookup_context
+ end
+
ActiveSupport.run_load_hooks(:action_view, self)
end
end
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/cache_expiry.rb b/actionview/lib/action_view/cache_expiry.rb
new file mode 100644
index 0000000000..820afc093d
--- /dev/null
+++ b/actionview/lib/action_view/cache_expiry.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module ActionView
+ class CacheExpiry
+ class Executor
+ def initialize(watcher:)
+ @cache_expiry = CacheExpiry.new(watcher: watcher)
+ end
+
+ def before(target)
+ @cache_expiry.clear_cache_if_necessary
+ end
+ end
+
+ def initialize(watcher:)
+ @watched_dirs = nil
+ @watcher_class = watcher
+ @watcher = nil
+ end
+
+ def clear_cache_if_necessary
+ watched_dirs = dirs_to_watch
+ if watched_dirs != @watched_dirs
+ @watched_dirs = watched_dirs
+ @watcher = @watcher_class.new([], watched_dirs) do
+ clear_cache
+ end
+ @watcher.execute
+ else
+ @watcher.execute_if_updated
+ end
+ end
+
+ def clear_cache
+ ActionView::LookupContext::DetailsKey.clear
+ end
+
+ private
+
+ def dirs_to_watch
+ fs_paths = all_view_paths.grep(FileSystemResolver)
+ fs_paths.map(&:path).sort.uniq
+ end
+
+ def all_view_paths
+ ActionView::ViewPaths.all_view_paths.flat_map(&:paths)
+ end
+ end
+end
diff --git a/actionview/lib/action_view/context.rb b/actionview/lib/action_view/context.rb
index ee263df484..2b22c30a3a 100644
--- a/actionview/lib/action_view/context.rb
+++ b/actionview/lib/action_view/context.rb
@@ -1,23 +1,20 @@
-module ActionView
- module CompiledTemplates #:nodoc:
- # holds compiled template code
- end
+# frozen_string_literal: true
+module ActionView
# = Action View Context
#
# 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 +24,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 7731773040..182f6e2eef 100644
--- a/actionview/lib/action_view/dependency_tracker.rb
+++ b/actionview/lib/action_view/dependency_tracker.rb
@@ -1,5 +1,7 @@
-require 'concurrent/map'
-require 'action_view/path_set'
+# frozen_string_literal: true
+
+require "concurrent/map"
+require "action_view/path_set"
module ActionView
class DependencyTracker # :nodoc:
@@ -105,7 +107,6 @@ module ActionView
attr_reader :name, :template
private :name, :template
-
private
def source
template.source
@@ -142,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}"
@@ -163,7 +164,7 @@ module ActionView
def explicit_dependencies
dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
- wildcards, explicits = dependencies.partition { |dependency| dependency[-1] == '*' }
+ wildcards, explicits = dependencies.partition { |dependency| dependency[-1] == "*" }
(explicits + resolve_directories(wildcards)).uniq
end
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index b99d1af998..97a6da3634 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -1,17 +1,10 @@
-require 'concurrent/map'
-require 'action_view/dependency_tracker'
-require 'monitor'
+# frozen_string_literal: true
+
+require "action_view/dependency_tracker"
module ActionView
class Digestor
- @@digest_mutex = Mutex.new
-
- class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc:
- def call(env)
- ActionView::LookupContext::DetailsKey.clear
- app.call(env)
- end
- end
+ @@digest_mutex = Mutex.new
class << self
# Supported options:
@@ -19,10 +12,12 @@ 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(name:, finder:, dependencies: [])
- dependencies ||= []
- cache_key = ([ name ].compact + dependencies).join('.')
+ def digest(name:, format:, finder:, dependencies: nil)
+ if dependencies.nil? || dependencies.empty?
+ cache_key = "#{name}.#{format}"
+ else
+ cache_key = [ name, format, dependencies ].flatten.compact.join(".")
+ end
# this is a correctly done double-checked locking idiom
# (Concurrent::Map's lookups have volatile semantics)
@@ -32,7 +27,7 @@ module ActionView
root = tree(name, finder, partial)
dependencies.each do |injected_dep|
root.children << Injected.new(injected_dep, nil, nil)
- end
+ end if dependencies
finder.digest_cache[cache_key] = root.digest(finder)
end
end
@@ -46,9 +41,7 @@ module ActionView
def tree(name, finder, partial = false, seen = {})
logical_name = name.gsub(%r|/_|, "/")
- if finder.disable_cache { finder.exists?(logical_name, [], partial) }
- template = finder.disable_cache { finder.find(logical_name, [], partial) }
-
+ if template = find_template(finder, logical_name, [], partial, [])
if node = seen[template.identifier] # handle cycles in the tree
node
else
@@ -61,11 +54,20 @@ module ActionView
node
end
else
- logger.error " '#{name}' file doesn't exist, so no dependencies"
- logger.error " Couldn't find template for digesting: #{name}"
+ unless name.include?("#") # Dynamic template partial names can never be tracked
+ logger.error " Couldn't find template for digesting: #{name}"
+ end
+
seen[name] ||= Missing.new(name, logical_name, nil)
end
end
+
+ private
+ def find_template(finder, name, prefixes, partial, keys)
+ finder.disable_cache do
+ finder.find_all(name, prefixes, partial, keys).first
+ end
+ end
end
class Node
@@ -84,7 +86,7 @@ module ActionView
end
def digest(finder, stack = [])
- Digest::MD5.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}")
+ ActiveSupport::Digest.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}")
end
def dependency_digest(finder, stack)
@@ -108,7 +110,7 @@ module ActionView
class Partial < Node; end
class Missing < Node
- def digest(finder, _ = []) '' end
+ def digest(finder, _ = []) "" end
end
class Injected < Node
diff --git a/actionview/lib/action_view/file_template.rb b/actionview/lib/action_view/file_template.rb
new file mode 100644
index 0000000000..dea02176eb
--- /dev/null
+++ b/actionview/lib/action_view/file_template.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require "action_view/template"
+
+module ActionView
+ class FileTemplate < Template
+ def initialize(filename, handler, details)
+ @filename = filename
+
+ super(nil, filename, handler, details)
+ end
+
+ def source
+ File.binread @filename
+ end
+
+ def refresh(_)
+ self
+ end
+
+ # Exceptions are marshalled when using the parallel test runner with DRb, so we need
+ # to ensure that references to the template object can be marshalled as well. This means forgoing
+ # the marshalling of the compiler mutex and instantiating that again on unmarshalling.
+ def marshal_dump # :nodoc:
+ [ @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant ]
+ end
+
+ def marshal_load(array) # :nodoc:
+ @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant = *array
+ @compile_mutex = Mutex.new
+ end
+ end
+end
diff --git a/actionview/lib/action_view/flows.rb b/actionview/lib/action_view/flows.rb
index bc61920848..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.
@@ -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 efb565bf59..5bed37583e 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 = "beta3"
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 413c35954c..59d70a1dc4 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', 'host').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".freeze, "", 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', 'host').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,8 +183,8 @@ 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(
@@ -138,7 +192,7 @@ module ActionView
"rel" => tag_options[:rel] || "alternate",
"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,94 +223,156 @@ 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" />
+ # # => <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" />
- def image_tag(source, options={})
+ # 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 blobs (images that are uploaded by the users of your app):
+ #
+ # image_tag(user.avatar)
+ # # => <img src="/rails/active_storage/blobs/.../tiger.jpg" />
+ # image_tag(user.avatar.variant(resize_to_limit: [100, 100]))
+ # # => <img src="/rails/active_storage/representations/.../tiger.jpg" />
+ # image_tag(user.avatar.variant(resize_to_limit: [100, 100]), size: '100')
+ # # => <img width="100" height="100" src="/rails/active_storage/representations/.../tiger.jpg" />
+ def image_tag(source, options = {})
options = options.symbolize_keys
check_for_image_tag_errors(options)
+ 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]
tag("img", options)
end
- # Returns a string suitable for an HTML image tag alt attribute.
- # The +src+ argument is meant to be an image file path.
- # The method removes the basename of the file path and the digest,
- # if any. It also removes hyphens and underscores from file names and
- # replaces them with spaces, returning a space-separated, titleized
- # string.
- #
- # ==== Examples
- #
- # image_alt('rails.png')
- # # => Rails
- #
- # image_alt('hyphenated-file-name.png')
- # # => Hyphenated file name
- #
- # image_alt('underscored_file_name.png')
- # # => Underscored file name
- def image_alt(src)
- File.basename(src, '.*'.freeze).sub(/-[[:xdigit:]]{32,64}\z/, ''.freeze).tr('-_'.freeze, ' '.freeze).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
#
@@ -264,10 +380,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")
@@ -281,15 +399,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>
@@ -300,31 +426,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
@@ -334,6 +471,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 717b326740..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,7 +29,7 @@ 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" />
#
@@ -36,11 +38,11 @@ module ActionView
# 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
+ # <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" />
#
@@ -55,8 +57,8 @@ module ActionView
# You can read more about setting up your DNS CNAME records from your ISP.
#
# Note: This is purely a browser performance optimization and is not meant
- # for server load balancing. See http://www.die.net/musings/page_load_time/
- # for background and http://www.browserscope.org/?category=network for
+ # for server load balancing. See https://www.die.net/musings/page_load_time/
+ # for background and https://www.browserscope.org/?category=network for
# connection limit data.
#
# Alternatively, you can exert more control over the asset host by setting
@@ -66,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" />
#
@@ -85,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" />
#
@@ -95,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?
@@ -117,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(/([\?#].+)$/, ''.freeze)
+ 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
@@ -168,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
@@ -202,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
@@ -213,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)
@@ -251,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
@@ -260,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
@@ -278,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
@@ -287,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
@@ -308,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
@@ -317,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
@@ -334,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
@@ -343,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.
@@ -360,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
@@ -369,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
@@ -385,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 c875f5870f..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).
@@ -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
@@ -138,7 +140,7 @@ module ActionView
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 4c7c4b91c6..020aebeea3 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 %>
@@ -106,9 +111,9 @@ module ActionView
# <%= 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 app/views or paths
+ # 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 `app/views/recordings/events` would be `recordings/events/*` etc.
+ # 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.
@@ -118,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 %>
@@ -128,13 +133,14 @@ module ActionView
#
# === Collection Caching
#
- # When rendering a collection of objects that each use the same partial, a `cached`
+ # When rendering a collection of objects that each use the same partial, a <tt>:cached</tt>
# option can be passed.
+ #
# For collections rendered such:
#
- # <%= render partial: 'notifications/notification', collection: @notifications, cached: true %>
+ # <%= render partial: 'projects/project', collection: @projects, cached: true %>
#
- # The `cached: true` will make Action View's rendering read several templates
+ # The <tt>cached: true</tt> will make Action View's rendering read several templates
# from cache at once instead of one call per template.
#
# Templates in the collection not already cached are written to cache.
@@ -142,13 +148,21 @@ module ActionView
# Works great alongside individual template fragment caching.
# For instance if the template the collection renders is cached like:
#
- # # notifications/_notification.html.erb
- # <% cache notification do %>
+ # # projects/_project.html.erb
+ # <% cache project do %>
# <%# ... %>
# <% end %>
#
# Any collection renders will find those cached templates when attempting
# to read multiple templates at once.
+ #
+ # 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:
+ #
+ # <%= render partial: 'projects/project', collection: @projects, cached: -> project { [ project, current_user ] } %>
+ #
+ # 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
name_options = options.slice(:skip_digest, :virtual_path)
@@ -187,46 +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
+
+ def digest_path_from_template(template) # :nodoc:
+ digest = Digestor.digest(name: template.virtual_path, format: template.format, finder: lookup_context, dependencies: view_cache_dependencies)
+
+ if digest.present?
+ "#{template.virtual_path}:#{digest}"
+ else
+ template.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
- name = controller.url_for(name).split("://").last if name.is_a?(Hash)
- digest = Digestor.digest name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies
- [ name, digest ]
+
+ if virtual_path || digest_path
+ name = controller.url_for(name).split("://").last if name.is_a?(Hash)
+
+ digest_path ||= digest_path_from_template(@current_template)
+
+ [ 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 df8d0affd0..c87c212cc7 100644
--- a/actionview/lib/action_view/helpers/capture_helper.rb
+++ b/actionview/lib/action_view/helpers/capture_helper.rb
@@ -1,13 +1,15 @@
-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 extracts part of a template as a String object.
# You can then use this object anywhere in your templates, layout, or helpers.
@@ -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,7 +114,7 @@ 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 %>
@@ -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..4415018845
--- /dev/null
+++ b/actionview/lib/action_view/helpers/csp_helper.rb
@@ -0,0 +1,26 @@
+# 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(**options)
+ if content_security_policy?
+ options[:name] = "csp-nonce"
+ options[:content] = content_security_policy_nonce
+ tag("meta", options)
+ 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..c0422c6ff5 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?
+ if defined?(protect_against_forgery?) && 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 233e613e97..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
@@ -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,17 @@ 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.
@@ -264,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
@@ -274,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
@@ -300,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.
#
@@ -345,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.
@@ -396,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
@@ -435,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
@@ -475,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
@@ -667,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>
#
@@ -679,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".freeze, 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[
@@ -707,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
@@ -778,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
@@ -798,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
@@ -808,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".freeze, 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
@@ -818,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
@@ -838,7 +852,7 @@ 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
@@ -858,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
@@ -883,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):
@@ -911,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]
@@ -973,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".freeze, 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
@@ -990,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".freeze, '', :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".freeze, 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".freeze, prompt, :value => '') : ''
+ prompt ? content_tag("option", prompt, value: "") : ""
end
# Builds hidden input tag for date part and value.
@@ -1026,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
@@ -1042,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
@@ -1053,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
@@ -1062,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
@@ -1076,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 7ced37572e..5533cef249 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, &block)
+ 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, &block)
+ 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.
@@ -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!'); }"/>
#
@@ -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" />
@@ -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,42 +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 = {})
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- datetime_field is deprecated and will be removed in Rails 5.1.
- Use datetime_local_field instead.
- MESSAGE
- Tags::DatetimeField.new(object_name, method, self, options).render
- end
-
# 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"
@@ -1135,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")
@@ -1223,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
@@ -1231,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
@@ -1257,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
@@ -1296,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
@@ -1319,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
@@ -1333,19 +1663,245 @@ 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|
+ ##
+ # :method: text_field
+ #
+ # :call-seq: text_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#text_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.text_field :name %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: password_field
+ #
+ # :call-seq: password_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#password_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.password_field :password %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: text_area
+ #
+ # :call-seq: text_area(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#text_area for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.text_area :detail %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: color_field
+ #
+ # :call-seq: color_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#color_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.color_field :favorite_color %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: search_field
+ #
+ # :call-seq: search_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#search_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.search_field :name %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: telephone_field
+ #
+ # :call-seq: telephone_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#telephone_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.telephone_field :phone %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: phone_field
+ #
+ # :call-seq: phone_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#phone_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.phone_field :phone %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: date_field
+ #
+ # :call-seq: date_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#date_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.date_field :born_on %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: time_field
+ #
+ # :call-seq: time_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#time_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.time_field :borned_at %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: datetime_field
+ #
+ # :call-seq: datetime_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#datetime_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.datetime_field :graduation_day %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: datetime_local_field
+ #
+ # :call-seq: datetime_local_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#datetime_local_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.datetime_local_field :graduation_day %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: month_field
+ #
+ # :call-seq: month_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#month_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.month_field :birthday_month %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: week_field
+ #
+ # :call-seq: week_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#week_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.week_field :birthday_week %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: url_field
+ #
+ # :call-seq: url_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#url_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.url_field :homepage %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: email_field
+ #
+ # :call-seq: email_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#email_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.email_field :address %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: number_field
+ #
+ # :call-seq: number_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#number_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.number_field :age %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ ##
+ # :method: range_field
+ #
+ # :call-seq: range_field(method, options = {})
+ #
+ # Wraps ActionView::Helpers::FormHelper#range_field for form builders:
+ #
+ # <%= form_with model: @user do |f| %>
+ # <%= f.range_field :age %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+
+ (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(
@@ -1414,9 +1970,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
#
@@ -1614,26 +2170,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 = 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
+ "#{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.
@@ -1760,7 +2327,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" />
@@ -1837,11 +2404,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:
@@ -1849,7 +2416,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:
@@ -1857,7 +2424,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)
@@ -1870,11 +2437,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:
@@ -1882,7 +2449,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:
@@ -1907,7 +2474,7 @@ module ActionView
@template.button_tag(value, options, &block)
end
- def emitted_hidden_id?
+ def emitted_hidden_id? # :nodoc:
@emitted_hidden_id ||= nil
end
@@ -1927,7 +2494,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}"
@@ -1982,12 +2554,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 430051379d..a7747456a4 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".freeze, 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".freeze, prompt_text(prompt), value: "")
+ body.safe_concat content_tag("option", prompt_text(prompt), value: "")
end
grouped_options.each do |container|
@@ -541,7 +548,7 @@ module ActionView
end
html_attributes = { label: label }.merge!(html_attributes)
- body.safe_concat content_tag("optgroup".freeze, options_for_select(container, selected_key), html_attributes)
+ body.safe_concat content_tag("optgroup", options_for_select(container, selected_key), html_attributes)
end
body
@@ -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".freeze, '-------------', value: '', disabled: true)
+ zone_options.safe_concat content_tag("option", "-------------", value: "", disabled: true)
zone_options.safe_concat "\n"
zones = zones - priority_zones
@@ -647,15 +654,15 @@ module ActionView
#
# ==== Gotcha
#
- # The HTML specification says when nothing is select on a collection of radio buttons
+ # The HTML specification says when nothing is selected 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 none category is selected no +category_id+ parameter is sent. So,
- # any strong parameters idiom like
+ # 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 +{user: ...}+ will be present.
+ # 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.
@@ -787,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
@@ -795,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
@@ -813,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:
@@ -825,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:
@@ -837,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:
@@ -849,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:
@@ -861,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:
@@ -873,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 cfff0bef5d..5d4ff36425 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 a 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
@@ -113,7 +117,7 @@ module ActionView
# # <option>Write</option></select>
#
# select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: true
- # # => <select id="people" name="people"><option value=""></option><option value="1">David</option></select>
+ # # => <select id="people" name="people"><option value="" label=" "></option><option value="1">David</option></select>
#
# select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: "All"
# # => <select id="people" name="people"><option value="">All</option><option value="1">David</option></select>
@@ -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".freeze, 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".freeze, prompt, value: '').safe_concat(option_tags)
+ option_tags = content_tag("option", prompt, value: "").safe_concat(option_tags)
end
- content_tag "select".freeze, 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" />
@@ -440,31 +446,19 @@ module ActionView
# # => <input name='commit' type='submit' value='Save' data-disable-with="Save" data-confirm="Are you sure?" />
#
def submit_tag(value = "Save changes", options = {})
- options = options.stringify_keys
+ options = options.deep_stringify_keys
tag_options = { "type" => "submit", "name" => "commit", "value" => value }.update(options)
-
- if ActionView::Base.automatically_disable_submit_tag
- unless tag_options["data-disable-with"] == false || (tag_options["data"] && tag_options["data"][:disable_with] == false)
- disable_with_text = tag_options["data-disable-with"]
- disable_with_text ||= tag_options["data"][:disable_with] if tag_options["data"]
- disable_with_text ||= value.to_s.clone
- tag_options.deep_merge!("data" => { "disable_with" => disable_with_text })
- else
- tag_options["data"].delete(:disable_with) if tag_options["data"]
- end
- tag_options.delete("data-disable-with")
- end
-
+ set_default_disable_with value, tag_options
tag :input, tag_options
end
# Creates a button element that defines a <tt>submit</tt> button,
- # <tt>reset</tt>button or a generic button which can be used in
+ # <tt>reset</tt> button or a generic button which can be used in
# JavaScript, for example. You can use the button tag as a regular
# submit tag but it isn't supported in legacy browsers. However,
# the button tag does allow for richer labels such as images and emphasis,
# 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.
@@ -516,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
@@ -542,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.
@@ -582,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".freeze, 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
@@ -683,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.
@@ -691,23 +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 = {})
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- datetime_field_tag is deprecated and will be removed in Rails 5.1.
- Use datetime_local_field_tag instead.
- MESSAGE
- text_field_tag(name, value, options.merge(type: :datetime))
+ 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".
#
@@ -868,11 +850,12 @@ module ActionView
authenticity_token = html_options.delete("authenticity_token")
method = html_options.delete("method").to_s.downcase
- method_tag = case method
- when 'get'
+ method_tag = \
+ case method
+ when "get"
html_options["method"] = "get"
- ''
- when 'post', ''
+ ""
+ when "post", ""
html_options["method"] = "post"
token_tag(authenticity_token, form_options: {
action: html_options["action"],
@@ -884,9 +867,9 @@ module ActionView
action: html_options["action"],
method: method
})
- end
+ 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
@@ -906,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 ed7e882c94..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
@@ -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".freeze, 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 161aa031c6..35206b7e48 100644
--- a/actionview/lib/action_view/helpers/number_helper.rb
+++ b/actionview/lib/action_view/helpers/number_helper.rb
@@ -1,11 +1,12 @@
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/string/output_safety'
-require 'active_support/number_helper'
+# frozen_string_literal: true
+
+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.
@@ -13,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
@@ -23,7 +23,7 @@ module ActionView
end
end
- # Formats a +number+ into a US phone number (e.g., (555)
+ # Formats a +number+ into a phone number (US by default e.g., (555)
# 123-9876). You can customize the format in the +options+ hash.
#
# ==== Options
@@ -35,6 +35,8 @@ module ActionView
# end of the generated number.
# * <tt>:country_code</tt> - Sets the country code for the phone
# number.
+ # * <tt>:pattern</tt> - Specifies how the number is divided into three
+ # groups with the custom regexp to override the default format.
# * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
# the argument is invalid.
#
@@ -52,6 +54,11 @@ module ActionView
#
# number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: ".")
# # => +1.123.555.1234 x 1343
+ #
+ # number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true)
+ # # => "(755) 6123-4567"
+ # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/))
+ # # => "133-1234-5678"
def number_to_phone(number, options = {})
return unless number
options = options.symbolize_keys
@@ -87,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
#
@@ -110,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
@@ -166,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.
#
@@ -182,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)
@@ -256,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.
#
@@ -388,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 c0fc3b820f..52a951b2ca 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
@@ -25,14 +27,44 @@ module ActionView #:nodoc:
# safe_join([raw("<p>foo</p>"), "<p>bar</p>"], "<br />")
# # => "<p>foo</p>&lt;br /&gt;&lt;p&gt;bar&lt;/p&gt;"
#
- # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />")
+ # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />"))
# # => "<p>foo</p><br /><p>bar</p>"
#
- def safe_join(array, sep=$,)
+ 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}[https://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 c98f2d74a8..7ead691113 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,7 +13,6 @@ 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
@@ -26,13 +27,15 @@ module ActionView
def render(options = {}, locals = {}, &block)
case options
when Hash
- if block_given?
- view_renderer.render_partial(self, options.merge(:partial => options[:layout]), &block)
- else
- view_renderer.render(self, options)
+ in_rendering_context(options) do |renderer|
+ if block_given?
+ view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
+ else
+ view_renderer.render(self, options)
+ end
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 f9784c3483..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 compatibility and can be removed in Rails 5.1.
+ # 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 42e7358a1d..3979721d34 100644
--- a/actionview/lib/action_view/helpers/tag_helper.rb
+++ b/actionview/lib/action_view/helpers/tag_helper.rb
@@ -1,39 +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 { "".freeze }
+ 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
+
+ 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.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 empty HTML tag of type +name+ which by default is XHTML
+ # 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
@@ -42,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 />
#
@@ -67,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
@@ -81,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
@@ -104,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
@@ -124,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
@@ -140,56 +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]}#{content}</#{name}>".html_safe
- end
-
- def tag_options(options, escape = true)
- return if options.blank?
- output = ""
- sep = " ".freeze
- 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
-
- 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, " ".freeze) : value.join(" ".freeze)
- 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..b58e1a6680 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:]]]/, "").downcase
+ 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 3dda47a458..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,9 +9,10 @@ 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
@@ -20,13 +23,13 @@ module ActionView
private
- def render_component(builder)
- builder.check_box + builder.label
- end
+ def render_component(builder)
+ builder.check_box + builder.label
+ end
- def hidden_field_name #:nodoc:
- "#{super}[]"
- 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 fb51460c8e..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,7 +19,7 @@ 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]
@@ -36,81 +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
-
- # 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
-
- [:checked, :selected, :disabled, :readonly].each do |option|
- current_value = @options[option]
- next if current_value.nil?
+ 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
- accept = if current_value.respond_to?(:call)
- current_value.call(item)
- else
- Array(current_value).map(&:to_s).include?(value.to_s)
+ # 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?
+
+ 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
+ end
end
- if accept
- html_options[option] = true
- elsif option == :checked
- html_options[option] = false
- 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)
- 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)
+ yield item, value, text, default_html_options.merge(additional_html_options)
+ end.join.html_safe
+ end
- yield item, value, text, default_html_options.merge(additional_html_options)
- end.join.html_safe
- end
+ 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)
- def render_collection_for(builder_class, &block) #:nodoc:
- 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)
+ if block_given?
+ @template_object.capture(builder, &block)
+ else
+ render_component(builder)
+ end
+ end
- if block_given?
- @template_object.capture(builder, &block)
+ # 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
- render_component(builder)
+ rendered_collection
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
+ def hidden_field
+ hidden_name = @html_options[:name] || hidden_field_name
+ @template_object.hidden_field_tag(hidden_name, "", id: nil)
end
- end
- def hidden_field #:nodoc:
- hidden_name = @html_options[:name] || hidden_field_name
- @template_object.hidden_field_tag(hidden_name, "", id: nil)
- end
-
- def hidden_field_name #:nodoc:
- "#{tag_name(false, @options[:index])}"
- 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 21aaf122f8..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,8 +9,9 @@ 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
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 835d1667d7..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:
diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb
index 58ce042f12..c282505e13 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
@@ -225,15 +227,8 @@ module ActionView
#
# pluralize(2, 'Person', locale: :de)
# # => 2 Personen
- def pluralize(count, singular, deprecated_plural = nil, plural: nil, locale: I18n.locale)
- if deprecated_plural
- ActiveSupport::Deprecation.warn("Passing plural as a positional argument " \
- "is deprecated and will be removed in Rails 5.1. Use e.g. " \
- "pluralize(1, 'person', plural: 'people') instead.")
- plural ||= deprecated_plural
- end
-
- 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.to_s =~ /^1(\.0+)?$/
singular
else
plural || singular.pluralize(locale)
@@ -264,15 +259,16 @@ module ActionView
# # => 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#{break_sequence}").strip : line
+ 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.
@@ -357,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)
@@ -426,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
@@ -450,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 152e1b1211..d5b0a9263f 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -1,18 +1,19 @@
-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
- self.debug_missing_translation = true
+ mattr_accessor :debug_missing_translation, default: true
end
# Delegates to <tt>I18n#translate</tt> but also performs three additional
@@ -58,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.
@@ -84,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
@@ -96,23 +98,23 @@ module ActionView
raise e if raise_error
keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
- 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(', ')
+ 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)
+ content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
end
end
alias :t :translate
# Delegates to <tt>I18n.localize</tt> with no additional functionality.
#
- # See http://rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
+ # See https://www.rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
# for more information.
def localize(*args)
I18n.localize(*args)
@@ -121,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
@@ -133,7 +138,7 @@ module ActionView
end
def html_safe_translation_key?(key)
- key.to_s =~ /(\b|_|\.)html$/
+ /(?:_|\b)html\z/.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 11c7daf4da..df83dff681 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,20 +37,20 @@ 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:
- _filtered_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'
+ if referrer && URI(referrer).scheme != "javascript"
referrer
end
end
@@ -105,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
@@ -138,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 %>
@@ -194,9 +200,9 @@ module ActionView
html_options = convert_options_to_data_attributes(options, html_options)
url = url_for(options)
- html_options["href".freeze] ||= url
+ html_options["href"] ||= url
- content_tag("a".freeze, 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
@@ -298,34 +304,34 @@ 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) : ''.freeze.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 = if form_method == 'post'
- request_method = method.empty? ? 'post' : method
+ request_token_tag = if form_method == "post"
+ request_method = method.empty? ? "post" : method
token_tag(nil, form_options: { action: url, method: request_method })
else
- ''.freeze
+ ""
end
html_options = convert_options_to_data_attributes(options, html_options)
- 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)
@@ -334,7 +340,7 @@ module ActionView
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
@@ -481,12 +487,12 @@ module ActionView
option = html_options.delete(item).presence || next
"#{item.dasherize}=#{ERB::Util.url_encode(option)}"
}.compact
- extras = extras.empty? ? ''.freeze : '?' + extras.join('&')
+ extras = extras.empty? ? "" : "?" + extras.join("&")
encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@")
html_options["href"] = "mailto:#{encoded_email_address}#{extras}"
- content_tag("a".freeze, 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+.
@@ -518,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
#
@@ -531,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 " \
@@ -540,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
+ # 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
@@ -559,51 +575,68 @@ 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'.freeze 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'.freeze)
+ 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'.freeze} : {}
+ link_to_remote_options?(options) ? { "data-remote" => "true" } : {}
end
end
def link_to_remote_options?(options)
if options.is_a?(Hash)
- options.delete('remote'.freeze) || 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".freeze && html_options["rel".freeze] !~ /nofollow/
- html_options["rel".freeze] = "#{html_options["rel".freeze]} nofollow".lstrip
+ if method_not_get_method?(method) && html_options["rel"] !~ /nofollow/
+ if html_options["rel"].blank?
+ html_options["rel"] = "nofollow"
+ else
+ html_options["rel"] = "#{html_options["rel"]} nofollow"
+ end
end
- html_options["data-method".freeze] = method
+ html_options["data-method"] = method
+ end
+
+ STRINGIFIED_COMMON_METHODS = {
+ get: "get",
+ delete: "delete",
+ patch: "patch",
+ post: "post",
+ put: "put",
+ }.freeze
+
+ def method_not_get_method?(method)
+ return false unless method
+ (STRINGIFIED_COMMON_METHODS[method] || method.to_s.downcase) != "get"
end
- def token_tag(token=nil, form_options: {})
- if token != false && protect_against_forgery?
+ def token_tag(token = nil, form_options: {})
+ if token != false && defined?(protect_against_forgery?) && protect_against_forgery?
token ||= form_authenticity_token(form_options: form_options)
tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token)
else
- ''.freeze
+ ""
end
end
def method_tag(method)
- tag('input', type: 'hidden', name: '_method', value: method.to_s)
+ 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'}]
+ # # => [{name: 'name', value: 'David'}, {name: 'nationality', value: 'Danish'}]
#
- # to_form_params(country: {name: 'Denmark'})
+ # to_form_params(country: { name: 'Denmark' })
# # => [{name: 'country[name]', value: 'Denmark'}]
#
# to_form_params(countries: ['Denmark', 'Sweden']})
@@ -613,7 +646,13 @@ module ActionView
#
# to_form_params({ name: 'Denmark' }, 'country')
# # => [{name: 'country[name]', value: 'Denmark'}]
- def to_form_params(attribute, namespace = nil) # :nodoc:
+ def to_form_params(attribute, namespace = nil)
+ attribute = if attribute.respond_to?(:permitted?)
+ attribute.to_h
+ else
+ attribute
+ end
+
params = []
case attribute
when Hash
@@ -627,7 +666,7 @@ module ActionView
params.push(*to_form_params(value, array_prefix))
end
else
- params << { name: namespace, value: attribute.to_param }
+ params << { name: namespace.to_s, value: attribute.to_param }
end
params.sort_by { |pair| pair[:name] }
diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb
index a74a5e05f3..08f66bf435 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,9 +279,9 @@ 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"]
+ 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
@@ -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(formats)
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def _layout(lookup_context, 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:
@@ -382,8 +388,8 @@ module ActionView
case name
when String then _normalize_layout(name)
when Proc then name
- when true then Proc.new { |formats| _default_layout(formats, true) }
- when :default then Proc.new { |formats| _default_layout(formats, false) }
+ when true then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, true) }
+ when :default then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, false) }
when false, nil then nil
else
raise ArgumentError,
@@ -400,14 +406,14 @@ module ActionView
#
# ==== Parameters
# * <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)
+ # * <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(formats, require_layout = false)
+ # * <tt>template</tt> - The template object for the default layout (or +nil+)
+ def _default_layout(lookup_context, formats, require_layout = false)
begin
- value = _layout(formats) if action_has_layout?
+ value = _layout(lookup_context, formats) if action_has_layout?
rescue NameError => e
raise e, "Could not render layout: #{e.message}"
end
@@ -421,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 5a29c68214..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,19 +16,28 @@ 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
- alias :render_partial :render_template
def render_collection(event)
- identifier = event.payload[:identifier] || 'templates'
+ identifier = event.payload[:identifier] || "templates"
info do
" Rendered collection of #{from_rails_root(identifier)}" \
- " #{render_count(event.payload)} (#{event.duration.round(1)}ms)"
+ " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
end
end
@@ -42,20 +53,20 @@ module ActionView
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)
+ def render_count(payload) # :doc:
if payload[:cache_hits]
"[#{payload[:cache_hits]} / #{payload[:count]} cache hits]"
else
@@ -63,11 +74,18 @@ module ActionView
end
end
- private
+ 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 = +" Rendering #{from_rails_root(payload[:identifier])}"
message << " within #{from_rails_root(payload[:layout])}" if payload[:layout]
message
end
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index 626c4b8f5e..fd3d025cbf 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -1,7 +1,10 @@
-require 'concurrent/map'
-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 "active_support/deprecation"
+require "action_view/template/resolver"
module ActionView
# = Action View Lookup Context
@@ -13,18 +16,18 @@ module ActionView
# only once during the request, it speeds up all cache accesses.
class LookupContext #:nodoc:
attr_accessor :prefixes, :rendered_format
+ deprecate :rendered_format
+ deprecate :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, &block)
- self.registered_details << name
+ registered_details << name
Accessors::DEFAULT_PROCS[name] = block
- Accessors.send :define_method, :"default_#{name}", &block
+ Accessors.define_method(:"default_#{name}", &block)
Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{name}
@details.fetch(:#{name}, [])
@@ -57,27 +60,36 @@ module ActionView
alias :eql? :equal?
@details_keys = Concurrent::Map.new
+ @digest_cache = Concurrent::Map.new
+
+ def self.digest_cache(details)
+ @digest_cache[details_cache_key(details)] ||= Concurrent::Map.new
+ end
- def self.get(details)
+ def self.details_cache_key(details)
if details[:formats]
details = details.dup
details[:formats] &= Template::Types.symbols
end
- @details_keys[details] ||= new
+ @details_keys[details] ||= Object.new
end
def self.clear
+ ActionView::ViewPaths.all_view_paths.each do |path_set|
+ path_set.each(&:clear_cache)
+ end
+ ActionView::LookupContext.fallbacks.each(&:clear_cache)
+ @view_context_class = nil
@details_keys.clear
+ @digest_cache.clear
end
def self.digest_caches
- @details_keys.values.map(&:digest_cache)
+ @digest_cache.values
end
- attr_reader :digest_cache
-
- def initialize
- @digest_cache = Concurrent::Map.new
+ def self.view_context_class(klass)
+ @view_context_class ||= klass.with_empty_template_cache
end
end
@@ -88,7 +100,7 @@ module ActionView
# Calculate the details key. Remove the handlers from calculation to improve performance
# since the user cannot modify it explicitly.
def details_key #:nodoc:
- @details_key ||= DetailsKey.get(@details) if @cache
+ @details_key ||= DetailsKey.details_cache_key(@details) if @cache
end
# Temporary skip passing the details_key forward.
@@ -99,10 +111,11 @@ module ActionView
@cache = old_value
end
- protected
+ private
- def _set_detail(key, value)
- @details = @details.dup if @details_key
+ def _set_detail(key, value) # :doc:
+ @details = @details.dup if @digest_cache || @details_key
+ @digest_cache = nil
@details_key = nil
@details[key] = value
end
@@ -112,12 +125,6 @@ module ActionView
module ViewPaths
attr_reader :view_paths, :html_fallback_for_js
- # Whenever setting view paths, makes a copy so that we can manipulate them in
- # instance objects as we wish.
- def view_paths=(paths)
- @view_paths = ActionView::PathSet.new(Array(paths))
- end
-
def find(name, prefixes = [], partial = false, keys = [], options = {})
@view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options))
end
@@ -144,32 +151,47 @@ module ActionView
# Adds fallbacks to the view paths. Useful in cases when you are rendering
# a :file.
def with_fallbacks
- added_resolvers = 0
- self.class.fallbacks.each do |resolver|
- next if view_paths.include?(resolver)
- view_paths.push(resolver)
- added_resolvers += 1
+ view_paths = build_view_paths((@view_paths.paths + self.class.fallbacks).uniq)
+
+ if block_given?
+ ActiveSupport::Deprecation.warn <<~eowarn.squish
+ Calling `with_fallbacks` with a block is deprecated. Call methods on
+ the lookup context returned by `with_fallbacks` instead.
+ eowarn
+
+ begin
+ _view_paths = @view_paths
+ @view_paths = view_paths
+ yield
+ ensure
+ @view_paths = _view_paths
+ end
+ else
+ ActionView::LookupContext.new(view_paths, @details, @prefixes)
end
- yield
- ensure
- added_resolvers.times { view_paths.pop }
end
- protected
+ private
+
+ # Whenever setting view paths, makes a copy so that we can manipulate them in
+ # instance objects as we wish.
+ def build_view_paths(paths)
+ ActionView::PathSet.new(Array(paths))
+ end
- def args_for_lookup(name, prefixes, partial, keys, details_options) #: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)
if @cache
- details_key = DetailsKey.get(user_details)
+ details_key = DetailsKey.details_cache_key(user_details)
else
details_key = nil
end
@@ -177,13 +199,13 @@ module ActionView
[user_details, details_key]
end
- def args_for_any(name, prefixes, partial) # :nodoc:
+ def args_for_any(name, prefixes, partial)
name, prefixes = normalize_name(name, prefixes)
details, details_key = detail_args_for_any
[name, prefixes, partial || false, details, details_key]
end
- def detail_args_for_any # :nodoc:
+ def detail_args_for_any
@detail_args_for_any ||= begin
details = {}
@@ -196,7 +218,7 @@ module ActionView
end
if @cache
- [details, DetailsKey.get(details)]
+ [details, DetailsKey.details_cache_key(details)]
else
[details, nil]
end
@@ -206,15 +228,15 @@ module ActionView
# Support legacy foo.erb names even though we now ignore .erb
# as well as incorrectly putting part of the path in the template
# name instead of the prefix.
- def normalize_name(name, prefixes) #:nodoc:
+ def normalize_name(name, prefixes)
prefixes = prefixes.presence
- parts = name.to_s.split('/'.freeze)
+ parts = 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('/'.freeze)
+ parts = parts.join("/")
prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts]
return name, prefixes
@@ -227,16 +249,23 @@ module ActionView
def initialize(view_paths, details = {}, prefixes = [])
@details_key = nil
+ @digest_cache = nil
@cache = true
@prefixes = prefixes
- @rendered_format = nil
@details = initialize_details({}, details)
- self.view_paths = view_paths
+ @view_paths = build_view_paths(view_paths)
end
def digest_cache
- details_key.digest_cache
+ @digest_cache ||= DetailsKey.digest_cache(@details)
+ end
+
+ def with_prepended_formats(formats)
+ details = @details.dup
+ details[:formats] = formats
+
+ self.class.new(@view_paths, details, @prefixes)
end
def initialize_details(target, details)
@@ -251,7 +280,15 @@ module ActionView
# add :html as fallback to :js.
def formats=(values)
if values
- values.concat(default_formats) if values.delete "*/*".freeze
+ values = values.dup
+ values.concat(default_formats) if values.delete "*/*"
+ values.uniq!
+
+ invalid_values = (values - Template::Types.symbols)
+ unless invalid_values.empty?
+ raise ArgumentError, "Invalid formats: #{invalid_values.map(&:inspect).join(", ")}"
+ end
+
if values == [:js]
values << :html
@html_fallback_for_js = true
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 f68d2a77ed..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
#
@@ -69,30 +71,30 @@ module ActionView #:nodoc:
private
- 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)
+ 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
- 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
+ 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 df14ae09f4..8bd526cdf9 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -1,12 +1,18 @@
+# frozen_string_literal: true
+
require "action_view"
require "rails"
module ActionView
# = Action View Railtie
- class Railtie < Rails::Railtie # :nodoc:
+ class Railtie < Rails::Engine # :nodoc:
+ NULL_OPTION = Object.new
+
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 = NULL_OPTION
config.eager_load_namespaces << ActionView
@@ -17,13 +23,48 @@ 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
+ option = app.config.action_view.delete(:finalize_compiled_template_methods)
+
+ if option != NULL_OPTION
+ ActiveSupport::Deprecation.warn "action_view.finalize_compiled_template_methods is deprecated and has no effect"
+ end
+ end
+ end
+
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
@@ -37,16 +78,10 @@ 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
- end
- end
-
initializer "action_view.per_request_digest_cache" do |app|
ActiveSupport.on_load(:action_view) do
- if app.config.consider_all_requests_local
- app.middleware.use ActionView::Digestor::PerRequestDigestCacheExpiry
+ unless ActionView::Resolver.caching?
+ app.executor.to_run ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
end
end
end
@@ -57,6 +92,10 @@ 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/cache_digests.rake"
diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb
index 4a2547b0fb..ee39b6050d 100644
--- a/actionview/lib/action_view/record_identifier.rb
+++ b/actionview/lib/action_view/record_identifier.rb
@@ -1,5 +1,7 @@
-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
@@ -57,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.
#
@@ -92,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.
@@ -102,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 1dddf53df0..475452f1bb 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, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context
+ delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
def initialize(lookup_context)
@lookup_context = lookup_context
@@ -25,29 +27,82 @@ module ActionView
raise NotImplementedError
end
- protected
+ class RenderedCollection # :nodoc:
+ def self.empty(format)
+ EmptyCollection.new format
+ end
- def extract_details(options)
- @lookup_context.registered_details.each_with_object({}) do |key, details|
- value = options[key]
+ attr_reader :rendered_templates
- details[key] = Array(value) if value
+ def initialize(rendered_templates, spacer)
+ @rendered_templates = rendered_templates
+ @spacer = spacer
end
- end
- def instrument(name, **options)
- options[:identifier] ||= (@template && @template.identifier) || @path
+ def body
+ @rendered_templates.map(&:body).join(@spacer.body).html_safe
+ end
- ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
- yield payload
+ def format
+ rendered_templates.first.format
+ end
+
+ class EmptyCollection
+ attr_reader :format
+
+ def initialize(format)
+ @format = format
+ end
+
+ def body; nil; end
end
end
- def prepend_formats(formats)
- formats = Array(formats)
- return if formats.empty? || @lookup_context.html_fallback_for_js
+ class RenderedTemplate # :nodoc:
+ attr_reader :body, :layout, :template
+
+ def initialize(body, layout, template)
+ @body = body
+ @layout = layout
+ @template = template
+ end
+
+ def format
+ template.format
+ end
- @lookup_context.formats = formats | @lookup_context.formats
+ EMPTY_SPACER = Struct.new(:body).new
end
+
+ private
+
+ def extract_details(options) # :doc:
+ @lookup_context.registered_details.each_with_object({}) do |key, details|
+ value = options[key]
+
+ details[key] = Array(value) if value
+ end
+ end
+
+ def instrument(name, **options) # :doc:
+ ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
+ yield payload
+ end
+ 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
+
+ def build_rendered_template(content, template, layout = nil)
+ RenderedTemplate.new content, layout, template
+ end
+
+ def build_rendered_collection(templates, spacer)
+ RenderedCollection.new templates, spacer
+ end
end
end
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index 13b4ec6133..ed8d5cf54e 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 'concurrent/map'
+# 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 %>
@@ -293,257 +295,272 @@ module ActionView
end
def render(context, options, block)
- setup(context, options, block)
- @template = find_partial
+ as = as_variable(options)
+ setup(context, options, as, block)
- @lookup_context.rendered_format ||= begin
- if @template && @template.formats.present?
- @template.formats.first
+ if @path
+ if @has_object || @collection
+ @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
+ @template_keys = retrieve_template_keys(@variable)
else
- formats.first
+ @template_keys = @locals.keys
end
+ template = find_partial(@path, @template_keys)
+ @variable ||= template.variable
+ else
+ if options[:cached]
+ raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
+ end
+ template = nil
end
if @collection
- render_collection
+ render_collection(context, template)
else
- instrument(:partial) do
- render_partial
- end
+ render_partial(context, template)
end
end
private
- 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)
+ def render_collection(view, template)
+ identifier = (template && template.identifier) || @path
+ instrument(:collection, identifier: identifier, count: @collection.size) do |payload|
+ return RenderedCollection.empty(@lookup_context.formats.first) if @collection.blank?
+
+ spacer = if @options.key?(:spacer_template)
+ spacer_template = find_template(@options[:spacer_template], @locals.keys)
+ build_rendered_template(spacer_template.render(view, @locals), spacer_template)
+ else
+ RenderedTemplate::EMPTY_SPACER
+ end
+
+ collection_body = if template
+ cache_collection_render(payload, view, template) do
+ collection_with_template(view, template)
+ end
+ else
+ collection_without_template(view)
+ end
+ build_rendered_collection(collection_body, spacer)
end
-
- cache_collection_render(payload) do
- @template ? collection_with_template : collection_without_template
- end.join(spacer).html_safe
end
- end
- def render_partial
- view, locals, block = @view, @locals, @block
- object, as = @object, @variable
+ def render_partial(view, template)
+ instrument(:partial, identifier: template.identifier) do |payload|
+ locals, block = @locals, @block
+ object, as = @object, @variable
- if !block && (layout = @options[:layout])
- 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] if object.nil? # Respect object when object is false
+ locals[as] = object if @has_object
- content = @template.render(view, locals) do |*name|
- view._layout_for(*name, &block)
- end
-
- content = layout.render(view, locals){ content } if layout
- content
- end
+ content = template.render(view, locals) do |*name|
+ view._layout_for(*name, &block)
+ 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
+ content = layout.render(view, locals) { content } if layout
+ payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
+ build_rendered_template(content, template, layout)
+ end
+ end
- if @collection
- paths = @collection_data = @collection.map { |o| partial_path(o) }
- @path = paths.uniq.one? ? paths.first : nil
+ # 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, as, block)
+ @options = options
+ @block = block
+
+ @locals = options[:locals] || {}
+ @details = extract_details(options)
+
+ partial = options[:partial]
+
+ if String === partial
+ @has_object = options.key?(:object)
+ @object = options[:object]
+ @collection = collection_from_options
+ @path = partial
else
- @path = partial_path
+ @has_object = true
+ @object = partial
+ @collection = collection_from_object || collection_from_options
+
+ if @collection
+ paths = @collection_data = @collection.map { |o| partial_path(o, context) }
+ if paths.uniq.length == 1
+ @path = paths.first
+ else
+ paths.map! { |path| retrieve_variable(path, as).unshift(path) }
+ @path = nil
+ end
+ else
+ @path = partial_path(@object, context)
+ end
end
- end
- if as = options[:as]
- raise_invalid_option_as(as) unless as.to_s =~ /\A[a-z_]\w*\z/
- as = as.to_sym
+ self
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 as_variable(options)
+ if as = options[:as]
+ raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
+ as.to_sym
+ end
end
- self
- end
-
- def collection_from_options
- if @options.key?(:collection)
- collection = @options[:collection]
- collection.respond_to?(:to_ary) ? collection.to_ary : []
+ def collection_from_options
+ if @options.key?(:collection)
+ collection = @options[:collection]
+ collection ? collection.to_a : []
+ end
end
- end
- def collection_from_object
- @object.to_ary if @object.respond_to?(:to_ary)
- end
+ def collection_from_object
+ @object.to_ary if @object.respond_to?(:to_ary)
+ end
- def find_partial
- find_template(@path, @template_keys) if @path
- end
+ def find_partial(path, template_keys)
+ find_template(path, template_keys)
+ end
- def find_template(path, locals)
- prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
- @lookup_context.find_template(path, prefixes, true, locals, @details)
- end
+ def find_template(path, locals)
+ prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
+ @lookup_context.find_template(path, prefixes, true, locals, @details)
+ end
- def collection_with_template
- view, locals, template = @view, @locals, @template
- as, counter, iteration = @variable, @variable_counter, @variable_iteration
+ def collection_with_template(view, template)
+ locals = @locals
+ as, counter, iteration = @variable, @variable_counter, @variable_iteration
- if layout = @options[:layout]
- layout = find_template(layout, @template_keys)
- end
+ if layout = @options[:layout]
+ layout = find_template(layout, @template_keys)
+ end
- partial_iteration = PartialIteration.new(@collection.size)
- locals[iteration] = partial_iteration
+ partial_iteration = PartialIteration.new(@collection.size)
+ locals[iteration] = partial_iteration
- @collection.map do |object|
- locals[as] = object
- locals[counter] = partial_iteration.index
+ @collection.map do |object|
+ locals[as] = object
+ locals[counter] = partial_iteration.index
- content = template.render(view, locals)
- content = layout.render(view, locals) { content } if layout
- partial_iteration.iterate!
- content
+ content = template.render(view, locals)
+ content = layout.render(view, locals) { content } if layout
+ partial_iteration.iterate!
+ build_rendered_template(content, template, layout)
+ end
end
- end
- def collection_without_template
- view, locals, collection_data = @view, @locals, @collection_data
- cache = {}
- keys = @locals.keys
+ def collection_without_template(view)
+ locals, collection_data = @locals, @collection_data
+ cache = {}
+ keys = @locals.keys
- partial_iteration = PartialIteration.new(@collection.size)
+ partial_iteration = PartialIteration.new(@collection.size)
- @collection.map do |object|
- index = partial_iteration.index
- path, as, counter, iteration = collection_data[index]
+ @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
+ 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!
+ build_rendered_template(content, template)
+ 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, view)
+ 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(variable)
+ keys = @locals.keys
+ keys << variable
+ if @collection
+ keys << @variable_counter
+ keys << @variable_iteration
+ end
+ keys
end
- keys
- end
- def retrieve_variable(path, as)
- variable = as || begin
- base = path[-1] == "/".freeze ? "".freeze : File.basename(path)
- raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
- $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 f7deba94ce..ed59033e27 100644
--- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActionView
module CollectionCaching # :nodoc:
extend ActiveSupport::Concern
@@ -5,42 +7,87 @@ 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(instrumentation_payload)
+ def cache_collection_render(instrumentation_payload, view, template)
return yield unless @options[:cached]
- keyed_collection = collection_by_cache_keys
+ # 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(view, template)
+
+ # 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
+ # 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
+
+ # 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
index = 0
- fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do
+ fetch_or_cache_partial(cached_partials, template, 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 collection_by_cache_keys
+ def callable_cache_key?
+ @options[:cached].respond_to?(:call)
+ end
+
+ def collection_by_cache_keys(view, template)
+ seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
+
+ digest_path = view.digest_path_from_template(template)
+
@collection.each_with_object({}) do |item, hash|
- hash[expanded_cache_key(item)] = item
+ hash[expanded_cache_key(seed.call(item), view, template, digest_path)] = item
end
end
- def expanded_cache_key(key)
- key = @view.fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path))
+ def expanded_cache_key(key, view, template, digest_path)
+ key = view.combined_fragment_cache_key(view.cache_fragment_name(key, virtual_path: template.virtual_path, digest_path: digest_path))
key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
end
- def fetch_or_cache_partial(cached_partials, order_by:)
+ # `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, template, order_by:)
order_by.map do |cache_key|
- cached_partials.fetch(cache_key) do
+ if content = cached_partials[cache_key]
+ build_rendered_template(content, template)
+ else
yield.tap do |rendered_partial|
- collection_cache.write(cache_key, rendered_partial)
+ collection_cache.write(cache_key, rendered_partial.body)
end
end
end
diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb
index 2a3b89aebf..485eb1a5b4 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
@@ -17,10 +19,14 @@ module ActionView
# Main render entry point shared by Action View and Action Controller.
def render(context, options)
+ render_to_object(context, options).body
+ end
+
+ def render_to_object(context, options) # :nodoc:
if options.key?(:partial)
- render_partial(context, options)
+ render_partial_to_object(context, options)
else
- render_template(context, options)
+ render_template_to_object(context, options)
end
end
@@ -39,11 +45,23 @@ module ActionView
# Direct access to template rendering.
def render_template(context, options) #:nodoc:
- TemplateRenderer.new(@lookup_context).render(context, options)
+ render_template_to_object(context, options).body
end
# Direct access to partial rendering.
def render_partial(context, options, &block) #:nodoc:
+ render_partial_to_object(context, options, &block).body
+ end
+
+ def cache_hits # :nodoc:
+ @cache_hits ||= {}
+ end
+
+ def render_template_to_object(context, options) #:nodoc:
+ TemplateRenderer.new(@lookup_context).render(context, options)
+ end
+
+ def render_partial_to_object(context, options, &block) #:nodoc:
PartialRenderer.new(@lookup_context).render(context, options, block)
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 f38e2764d0..19fa399e2c 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,77 +28,78 @@ 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.annotated_source_code.to_s if exception.respond_to?(:annotated_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
# object that responds to each. This object is initialized with a block
# that knows how to render the template.
- def render_template(template, layout_name = nil, locals = {}) #:nodoc:
- return [super] unless layout_name && template.supports_streaming?
+ def render_template(view, template, layout_name = nil, locals = {}) #:nodoc:
+ return [super.body] unless layout_name && template.supports_streaming?
locals ||= {}
layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
Body.new do |buffer|
- delayed_render(buffer, template, layout, @view, locals)
+ delayed_render(buffer, template, layout, view, locals)
end
end
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 9d15bbfca7..1faef9ca81 100644
--- a/actionview/lib/action_view/renderer/template_renderer.rb
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -1,102 +1,108 @@
-require 'active_support/core_ext/object/try'
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/try"
module ActionView
class TemplateRenderer < AbstractRenderer #:nodoc:
def render(context, options)
- @view = context
@details = extract_details(options)
template = determine_template(options)
- prepend_formats(template.formats)
-
- @lookup_context.rendered_format ||= (template.formats.first || formats.first)
+ prepend_formats(template.format)
- render_template(template, options[:layout], options[:locals])
+ render_template(context, template, options[:layout], options[:locals] || {})
end
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_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]
+ 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)
+ if File.exist?(options[:file])
+ Template::RawFile.new(options[:file])
+ else
+ ActiveSupport::Deprecation.warn "render file: should be given the absolute path to a file"
+ @lookup_context.with_fallbacks.find_file(options[:file], nil, false, keys, @details)
+ end
+ elsif options.key?(:inline)
+ handler = Template.handler_for_extension(options[:type] || "erb")
+ format = if handler.respond_to?(:default_format)
+ handler.default_format
+ else
+ @lookup_context.formats.first
+ end
+ Template::Inline.new(options[:inline], "inline template", handler, locals: keys, format: format)
+ elsif options.key?(:template)
+ if options[:template].respond_to?(:render)
+ options[:template]
+ else
+ @lookup_context.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 || {}
- 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) }
+ # Renders the given template. A string representing the layout can be
+ # supplied as well.
+ def render_template(view, template, layout_name, locals)
+ render_with_layout(view, template, 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, [formats.first])
- content = yield(layout)
+ def render_with_layout(view, template, 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
+ body = if layout
+ view.view_flow.set(:layout, content)
+ layout.render(view, locals) { |*name| view._layout_for(*name) }
+ else
+ content
+ end
+ build_rendered_template(body, template, layout)
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, formats)
- resolve_layout(layout, keys, formats)
- 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, formats)
- details = @details.dup
- details[:formats] = formats
+ def resolve_layout(layout, keys, formats)
+ details = @details.dup
+ details[:formats] = formats
- case layout
- when String
- begin
- if layout =~ /^\//
- with_fallbacks { find_template(layout, nil, false, keys, details) }
- else
- find_template(layout, nil, false, keys, details)
+ case layout
+ when String
+ begin
+ if layout.start_with?("/")
+ ActiveSupport::Deprecation.warn "Rendering layouts from an absolute path is deprecated."
+ @lookup_context.with_fallbacks.find_template(layout, nil, false, [], details)
+ else
+ @lookup_context.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(@lookup_context, formats), keys, formats)
+ else
+ layout
end
- when Proc
- resolve_layout(layout.call(formats), keys, formats)
- else
- layout
end
- end
end
end
diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb
index 3ca7f9d220..5a06bd9da6 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
@@ -24,6 +26,13 @@ module ActionView
extend ActiveSupport::Concern
include ActionView::ViewPaths
+ attr_reader :rendered_format
+
+ def initialize
+ @rendered_format = nil
+ super
+ end
+
# Overwrite process to setup I18n proxy.
def process(*) #:nodoc:
old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
@@ -33,48 +42,59 @@ module ActionView
end
module ClassMethods
- def view_context_class
- @view_context_class ||= begin
- supports_path = supports_path?
- routes = respond_to?(:_routes) && _routes
- helpers = respond_to?(:_helpers) && _helpers
-
- Class.new(ActionView::Base) do
- if routes
- include routes.url_helpers(supports_path)
- include routes.mounted_helpers
- end
-
- if helpers
- include helpers
- end
+ def _routes
+ end
+
+ def _helpers
+ end
+
+ def build_view_context_class(klass, supports_path, routes, helpers)
+ Class.new(klass) do
+ if routes
+ include routes.url_helpers(supports_path)
+ include routes.mounted_helpers
+ end
+
+ if helpers
+ include helpers
end
end
end
- end
- attr_internal_writer :view_context_class
+ def view_context_class
+ klass = ActionView::LookupContext::DetailsKey.view_context_class(ActionView::Base)
+
+ @view_context_class ||= build_view_context_class(klass, supports_path?, _routes, _helpers)
+
+ if klass.changed?(@view_context_class)
+ @view_context_class = build_view_context_class(klass, supports_path?, _routes, _helpers)
+ end
+
+ @view_context_class
+ end
+ end
def view_context_class
- @_view_context_class ||= self.class.view_context_class
+ self.class.view_context_class
end
# An instance of a view class. The default view class is ActionView::Base.
#
# The view class must have the following methods:
- # View.new[lookup_context, assigns, controller]
- # Create a new ActionView instance for a controller and we can also pass the arguments.
- # View#render(option)
- # Returns String with the rendered template
+ #
+ # * <tt>View.new(lookup_context, assigns, controller)</tt> — Create a new
+ # ActionView instance for a controller and we can also pass the arguments.
+ #
+ # * <tt>View#render(option)</tt> — Returns String with the rendered template.
#
# Override this method in a module to change the default behavior.
def view_context
- view_context_class.new(view_renderer, view_assigns, self)
+ view_context_class.new(lookup_context, view_assigns, self)
end
# Returns an object that is able to render templates.
- # :api: private
- def view_renderer
+ def view_renderer # :nodoc:
+ # Lifespan: Per controller
@_view_renderer ||= ActionView::Renderer.new(lookup_context)
end
@@ -83,37 +103,36 @@ module ActionView
_render_template(options)
end
- def 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
context.assign assigns if assigns
- lookup_context.rendered_format = nil if options[:formats]
lookup_context.variants = variant if variant
- view_renderer.render(context, options)
+ rendered_template = context.in_rendering_context(options) do |renderer|
+ renderer.render_to_object(context, options)
+ end
+
+ rendered_format = rendered_template.format || lookup_context.formats.first
+ @rendered_format = Template::Types[rendered_format]
+
+ rendered_template.body
end
# Assign the rendered format to look up context.
- def _process_format(format) #:nodoc:
+ def _process_format(format)
super
- lookup_context.formats = [format.to_sym]
- lookup_context.rendered_format = lookup_context.formats.first
+ lookup_context.formats = [format.to_sym] if format.to_sym
end
# 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 +143,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 45e78d1ad9..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
@@ -83,25 +84,24 @@ module ActionView
super(only_path: _generate_paths_by_default)
when Hash
options = options.symbolize_keys
- unless options.key?(:only_path)
- options[:only_path] = only_path?(options[:host])
- end
+ ensure_only_path_option(options)
super(options)
when ActionController::Parameters
- unless options.key?(:only_path)
- options[:only_path] = only_path?(options[:host])
- end
+ ensure_only_path_option(options)
super(options)
when :back
_back_url
when Array
components = options.dup
- if _generate_paths_by_default
- polymorphic_path(components, components.extract_options!)
+ options = components.extract_options!
+ ensure_only_path_option(options)
+
+ if options[:only_path]
+ polymorphic_path(components, options)
else
- polymorphic_url(components, components.extract_options!)
+ polymorphic_url(components, options)
end
else
method = _generate_paths_by_default ? :path : :url
@@ -123,25 +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 only_path?(host)
- _generate_paths_by_default unless host
+ 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
index 045bdf5691..dd8e94bd88 100644
--- a/actionview/lib/action_view/tasks/cache_digests.rake
+++ b/actionview/lib/action_view/tasks/cache_digests.rake
@@ -1,19 +1,21 @@
+# 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?
+ 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?
+ 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
+ ENV["TEMPLATE"].split(".", 2).first
end
def self.finder
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 169ee55fdc..94f8a194a0 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -1,12 +1,24 @@
-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 "active_support/deprecation"
+require "thread"
+require "delegate"
module ActionView
# = Action View Template
class Template
extend ActiveSupport::Autoload
+ def self.finalize_compiled_template_methods
+ ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods is deprecated and has no effect"
+ end
+
+ def self.finalize_compiled_template_methods=(_)
+ ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods= is deprecated and has no effect"
+ end
+
# === Encodings in ActionView::Template
#
# ActionView::Template is one of a few sources of potential
@@ -65,8 +77,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
@@ -102,46 +113,60 @@ module ActionView
eager_autoload do
autoload :Error
+ autoload :RawFile
autoload :Handlers
autoload :HTML
+ autoload :Inline
autoload :Text
autoload :Types
end
extend Template::Handlers
- attr_accessor :locals, :formats, :variants, :virtual_path
-
attr_reader :source, :identifier, :handler, :original_encoding, :updated_at
+ attr_reader :variable, :format, :variant, :locals, :virtual_path
- # This finalizer is needed (and exactly with a proc inside another proc)
- # otherwise templates leak in development.
- Finalizer = proc do |method_name, mod| # :nodoc:
- proc do
- mod.module_eval do
- remove_possible_method method_name
- end
+ def initialize(source, identifier, handler, format: nil, variant: nil, locals: nil, virtual_path: nil, updated_at: nil)
+ unless locals
+ ActiveSupport::Deprecation.warn "ActionView::Template#initialize requires a locals parameter"
+ locals = []
end
- end
-
- def initialize(source, identifier, handler, details)
- format = details[:format] || (handler.default_format if handler.respond_to?(:default_format))
@source = source
@identifier = identifier
@handler = handler
@compiled = false
- @original_encoding = nil
- @locals = details[:locals] || []
- @virtual_path = details[:virtual_path]
- @updated_at = details[:updated_at] || Time.now
- @formats = Array(format).map { |f| f.respond_to?(:ref) ? f.ref : f }
- @variants = [details[:variant]]
+ @locals = locals
+ @virtual_path = virtual_path
+
+ @variable = if @virtual_path
+ base = @virtual_path[-1] == "/" ? "" : ::File.basename(@virtual_path)
+ base =~ /\A_?(.*?)(?:\.\w+)*\z/
+ $1.to_sym
+ end
+
+ if updated_at
+ ActiveSupport::Deprecation.warn "ActionView::Template#updated_at is deprecated"
+ @updated_at = updated_at
+ else
+ @updated_at = Time.now
+ end
+ @format = format
+ @variant = variant
@compile_mutex = Mutex.new
end
+ deprecate :original_encoding
+ deprecate :updated_at
+ deprecate def virtual_path=(_); end
+ deprecate def locals=(_); end
+ deprecate def formats=(_); end
+ deprecate def formats; Array(format); end
+ deprecate def variants=(_); end
+ deprecate def variants; [variant]; end
+
# Returns whether the underlying handler supports streaming. If so,
- # a streaming buffer *may* be passed when it start rendering.
+ # a streaming buffer *may* be passed when it starts rendering.
def supports_streaming?
handler.respond_to?(:supports_streaming?) && handler.supports_streaming?
end
@@ -152,17 +177,17 @@ 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".freeze) do
+ def render(view, locals, buffer = ActionView::OutputBuffer.new, &block)
+ instrument_render_template do
compile!(view)
- view.send(method_name, locals, buffer, &block)
+ view._run(method_name, self, locals, buffer, &block)
end
rescue => e
handle_render_error(view, e)
end
def type
- @type ||= Types[@formats.first] if @formats.first
+ @type ||= Types[format]
end
# Receives a view object and return a template similar to self by using @virtual_path.
@@ -180,12 +205,16 @@ 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 short_identifier
+ @short_identifier ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", "") : identifier
+ end
+
def inspect
- @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", ''.freeze) : identifier
+ "#<#{self.class.name} #{short_identifier} locals=#{@locals.inspect}>"
end
# This method is responsible for properly setting the encoding of the
@@ -199,12 +228,14 @@ module ActionView
# before passing the source on to the template engine, leaving a
# blank line in its stead.
def encode!
- return unless source.encoding == Encoding::BINARY
+ source = self.source
+
+ return source unless source.encoding == Encoding::BINARY
# Look for # encoding: *. If we find one, we'll encode the
# String in that encoding, otherwise, we'll use the
# default external encoding.
- if source.sub!(/\A#{ENCODING_FLAG}/, '')
+ if source.sub!(/\A#{ENCODING_FLAG}/, "")
encoding = magic_encoding = $1
else
encoding = Encoding.default_external
@@ -232,11 +263,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, @locals, @virtual_path, @updated_at, @format, @variant ]
+ end
+
+ def marshal_load(array) # :nodoc:
+ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant = *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
@@ -248,11 +292,7 @@ module ActionView
# re-compilation
return if @compiled
- if view.is_a?(ActionView::CompiledTemplates)
- mod = ActionView::CompiledTemplates
- else
- mod = view.singleton_class
- end
+ mod = view.compiled_method_container
instrument("!compile_template") do
compile(mod)
@@ -265,6 +305,15 @@ module ActionView
end
end
+ class LegacyTemplate < DelegateClass(Template) # :nodoc:
+ attr_reader :source
+
+ def initialize(template, source)
+ super(template)
+ @source = source
+ end
+ end
+
# Among other things, this method is responsible for properly setting
# the encoding of the compiled template.
#
@@ -277,18 +326,16 @@ 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:
- encode!
- method_name = self.method_name
- code = @handler.call(self)
+ def compile(mod)
+ source = encode!
+ code = @handler.call(self, source)
# Make sure that the resulting String to be eval'd is in the
# encoding of the code
- source = <<-end_src
+ original_source = source
+ 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
- @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
+ @virtual_path = #{@virtual_path.inspect};#{locals_code};#{code}
end
end_src
@@ -303,14 +350,20 @@ module ActionView
# handler is valid in the default_internal. This is for handlers
# that handle encoding but screw up
unless source.valid_encoding?
- raise WrongEncodingError.new(@source, Encoding.default_internal)
+ raise WrongEncodingError.new(source, Encoding.default_internal)
end
- mod.module_eval(source, identifier, 0)
- ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
+ begin
+ mod.module_eval(source, identifier, 0)
+ rescue SyntaxError
+ # Account for when code in the template is not syntactically valid; e.g. if we're using
+ # ERB and the user writes <%= foo( %>, attempting to call a helper `foo` and interpolate
+ # the result into the template, but missing an end parenthesis.
+ raise SyntaxErrorInTemplate.new(self, original_source)
+ 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
@@ -324,31 +377,38 @@ module ActionView
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!('-'.freeze, '_'.freeze)
+ m = +"_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
+ m.tr!("-", "_")
m
end
end
- def identifier_method_name #:nodoc:
- inspect.tr('^a-z_'.freeze, '_'.freeze)
+ def identifier_method_name
+ short_identifier.tr("^a-z_", "_")
end
- def instrument(action, &block)
- payload = { virtual_path: @virtual_path, identifier: @identifier }
- case action
- when "!render_template".freeze
- ActiveSupport::Notifications.instrument("!render_template.action_view".freeze, payload, &block)
- else
- ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, payload, &block)
- end
+ def instrument(action, &block) # :doc:
+ ActiveSupport::Notifications.instrument("#{action}.action_view", instrument_payload, &block)
+ end
+
+ def instrument_render_template(&block)
+ ActiveSupport::Notifications.instrument("!render_template.action_view", instrument_payload, &block)
+ end
+
+ 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 3f38c3d2b9..d0ea03e228 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?
@@ -62,23 +61,13 @@ module ActionView
# Override to prevent #cause resetting during re-raise.
attr_reader :cause
- def initialize(template, original_exception = nil)
- if original_exception
- ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \
- "Exceptions will automatically capture the original exception.", caller)
- end
-
+ def initialize(template)
super($!.message)
set_backtrace($!.backtrace)
@cause = $!
@template, @sub_templates = template, nil
end
- def original_exception
- ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller)
- cause
- end
-
def file_name
@template.identifier
end
@@ -120,7 +109,7 @@ module ActionView
end
end
- def annoted_source_code
+ def annotated_source_code
source_extract(4)
end
@@ -130,7 +119,7 @@ module ActionView
if line_number
"on line ##{line_number} of "
else
- 'in '
+ "in "
end + file_name
end
@@ -149,4 +138,24 @@ module ActionView
end
TemplateError = Template::Error
+
+ class SyntaxErrorInTemplate < TemplateError #:nodoc
+ def initialize(template, offending_code_string)
+ @offending_code_string = offending_code_string
+ super(template)
+ end
+
+ def message
+ <<~MESSAGE
+ Encountered a syntax error while rendering template: check #{@offending_code_string}
+ MESSAGE
+ end
+
+ def annotated_source_code
+ @offending_code_string.split("\n").map.with_index(1) { |line, index|
+ indentation = " " * 4
+ "#{index}:#{indentation}#{line}"
+ }
+ end
+ end
end
diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb
index ad4c353608..ddaac7a100 100644
--- a/actionview/lib/action_view/template/handlers.rb
+++ b/actionview/lib/action_view/template/handlers.rb
@@ -1,18 +1,22 @@
+# frozen_string_literal: true
+
+require "active_support/deprecation"
+
module ActionView #:nodoc:
# = Action View Template Handlers
- class Template
+ class Template #:nodoc:
module Handlers #:nodoc:
- 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'
+ 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
+ base.register_template_handler :ruby, lambda { |_, source| source }
end
@@template_handlers = {}
@@ -22,11 +26,35 @@ module ActionView #:nodoc:
@@template_extensions ||= @@template_handlers.keys
end
+ class LegacyHandlerWrapper < SimpleDelegator # :nodoc:
+ def call(view, source)
+ __getobj__.call(ActionView::Template::LegacyTemplate.new(view, source))
+ end
+ end
+
# Register an object that knows how to handle template files with the given
# extensions. This can be used to implement new template types.
# The handler must respond to +:call+, which will be passed the template
# and should return the rendered template as a String.
def register_template_handler(*extensions, handler)
+ params = if handler.is_a?(Proc)
+ handler.parameters
+ else
+ handler.method(:call).parameters
+ end
+
+ unless params.find_all { |type, _| type == :req || type == :opt }.length >= 2
+ ActiveSupport::Deprecation.warn <<~eowarn
+ Single arity template handlers are deprecated. Template handlers must
+ now accept two parameters, the view object and the source for the view object.
+ Change:
+ >> #{handler.class}#call(#{params.map(&:last).join(", ")})
+ To:
+ >> #{handler.class}#call(#{params.map(&:last).join(", ")}, source)
+ eowarn
+ handler = LegacyHandlerWrapper.new(handler)
+ end
+
raise(ArgumentError, "Extension is required") if extensions.empty?
extensions.each do |extension|
@@template_handlers[extension.to_sym] = handler
diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb
index d90b0c6378..e5413cd2a0 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)
+ def call(template, source)
require_engine
- "xml = ::Builder::XmlMarkup.new(:indent => 2);" +
+ "xml = ::Builder::XmlMarkup.new(:indent => 2);" \
"self.output_buffer = xml.target!;" +
- template.source +
+ 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 85a100ed4c..b6314a5bc3 100644
--- a/actionview/lib/action_view/template/handlers/erb.rb
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -1,96 +1,35 @@
-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.alias_method :escape_whitelist, :escape_ignore_list
+ base.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]*")
- def self.call(template)
- new.call(template)
+ def self.call(template, source)
+ new.call(template, source)
end
def supports_streaming?
@@ -101,25 +40,25 @@ module ActionView
true
end
- def call(template)
+ def call(template, source)
# First, convert to BINARY, so in case the encoding is
# wrong, we can still find an encoding tag
# (<%# encoding %>) inside the String using a regular
# expression
- template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT)
+ template_source = source.dup.force_encoding(Encoding::ASCII_8BIT)
- erb = template_source.gsub(ENCODING_TAG, '')
+ erb = template_source.gsub(ENCODING_TAG, "")
encoding = $2
- erb.force_encoding valid_encoding(template.source.dup, encoding)
+ erb.force_encoding valid_encoding(source.dup, encoding)
# Always make sure we return a String in the default_internal
erb.encode!
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
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..307b852440
--- /dev/null
+++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb
@@ -0,0 +1,87 @@
+# 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] = ""
+ properties[:postamble] = "@output_buffer.to_s"
+ properties[:bufvar] = "@output_buffer"
+ properties[:escapefunc] = ""
+
+ super
+ end
+
+ def evaluate(action_view_erb_handler_context)
+ src = @src
+ view = Class.new(ActionView::Base) {
+ include action_view_erb_handler_context._routes.url_helpers
+ class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", @filename || "(erubi)", 0)
+ }.empty
+ view._run(:_template, nil, {}, ActionView::OutputBuffer.new)
+ 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
index ccaa8d1469..65857d8587 100644
--- a/actionview/lib/action_view/template/handlers/html.rb
+++ b/actionview/lib/action_view/template/handlers/html.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module ActionView
module Template::Handlers
class Html < Raw
- def call(template)
+ def call(template, source)
"ActionView::OutputBuffer.new #{super}"
end
end
diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb
index 760f517431..57ebb169fc 100644
--- a/actionview/lib/action_view/template/handlers/raw.rb
+++ b/actionview/lib/action_view/template/handlers/raw.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
module ActionView
module Template::Handlers
class Raw
- def call(template)
- "#{template.source.inspect};"
+ def call(template, source)
+ "#{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..ecd1c31e79 100644
--- a/actionview/lib/action_view/template/html.rb
+++ b/actionview/lib/action_view/template/html.rb
@@ -1,22 +1,28 @@
+# frozen_string_literal: true
+
+require "active_support/deprecation"
+
module ActionView #:nodoc:
# = Action View HTML Template
- class Template
+ class Template #:nodoc:
class HTML #:nodoc:
- attr_accessor :type
+ attr_reader :type
def initialize(string, type = nil)
+ unless type
+ ActiveSupport::Deprecation.warn "ActionView::Template::HTML#initialize requires a type parameter"
+ type = :html
+ end
+
@string = string.to_s
- @type = Types[type] || type if type
- @type ||= Types[:html]
+ @type = type
end
def identifier
- 'html template'
+ "html template"
end
- def inspect
- 'html template'
- end
+ alias_method :inspect, :identifier
def to_str
ERB::Util.h(@string)
@@ -26,9 +32,12 @@ module ActionView #:nodoc:
to_str
end
- def formats
- [@type.respond_to?(:ref) ? @type.ref : @type.to_s]
+ def format
+ @type
end
+
+ def formats; Array(format); end
+ deprecate :formats
end
end
end
diff --git a/actionview/lib/action_view/template/inline.rb b/actionview/lib/action_view/template/inline.rb
new file mode 100644
index 0000000000..44658487ea
--- /dev/null
+++ b/actionview/lib/action_view/template/inline.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module ActionView #:nodoc:
+ class Template #:nodoc:
+ class Inline < Template #:nodoc:
+ # This finalizer is needed (and exactly with a proc inside another proc)
+ # otherwise templates leak in development.
+ Finalizer = proc do |method_name, mod| # :nodoc:
+ proc do
+ mod.module_eval do
+ remove_possible_method method_name
+ end
+ end
+ end
+
+ def compile(mod)
+ super
+ ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/raw_file.rb b/actionview/lib/action_view/template/raw_file.rb
new file mode 100644
index 0000000000..623351305f
--- /dev/null
+++ b/actionview/lib/action_view/template/raw_file.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module ActionView #:nodoc:
+ # = Action View RawFile Template
+ class Template #:nodoc:
+ class RawFile #:nodoc:
+ attr_accessor :type, :format
+
+ def initialize(filename)
+ @filename = filename.to_s
+ extname = ::File.extname(filename).delete(".")
+ @type = Template::Types[extname] || Template::Types[:text]
+ @format = @type.symbol
+ end
+
+ def identifier
+ @filename
+ end
+
+ def render(*args)
+ ::File.read(@filename)
+ end
+
+ def formats; Array(format); end
+ deprecate :formats
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index f33acc2103..095e6cc3a1 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "pathname"
require "active_support/core_ext/class"
require "active_support/core_ext/module/attribute_accessors"
@@ -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
@@ -37,15 +39,15 @@ module ActionView
class Cache #:nodoc:
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
@@ -55,28 +57,17 @@ module ActionView
@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
def cache(key, name, prefix, partial, locals)
- if Resolver.caching?
- @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
- else
- fresh_templates = yield
- cached_templates = @data[key][name][prefix][partial][locals]
-
- if templates_have_changed?(cached_templates, fresh_templates)
- @data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates)
- else
- cached_templates || NO_TEMPLATES
- end
- end
+ @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
end
def cache_query(query) # :nodoc:
- if Resolver.caching?
- @query_cache[query] ||= canonical_no_templates(yield)
- else
- yield
- end
+ @query_cache[query] ||= canonical_no_templates(yield)
end
def clear
@@ -84,28 +75,31 @@ module ActionView
@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?
- end
-
- cached_templates_max_updated_at = cached_templates.map(&:updated_at).max
+ private
- # 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
+ def canonical_no_templates(templates)
+ templates.empty? ? NO_TEMPLATES : templates
+ end
end
- cattr_accessor :caching
- self.caching = true
+ cattr_accessor :caching, default: true
class << self
alias :caching? :caching
@@ -120,15 +114,19 @@ 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 = [])
+ locals = locals.map(&:to_s).sort!.freeze
+
cached(key, [name, prefix, partial], details, locals) do
- find_templates(name, prefix, partial, details)
+ find_templates(name, prefix, partial, details, false, locals)
end
end
- def find_all_anywhere(name, prefix, partial=false, details={}, key=nil, locals=[])
+ def find_all_anywhere(name, prefix, partial = false, details = {}, key = nil, locals = [])
+ locals = locals.map(&:to_s).sort!.freeze
+
cached(key, [name, prefix, partial], details, locals) do
- find_templates(name, prefix, partial, details, true)
+ find_templates(name, prefix, partial, details, true, locals)
end
end
@@ -143,185 +141,148 @@ 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"
- end
-
- # Helpers that builds a path. Useful for building virtual paths.
- def build_path(name, prefix, partial)
- Path.build(name, prefix, partial)
+ def find_templates(name, prefix, partial, details, outside_app_allowed = false, locals = [])
+ raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, outside_app_allowed = false, locals = []) method"
end
# Handles templates caching. If a key is given and caching is on
# 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!
if key
@cache.cache(key, name, prefix, partial, locals) do
- decorate(yield, path_info, details, locals)
+ yield
end
else
- decorate(yield, path_info, details, locals)
- end
- end
-
- # Ensures all the resolver information is set in the template.
- def decorate(templates, path_info, details, locals) #:nodoc:
- cached = nil
- templates.each do |t|
- t.locals = locals
- t.formats = details[:formats] || [:html] if t.formats.empty?
- t.variants = details[:variants] || [] if t.variants.empty?
- t.virtual_path ||= (cached ||= build_path(*path_info))
+ yield
end
end
end
# 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)
- @pattern = pattern || DEFAULT_PATTERN
+ def initialize(pattern = nil)
+ if pattern
+ ActiveSupport::Deprecation.warn "Specifying a custom path for #{self.class} is deprecated. Implement a custom Resolver subclass instead."
+ @pattern = pattern
+ else
+ @pattern = DEFAULT_PATTERN
+ end
super()
end
private
- 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 find_templates(name, prefix, partial, details, outside_app_allowed = false, locals)
+ path = Path.build(name, prefix, partial)
+ query(path, details, details[:formats], outside_app_allowed, locals)
+ end
- def query(path, details, formats, outside_app_allowed)
- query = build_query(path, details)
+ def query(path, details, formats, outside_app_allowed, locals)
+ template_paths = find_template_paths_from_details(path, details)
+ template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
- template_paths = find_template_paths(query)
- template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
+ template_paths.map do |template|
+ build_template(template, path.virtual, locals)
+ end
+ end
- template_paths.map do |template|
- handler, format, variant = extract_handler_and_format_and_variant(template, formats)
- contents = File.binread(template)
+ def build_template(template, virtual_path, locals)
+ handler, format, variant = extract_handler_and_format_and_variant(template)
- Template.new(contents, File.expand_path(template), handler,
- :virtual_path => path.virtual,
- :format => format,
- :variant => variant,
- :updated_at => mtime(template)
+ FileTemplate.new(File.expand_path(template), handler,
+ virtual_path: virtual_path,
+ format: format,
+ variant: variant,
+ locals: locals
)
end
- end
- def reject_files_external_to_app(files)
- files.reject { |filename| !inside_path?(@path, filename) }
- end
+ def reject_files_external_to_app(files)
+ files.reject { |filename| !inside_path?(@path, filename) }
+ end
- 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)
+ def find_template_paths_from_details(path, details)
+ query = build_query(path, details)
+ find_template_paths(query)
end
- end
- def inside_path?(path, filename)
- filename = File.expand_path(filename)
- path = File.join(path, '')
- filename.start_with?(path)
- end
+ 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
- # 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, candidates|
- if ext == :variants && candidates == :any
- query.gsub!(/:#{ext}/, "*")
- else
- query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}")
- end
- 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(/[*?{}\[\]]/, '\\\\\\&'.freeze)
- 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('.'.freeze)
- pieces.shift
+ # 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
- extension = pieces.pop
+ extension = pieces.pop
- handler = Template.handler_for_extension(extension)
- format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
- format &&= Template::Types[format]
+ handler = Template.handler_for_extension(extension)
+ format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
+ format = if format
+ Template::Types[format]&.ref
+ else
+ if handler.respond_to?(:default_format) # default_format can return nil
+ handler.default_format
+ else
+ nil
+ end
+ end
- [handler, format, variant]
- end
+ # Template::Types[format] and handler.default_format can return nil
+ [handler, format, variant]
+ end
end
- # A resolver that loads files from the filesystem. It allows setting your own
- # resolving pattern. Such pattern can be a glob string supported by some variables.
- #
- # ==== 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},}`
- #
- # 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.
- #
- # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}")
- #
- # If you don't specify a pattern then the default will be used.
- #
- # In order to use any of the customized resolvers above in a Rails application, you just need
- # to configure ActionController::Base.view_paths in an initializer, for example:
- #
- # ActionController::Base.view_paths = FileSystemResolver.new(
- # Rails.root.join("app/views"),
- # ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}",
- # )
- #
- # ==== Pattern format and variables
- #
- # Pattern has to be a valid glob string, and it allows you to use the
- # following variables:
- #
- # * <tt>:prefix</tt> - usually the controller path
- # * <tt>:action</tt> - name of the action
- # * <tt>:locale</tt> - possible locale versions
- # * <tt>:formats</tt> - possible request formats (for example html, json, xml...)
- # * <tt>:variants</tt> - possible request variants (for example phone, tablet...)
- # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
- #
+ # A resolver that loads files from the filesystem.
class FileSystemResolver < PathResolver
- def initialize(path, pattern=nil)
+ attr_reader :path
+
+ 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)
@@ -340,19 +301,60 @@ 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))
+ def initialize(path)
+ super(path)
+ end
- exts = EXTENSIONS.map do |ext, prefix|
- if ext == :variants && details[ext] == :any
- "{#{prefix}*,}"
- else
- "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}"
+ private
+
+ 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.
+ !regex.match?(filename) ||
+ 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.join
+ 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
@@ -362,8 +364,8 @@ module ActionView
[new(""), new("/")]
end
- def decorate(*)
- super.each { |t| t.virtual_path = nil }
+ def build_template(template, virtual_path, locals)
+ super(template, nil, locals)
end
end
end
diff --git a/actionview/lib/action_view/template/text.rb b/actionview/lib/action_view/template/text.rb
index 04f5b8d17a..c5fd55f1b3 100644
--- a/actionview/lib/action_view/template/text.rb
+++ b/actionview/lib/action_view/template/text.rb
@@ -1,22 +1,20 @@
+# 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]
end
def identifier
- 'text template'
+ "text template"
end
- def inspect
- 'text template'
- end
+ alias_method :inspect, :identifier
def to_str
@string
@@ -26,9 +24,12 @@ module ActionView #:nodoc:
to_str
end
- def formats
- [@type.respond_to?(:ref) ? @type.ref : @type.to_s]
+ def format
+ :text
end
+
+ def formats; Array(format); end
+ deprecate :formats
end
end
end
diff --git a/actionview/lib/action_view/template/types.rb b/actionview/lib/action_view/template/types.rb
index b32567cd66..67b7a62de6 100644
--- a/actionview/lib/action_view/template/types.rb
+++ b/actionview/lib/action_view/template/types.rb
@@ -1,7 +1,9 @@
-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
SET = Struct.new(:symbols).new([ :html, :text, :js, :css, :xml, :json ])
diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb
index 120962b5aa..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,16 +20,16 @@ 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
+ @request = ActionController::TestRequest.create(self.class)
@response = ActionDispatch::TestResponse.new
- @request.env.delete('PATH_INFO')
+ @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,25 +266,33 @@ module ActionView
end]
end
- def _routes
- @controller._routes if @controller.respond_to?(:_routes)
- end
-
def method_missing(selector, *args)
begin
routes = @controller.respond_to?(:_routes) && @controller._routes
rescue
- # Dont call routes, if there is an error on _routes call
+ # Don't call routes, if there is an error on _routes call
end
if routes &&
- ( routes.named_routes.route_defined?(selector) ||
- routes.mounted_helpers.method_defined?(selector) )
+ (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 2664aca991..a16dc0096e 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
@@ -6,48 +8,48 @@ module ActionView #:nodoc:
# useful for testing extensions that have no way of knowing what the file
# system will look like at runtime.
class FixtureResolver < PathResolver
- attr_reader :hash
-
- def initialize(hash = {}, pattern=nil)
+ def initialize(hash = {}, pattern = nil)
super(pattern)
@hash = hash
end
- def to_s
- @hash.keys.join(', ')
+ def data
+ @hash
end
- private
+ def to_s
+ @hash.keys.join(", ")
+ end
- 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, _, _, locals)
+ 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, source|
+ 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,
+ locals: locals
+ )
+ 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.virtual, handler, :virtual_path => path.virtual, :format => format, :variant => variant)]
+ def query(path, exts, _, _, locals)
+ 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, locals: locals)]
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 717d6866c5..3ca5aedc14 100644
--- a/actionview/lib/action_view/view_paths.rb
+++ b/actionview/lib/action_view/view_paths.rb
@@ -1,17 +1,25 @@
+# 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
+ ViewPaths.set_view_paths(self, ActionView::PathSet.new.freeze)
end
delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=,
- :locale, :locale=, :to => :lookup_context
+ :locale, :locale=, to: :lookup_context
module ClassMethods
+ def _view_paths
+ ViewPaths.get_view_paths(self)
+ end
+
+ def _view_paths=(paths)
+ ViewPaths.set_view_paths(self, paths)
+ end
+
def _prefixes # :nodoc:
@_prefixes ||= begin
return local_prefixes if superclass.abstract?
@@ -22,12 +30,28 @@ 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
+
+ # :stopdoc:
+ @all_view_paths = {}
+
+ def self.get_view_paths(klass)
+ @all_view_paths[klass] || get_view_paths(klass.superclass)
+ end
+
+ def self.set_view_paths(klass, paths)
+ @all_view_paths[klass] = paths
+ end
+
+ def self.all_view_paths
+ @all_view_paths.values.uniq
end
+ # :startdoc:
# The prefixes used in render "foo" shortcuts.
def _prefixes # :nodoc:
@@ -43,13 +67,25 @@ module ActionView
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..e5c8b9bb7d
--- /dev/null
+++ b/actionview/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@rails/ujs",
+ "version": "6.0.0-beta3",
+ "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 3256d8fc4d..fe317bf39e 100644
--- a/actionview/test/abstract_unit.rb
+++ b/actionview/test/abstract_unit.rb
@@ -1,36 +1,30 @@
-$:.unshift(File.dirname(__FILE__) + '/lib')
-$:.unshift(File.dirname(__FILE__) + '/fixtures/helpers')
-$:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers')
+# frozen_string_literal: true
-ENV['TMPDIR'] = File.join(File.dirname(__FILE__), 'tmp')
+$:.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'
+ENV["TMPDIR"] = File.expand_path("tmp", __dir__)
+
+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 '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 "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
ActiveSupport::Dependencies.hook!
@@ -43,18 +37,19 @@ 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
@view ||= begin
path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
view_paths = ActionView::PathSet.new([path])
- ActionView::Base.new(view_paths)
+ view = ActionView::Base.with_empty_template_cache
+ view.with_view_paths(view_paths)
end
end
@@ -64,53 +59,11 @@ module RenderERBUtils
template = ActionView::Template.new(
string.strip,
"test template",
- ActionView::Template::Handlers::ERB,
- {})
+ ActionView::Template.handler_for_extension(:erb),
+ format: :html, locals: [])
- template.render(self, {}).strip
- 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
-
- ActiveSupport::Deprecation.silence do
- SharedTestRoutes.draw do
- get ':controller(/:action)'
- end
-
- ActionDispatch::IntegrationTest.app.routes.draw do
- get ':controller(/:action)'
- end
- end
-
- DrawOnce.drew = true
- end
- end
-end
-
-module ActiveSupport
- class TestCase
- include ActionDispatch::DrawOnce
+ view = ActionView::Base.with_empty_template_cache
+ template.render(view.empty, {}).strip
end
end
@@ -133,21 +86,22 @@ 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|
+ 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
@@ -160,54 +114,13 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
self.app = build_app
- # Stub Rails dispatcher so it does not get controller references and
- # simply return the controller#action as Rack::Body.
- class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher
- 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 +128,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,23 +183,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
+
+ # 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..eecc19d413 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
@@ -229,13 +230,13 @@ module AbstractController
class ActionMissingRespondToActionController < AbstractController::Base
# No actions
- private
- def action_missing(action_name)
- self.response_body = "success"
- end
+ 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 80bc665b0a..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,19 +47,23 @@ 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
@@ -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 plain: '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 e185b76adb..e4e8ac93b2 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,15 +22,15 @@ module AbstractController
)]
def template
- render :template => "template"
+ render template: "template"
end
def file
- render :file => "some/file"
+ ActiveSupport::Deprecation.silence { render file: "some/file" }
end
def inline
- render :inline => "With <%= :Inline %>"
+ render inline: "With <%= :Inline %>"
end
def text
@@ -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 933456ce9d..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
diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb
index 64bc4c41d6..f946e4360d 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,34 +60,34 @@ 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
- with_template_handler :mab, lambda { |template| template.source.inspect } do
+ with_template_handler :mab, lambda { |template, source| source.inspect } do
@controller = ThirdPartyTemplateLibraryController.new
get :hello
assert_response :success
- 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,83 +152,90 @@ 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
+ with_template_handler :mab, lambda { |template, source| source.inspect } do
@controller = SetsLayoutInRenderController.new
get :hello
- assert_includes @response.body, 'layouts/third_party_template_library.mab'
+ 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
@controller = AbsolutePathLayoutController.new
- get :hello
+ assert_deprecated do
+ get :hello
+ end
assert_equal "layout_test.erb hello.erb", @response.body.strip
end
end
@@ -232,6 +245,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 +257,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 +273,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 bdb9e0397b..c8ce7366d1 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,12 +74,12 @@ 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:
@@ -126,11 +90,11 @@ class TestController < ApplicationController
# :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,7 +102,7 @@ class TestController < ApplicationController
end
def render_action_hello_world_with_symbol
- render :action => :hello_world
+ render action: :hello_world
end
# :ported:
@@ -149,70 +113,74 @@ class TestController < ApplicationController
# :ported:
def render_text_hello_world_with_layout
@variable_for_layout = ", I am here!"
- render plain: "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
+
+ def inline_rendered_format_without_format
+ render inline: "test"
end
# :ported:
def render_custom_code
- render plain: "hello world", :status => 404
+ render plain: "hello world", status: 404
end
# :ported:
@@ -240,7 +208,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 +217,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 +230,49 @@ class TestController < ApplicationController
# :ported:
def blank_response
- render plain: ' '
+ 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 plain: "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 +288,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 +301,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
@@ -364,38 +332,38 @@ class TestController < ApplicationController
@before = "i'm before the render"
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,7 +372,7 @@ 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:
@@ -414,13 +382,13 @@ class TestController < ApplicationController
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 plain: "hello"
- redirect_to :action => "double_render"
+ redirect_to action: "double_render"
end
def render_to_string_and_render
@@ -429,22 +397,22 @@ class TestController < ApplicationController
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
@@ -458,11 +426,11 @@ class TestController < ApplicationController
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
@@ -471,117 +439,121 @@ class TestController < ApplicationController
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_hashlike_locals
+ render partial: "customer", locals: ActionController::Parameters.new(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 +561,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 +577,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 +603,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 +614,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 +624,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 +636,125 @@ 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 :inline_rendered_format_without_format, to: "test#inline_rendered_format_without_format"
+ 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_hashlike_locals, to: "test#partial_with_hashlike_locals"
+ get :partials_list, to: "test#partials_list"
+ get :render_action_hello_world, to: "test#render_action_hello_world"
+ get :render_action_hello_world_as_string, to: "test#render_action_hello_world_as_string"
+ 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 +763,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 +822,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
@@ -776,53 +867,69 @@ class RenderTest < ActionController::TestCase
# :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:
def test_render_file_with_instance_variables
- get :render_file_with_instance_variables
+ assert_deprecated do
+ get :render_file_with_instance_variables
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
def test_render_file
- get :hello_world_file
+ assert_deprecated do
+ get :hello_world_file
+ end
assert_equal "Hello world!", @response.body
end
# :ported:
def test_render_file_not_using_full_path
- get :render_file_not_using_full_path
+ assert_deprecated do
+ get :render_file_not_using_full_path
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
# :ported:
def test_render_file_not_using_full_path_with_dot_in_path
- get :render_file_not_using_full_path_with_dot_in_path
+ assert_deprecated do
+ get :render_file_not_using_full_path_with_dot_in_path
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
# :ported:
def test_render_file_using_pathname
- get :render_file_using_pathname
+ assert_deprecated do
+ get :render_file_using_pathname
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
# :ported:
def test_render_file_with_locals
- get :render_file_with_locals
+ assert_deprecated do
+ get :render_file_with_locals
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
# :ported:
def test_render_file_as_string_with_locals
- get :render_file_as_string_with_locals
+ assert_deprecated do
+ get :render_file_as_string_with_locals
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
# :assessed:
def test_render_file_from_template
- get :render_file_from_template
+ assert_deprecated do
+ get :render_file_from_template
+ end
assert_equal "The secret is in the sauce\n", @response.body
end
@@ -831,27 +938,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
@@ -929,6 +1036,12 @@ class RenderTest < ActionController::TestCase
assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body
end
+ def test_rendered_format_without_format
+ get :inline_rendered_format_without_format
+ assert_equal "test", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
def test_partials_list
get :partials_list
assert_equal "goodbyeHello: davidHello: marygoodbye\n", @response.body
@@ -946,7 +1059,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 +1092,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 +1139,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
@@ -1036,11 +1149,19 @@ class RenderTest < ActionController::TestCase
end
def test_bad_render_to_string_still_throws_exception
- assert_raise(ActionView::MissingTemplate) { get :render_to_string_with_exception }
+ assert_deprecated do
+ assert_raise(ActionView::MissingTemplate) do
+ get :render_to_string_with_exception
+ end
+ end
end
def test_render_to_string_that_throws_caught_exception_doesnt_break_assigns
- assert_nothing_raised { get :render_to_string_with_caught_exception }
+ assert_deprecated do
+ assert_nothing_raised do
+ get :render_to_string_with_caught_exception
+ end
+ end
assert_equal "i'm before the render", @controller.instance_variable_get(:@before)
assert_equal "i'm after the render", @controller.instance_variable_get(:@after)
end
@@ -1108,7 +1229,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:
@@ -1206,6 +1327,11 @@ class RenderTest < ActionController::TestCase
assert_equal "Hello: david", @response.body
end
+ def test_partial_with_hashlike_locals
+ get :partial_with_hashlike_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 e99659c802..4449d08496 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 = ActionDispatch::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))
@@ -69,10 +77,10 @@ class ViewLoadPathsTest < ActionController::TestCase
end
def test_template_appends_view_path_correctly
- @controller.instance_variable_set :@template, ActionView::Base.new(TestController.view_paths, {}, @controller)
+ @controller.instance_variable_set :@template, ActionView::Base.with_view_paths(TestController.view_paths, {}, @controller)
class_view_paths = TestController.view_paths
- @controller.append_view_path 'foo'
+ @controller.append_view_path "foo"
assert_paths FIXTURE_LOAD_PATH, "foo"
@controller.append_view_path(%w(bar baz))
@@ -81,10 +89,10 @@ class ViewLoadPathsTest < ActionController::TestCase
end
def test_template_prepends_view_path_correctly
- @controller.instance_variable_set :@template, ActionView::Base.new(TestController.view_paths, {}, @controller)
+ @controller.instance_variable_set :@template, ActionView::Base.with_view_paths(TestController.view_paths, {}, @controller)
class_view_paths = TestController.view_paths
- @controller.prepend_view_path 'baz'
+ @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,9 @@ 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.format,
+ locals: template.locals
)
end
end
@@ -157,14 +168,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..f56168bda5
--- /dev/null
+++ b/actionview/test/activerecord/multifetch_cache_test.rb
@@ -0,0 +1,37 @@
+# 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_paths.each(&:clear_cache)
+ ActionView::LookupContext.fallbacks.each(&:clear_cache)
+
+ @view = Class.new(ActionView::Base.with_empty_template_cache) do
+ def view_cache_dependencies
+ []
+ end
+
+ def combined_fragment_cache_key(key)
+ [ :views, key ]
+ end
+ end.with_view_paths(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
index 8e97417b94..6fe83dcb9a 100644
--- a/actionview/test/activerecord/relation_cache_test.rb
+++ b/actionview/test/activerecord/relation_cache_test.rb
@@ -1,18 +1,25 @@
-require 'active_record_unit'
+# frozen_string_literal: true
-class RelationCacheTest < ActionView::TestCase
+require "active_record_unit"
+
+class RelationCacheTest < ActionView::TestCase
tests ActionView::Helpers::CacheHelper
def setup
- @virtual_path = "path"
+ 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"
+ @current_template = lookup_context.find "test/hello_world"
+
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/projects-#{Project.count}/")
+ cache(Project.all) { concat("Hello World") }
+ assert_equal "Hello World", controller.cache_store.read("views/test/hello_world:fa9482a68ce25bf7589b8eddad72f736/projects-#{Project.count}")
end
- def view_cache_dependencies; 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/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..4c34ee85f0 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(template: "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/show.js.erb b/actionview/test/fixtures/digestor/comments/show.js.erb
new file mode 100644
index 0000000000..38b37dfa2b
--- /dev/null
+++ b/actionview/test/fixtures/digestor/comments/show.js.erb
@@ -0,0 +1 @@
+alert("<%=j render("comments/comment") %>")
diff --git a/actionview/test/fixtures/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/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 404b12cbab..019ddb7aef 100644
--- a/actionview/test/fixtures/project.rb
+++ b/actionview/test/fixtures/project.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Project < ActiveRecord::Base
has_and_belongs_to_many :developers, -> { uniq }
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/_first.html.erb b/actionview/test/fixtures/test/_first.html.erb
new file mode 100644
index 0000000000..2e2c825acb
--- /dev/null
+++ b/actionview/test/fixtures/test/_first.html.erb
@@ -0,0 +1 @@
+"HTML"
diff --git a/actionview/test/fixtures/test/_first.xml.erb b/actionview/test/fixtures/test/_first.xml.erb
new file mode 100644
index 0000000000..9cf4f4ec85
--- /dev/null
+++ b/actionview/test/fixtures/test/_first.xml.erb
@@ -0,0 +1 @@
+"XML"
diff --git a/actionview/test/fixtures/test/_first_layer.html.erb b/actionview/test/fixtures/test/_first_layer.html.erb
new file mode 100644
index 0000000000..c1f1acb410
--- /dev/null
+++ b/actionview/test/fixtures/test/_first_layer.html.erb
@@ -0,0 +1,4 @@
+{"format":"HTML", "children":
+[
+ <%= render(partial: "first").chomp.html_safe %>,
+]}
diff --git a/actionview/test/fixtures/test/_first_layer.xml.erb b/actionview/test/fixtures/test/_first_layer.xml.erb
new file mode 100644
index 0000000000..b8581bbbfc
--- /dev/null
+++ b/actionview/test/fixtures/test/_first_layer.xml.erb
@@ -0,0 +1,4 @@
+{"format":"XML", "children":
+[
+ <%= render(partial: "first").chomp.html_safe %>
+]}
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/_second.html.erb b/actionview/test/fixtures/test/_second.html.erb
new file mode 100644
index 0000000000..2e2c825acb
--- /dev/null
+++ b/actionview/test/fixtures/test/_second.html.erb
@@ -0,0 +1 @@
+"HTML"
diff --git a/actionview/test/fixtures/test/_second.xml.erb b/actionview/test/fixtures/test/_second.xml.erb
new file mode 100644
index 0000000000..9cf4f4ec85
--- /dev/null
+++ b/actionview/test/fixtures/test/_second.xml.erb
@@ -0,0 +1 @@
+"XML"
diff --git a/actionview/test/fixtures/test/_second_layer.html.erb b/actionview/test/fixtures/test/_second_layer.html.erb
new file mode 100644
index 0000000000..307706abd2
--- /dev/null
+++ b/actionview/test/fixtures/test/_second_layer.html.erb
@@ -0,0 +1,4 @@
+{"format":"HTML", "children":
+[
+ <%= render(partial: "first").chomp.html_safe %>
+]}
diff --git a/actionview/test/fixtures/test/_second_layer.xml.erb b/actionview/test/fixtures/test/_second_layer.xml.erb
new file mode 100644
index 0000000000..b8581bbbfc
--- /dev/null
+++ b/actionview/test/fixtures/test/_second_layer.xml.erb
@@ -0,0 +1,4 @@
+{"format":"XML", "children":
+[
+ <%= render(partial: "first").chomp.html_safe %>
+]}
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..4c34ee85f0 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(template: "test/greeting")
+end
diff --git a/actionview/test/fixtures/test/layout_render_file.erb b/actionview/test/fixtures/test/layout_render_file.erb
index 2f8e921c5f..0477743dc4 100644
--- a/actionview/test/fixtures/test/layout_render_file.erb
+++ b/actionview/test/fixtures/test/layout_render_file.erb
@@ -1,2 +1,2 @@
<% content_for :title do %>title<% end -%>
-<%= render :file => 'layouts/yield' -%> \ No newline at end of file
+<%= render template: 'layouts/yield' -%>
diff --git a/actionview/test/fixtures/test/mixing_formats.html.erb b/actionview/test/fixtures/test/mixing_formats.html.erb
new file mode 100644
index 0000000000..c65cdd7dd4
--- /dev/null
+++ b/actionview/test/fixtures/test/mixing_formats.html.erb
@@ -0,0 +1,5 @@
+{"format":"HTML", "children":
+[
+ <%= render(partial: "first", formats: :xml).chomp.html_safe %>,
+ <%= render(partial: "second").chomp.html_safe %>
+]}
diff --git a/actionview/test/fixtures/test/mixing_formats_deep.html.erb b/actionview/test/fixtures/test/mixing_formats_deep.html.erb
new file mode 100644
index 0000000000..e328887eeb
--- /dev/null
+++ b/actionview/test/fixtures/test/mixing_formats_deep.html.erb
@@ -0,0 +1,5 @@
+{"format":"HTML", "children":
+[
+ <%= render(partial: "first_layer", formats: :xml).chomp.html_safe %>,
+ <%= render(partial: "second_layer").chomp.html_safe %>
+]}
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/syntax_error.html.erb b/actionview/test/fixtures/test/syntax_error.html.erb
new file mode 100644
index 0000000000..4004a2b187
--- /dev/null
+++ b/actionview/test/fixtures/test/syntax_error.html.erb
@@ -0,0 +1,4 @@
+<%= foo(
+ 1,
+ 2,
+%>
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 55d62cf692..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
@@ -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 8bfd19eb26..e371a87614 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,6 +29,10 @@ class AssetTagHelperTest < ActionView::TestCase
"http://www.example.com"
end
+ def content_security_policy_nonce
+ "iyhD0Yc0W+c="
+ end
+
AssetPathToTag = {
%(asset_path("")) => %(),
%(asset_path(" ")) => %(),
@@ -53,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" />),
@@ -180,23 +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("gold.png", data: { title: 'Rails Application' })) => %(<img data-title="Rails Application" src="/images/gold.png" alt="Gold" />)
+ %(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 = {
@@ -207,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),
@@ -238,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>),
@@ -288,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 = {
@@ -299,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)
@@ -306,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
@@ -322,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
@@ -362,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
@@ -407,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
@@ -455,29 +512,19 @@ class AssetTagHelperTest < ActionView::TestCase
UrlToImageToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
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 'Rails', image_alt("#{prefix}rails-f56ef62bc41b040664e801a38f068082a75d506d9048307e8096737463503d0b.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")
- end
- end
-
def test_image_tag
ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
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)
@@ -487,6 +534,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
@@ -531,12 +582,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
@@ -545,11 +604,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
@@ -557,13 +616,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
@@ -614,7 +670,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
@@ -630,15 +688,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
@@ -652,12 +710,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
@@ -708,26 +766,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
@@ -736,6 +794,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
@@ -747,10 +822,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
@@ -813,6 +888,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 ffaf773c53..45070674ad 100644
--- a/actionview/test/template/capture_helper_test.rb
+++ b/actionview/test/template/capture_helper_test.rb
@@ -1,27 +1,29 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class CaptureHelperTest < ActionView::TestCase
def setup
super
- @av = ActionView::Base.new
+ @av = ActionView::Base.empty
@view_flow = ActionView::OutputFlow.new
end
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,140 +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 { raw('&lt;em&gt;bar&lt;/em&gt;') }
- 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 ! content_for?(:title)
+ assert_not content_for?(:title)
content_for :title do
- content_tag(:p, 'title')
+ content_tag(:p, "title")
end
- assert content_for(:title).html_safe?
+ assert_predicate content_for(:title), :html_safe?
content_for :title, "", flush: true
content_for(:title) do
- content_tag(:p, 'title')
+ content_tag(:p, "title")
end
- assert content_for(:title).html_safe?
+ assert_predicate content_for(:title), :html_safe?
end
def test_provide
- assert !content_for?(:title)
+ assert_not content_for?(:title)
provide :title, "hi"
assert content_for?(:title)
assert_equal "hi", content_for(:title)
@@ -178,19 +180,19 @@ class CaptureHelperTest < ActionView::TestCase
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
@@ -198,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
@@ -217,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..d7f2e8ee07 100644
--- a/actionview/test/template/compiled_templates_test.rb
+++ b/actionview/test/template/compiled_templates_test.rb
@@ -1,33 +1,79 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class CompiledTemplatesTest < ActiveSupport::TestCase
- teardown do
+ attr_reader :view_class
+
+ def setup
+ super
+ view_paths = ActionController::Base.view_paths
+ view_paths.each(&:clear_cache)
+ ActionView::LookupContext.fallbacks.each(&:clear_cache)
+ @view_class = ActionView::Base.with_empty_template_cache
+ end
+
+ def teardown
+ super
ActionView::LookupContext::DetailsKey.clear
end
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(template: "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(template: "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(template: "test/test_template_with_delegation_reserved_keywords", locals: locals)
+ end
+
+ def test_template_with_unicode_identifier
+ assert_equal "🎂", render(template: "test/render_file_unicode_local", locals: { 🎃: "🎂" })
+ end
+
+ def test_template_with_instance_variable_identifier
+ assert_equal "bar", render(template: "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(template: "test/render_file_with_locals_and_default")
+ assert_equal "two", render(template: "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(template: "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(template: "test/hello_world")
end
- assert_equal "Hello world!", render(:file => "test/hello_world")
+ assert_equal "Hello world!", render(template: "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(template: "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(template: "test/hello_world")
end
- assert_equal "Hello world!", render_without_cache(:file => "test/hello_world")
+ assert_equal "Hello world!", render_without_cache(template: "test/hello_world")
end
private
@@ -37,13 +83,13 @@ class CompiledTemplatesTest < ActiveSupport::TestCase
def render_with_cache(*args)
view_paths = ActionController::Base.view_paths
- ActionView::Base.new(view_paths, {}).render(*args)
+ view_class.with_view_paths(view_paths, {}).render(*args)
end
def render_without_cache(*args)
path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
view_paths = ActionView::PathSet.new([path])
- ActionView::Base.new(view_paths, {}).render(*args)
+ view_class.with_view_paths(view_paths, {}).render(*args)
end
def modify_template(template, content)
diff --git a/actionview/test/template/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/csp_helper_test.rb b/actionview/test/template/csp_helper_test.rb
new file mode 100644
index 0000000000..1b7fd4665f
--- /dev/null
+++ b/actionview/test/template/csp_helper_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class CspHelperWithCspEnabledTest < ActionView::TestCase
+ tests ActionView::Helpers::CspHelper
+
+ def content_security_policy_nonce
+ "iyhD0Yc0W+c="
+ end
+
+ def content_security_policy?
+ true
+ end
+
+ def test_csp_meta_tag
+ assert_equal "<meta name=\"csp-nonce\" content=\"iyhD0Yc0W+c=\" />", csp_meta_tag
+ end
+
+ def test_csp_meta_tag_with_options
+ assert_equal "<meta property=\"csp-nonce\" name=\"csp-nonce\" content=\"iyhD0Yc0W+c=\" />", csp_meta_tag(property: "csp-nonce")
+ end
+end
+
+class CspHelperWithCspDisabledTest < ActionView::TestCase
+ tests ActionView::Helpers::CspHelper
+
+ def content_security_policy?
+ false
+ end
+
+ def test_csp_meta_tag
+ assert_nil csp_meta_tag
+ end
+end
diff --git a/actionview/test/template/csrf_helper_test.rb b/actionview/test/template/csrf_helper_test.rb
new file mode 100644
index 0000000000..dd9821eb6c
--- /dev/null
+++ b/actionview/test/template/csrf_helper_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class CsrfHelperTest < ActiveSupport::TestCase
+ cattr_accessor :request_forgery, default: false
+
+ include ActionView::Helpers::CsrfHelper
+ include ActionView::Helpers::TagHelper
+ include Rails::Dom::Testing::Assertions::DomAssertions
+
+ def test_csrf_meta_tags_without_request_forgery_protection
+ assert_dom_equal "", csrf_meta_tags
+ end
+
+ def test_csrf_meta_tags_with_request_forgery_protection
+ self.request_forgery = true
+
+ assert_dom_equal <<~DOM.chomp, csrf_meta_tags
+ <meta name="csrf-param" content="form_token" />
+ <meta name="csrf-token" content="secret" />
+ DOM
+ ensure
+ self.request_forgery = false
+ end
+
+ def test_csrf_meta_tags_without_protect_against_forgery_method
+ self.class.undef_method(:protect_against_forgery?)
+
+ assert_dom_equal "", csrf_meta_tags
+ ensure
+ self.class.define_method(:protect_against_forgery?) { request_forgery }
+ end
+
+ def protect_against_forgery?
+ request_forgery
+ end
+
+ def form_authenticity_token(*args)
+ "secret"
+ end
+
+ def request_forgery_protection_token
+ "form_token"
+ end
+end
diff --git a/actionview/test/template/date_helper_i18n_test.rb b/actionview/test/template/date_helper_i18n_test.rb
index 52aef56a61..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,30 +48,30 @@ class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase
end
def test_time_ago_in_words_passes_locale
- 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')
+ 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
@@ -78,11 +80,11 @@ 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
assert_called_with(I18n, :t, [key, options]) do
- distance_of_time_in_words(@from, to, passed_options.merge(locale: 'en'))
+ distance_of_time_in_words(@from, to, passed_options.merge(locale: "en"))
end
end
end
@@ -95,28 +97,28 @@ class DateHelperSelectTagsI18nTests < ActiveSupport::TestCase
def test_select_month_given_use_month_names_option_does_not_translate_monthnames
assert_not_called(I18n, :translate) do
- select_month(8, :locale => 'en', :use_month_names => Date::MONTHNAMES)
+ select_month(8, locale: "en", use_month_names: Date::MONTHNAMES)
end
end
def test_select_month_translates_monthnames
- assert_called_with(I18n, :translate, [:'date.month_names', :locale => 'en'], returns: Date::MONTHNAMES) do
- 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
- 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)
+ 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 = {:year => 'Year', :month => 'Month', :day => 'Day', :hour => 'Hour', :minute => 'Minute', :second => 'Seconds'}
- defaults = {[:'date.order', :locale => 'en', :default => []] => %w(year month day)}
+ 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
+ defaults[[("datetime.prompts." + key.to_s).to_sym, locale: "en"]] = prompt
end
prompts_check = -> (prompt, x) do
@@ -129,7 +131,7 @@ class DateHelperSelectTagsI18nTests < ActiveSupport::TestCase
end
I18n.stub(:translate, prompts_check) do
- datetime_select('post', 'updated_at', :locale => 'en', :include_seconds => true, :prompt => true, :use_month_names => Date::MONTHNAMES)
+ 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
@@ -138,27 +140,27 @@ class DateHelperSelectTagsI18nTests < ActiveSupport::TestCase
def test_date_or_time_select_given_an_order_options_does_not_translate_order
assert_not_called(I18n, :translate) do
- datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en', :use_month_names => Date::MONTHNAMES)
+ 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
- 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')
+ 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
- assert_called_with(I18n, :translate, [:'date.order', :locale => 'en', :default => []], returns: %w(invalid month day)) do
+ 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')
+ datetime_select("post", "updated_at", locale: "en")
end
end
end
def test_date_or_time_select_given_symbol_keys
- 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')
+ 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 4678998bdc..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,230 +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')
+ 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')
+ 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"
@@ -496,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"
@@ -568,87 +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')
+ 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"
@@ -656,63 +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')
+ 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"
@@ -724,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"
@@ -745,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"
@@ -774,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
@@ -797,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)
@@ -821,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
@@ -845,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"
@@ -862,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)
@@ -878,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"
@@ -895,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)
@@ -911,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)
@@ -927,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"
@@ -943,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"
@@ -963,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"
@@ -979,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"
@@ -1015,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"
@@ -1031,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"
@@ -1059,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"
@@ -1087,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"
@@ -1115,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)
@@ -1143,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"
@@ -1172,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"
@@ -1204,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
@@ -1212,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"
@@ -1236,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"
@@ -1266,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"
@@ -1295,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)
@@ -1327,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)
@@ -1345,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)
@@ -1362,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)
@@ -1375,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)
@@ -1411,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)
@@ -1429,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
@@ -1438,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)
@@ -1458,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)
@@ -1482,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"
@@ -1521,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"
@@ -1534,14 +1830,14 @@ 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 = +%{<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"
@@ -1554,7 +1850,7 @@ class DateHelperTest < ActionView::TestCase
expected << "</select>\n"
- assert_dom_equal expected, date_select("post", "written_on", :selected => {day: 10, month: 07, year: 2004})
+ assert_dom_equal expected, date_select("post", "written_on", selected: { day: 10, month: 07, year: 2004 })
end
def test_date_select_with_selected_nil
@@ -1579,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"
@@ -1589,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"
@@ -1622,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"
@@ -1639,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
@@ -1650,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}
@@ -1662,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}
@@ -1678,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}
@@ -1695,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"
@@ -1707,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
@@ -1715,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"
@@ -1734,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"
@@ -1742,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"
@@ -1772,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"
@@ -1789,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"
@@ -1809,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
@@ -1843,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"
@@ -1862,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"
@@ -1882,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
@@ -1890,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"
@@ -1913,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"
@@ -1930,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"
@@ -1953,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"
@@ -1971,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"
@@ -1991,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"
@@ -2011,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}
@@ -2037,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}
@@ -2056,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}
@@ -2075,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 << " : "
@@ -2083,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}
@@ -2106,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}
@@ -2125,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
@@ -2133,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}
@@ -2155,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}
@@ -2175,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}
@@ -2192,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}
@@ -2213,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" })
+ 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", :prompt => {:hour => 'Choose hour', :minute => 'Choose minute'})
+ 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}
@@ -2236,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"
@@ -2272,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"
@@ -2294,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
@@ -2328,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
@@ -2338,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"
@@ -2370,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}
@@ -2386,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"
@@ -2420,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
@@ -2431,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
@@ -2439,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"
@@ -2461,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"
@@ -2490,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)
@@ -2506,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"
@@ -2523,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)
@@ -2539,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)
@@ -2555,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)
@@ -2583,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
@@ -2591,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"
@@ -2613,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
@@ -2621,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"
@@ -2655,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"
@@ -2684,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}
@@ -2708,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"
@@ -2733,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}
@@ -2756,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}
@@ -2775,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}
@@ -2794,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}
@@ -2811,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}
@@ -2835,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}
@@ -2859,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}
@@ -2886,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"
@@ -2911,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}
@@ -2938,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"
@@ -2958,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}
@@ -2985,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"
@@ -3014,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
@@ -3022,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
@@ -3048,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
@@ -3074,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
@@ -3193,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){ raw('<span>Right now</span>') })
+ 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 3ece9e50cd..42cb14a18a 100644
--- a/actionview/test/template/dependency_tracker_test.rb
+++ b/actionview/test/template/dependency_tracker_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'action_view/dependency_tracker'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "action_view/dependency_tracker"
class NeckbeardTracker
def self.call(name, template)
@@ -15,8 +17,8 @@ class FakeTemplate
end
end
-Neckbeard = lambda {|template| template.source }
-Bowtie = lambda {|template| template.source }
+Neckbeard = lambda { |template, source| source }
+Bowtie = lambda { |template, source| source }
class DependencyTrackerTest < ActionView::TestCase
def tracker
diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb
index d4c5048bde..4515afdfff 100644
--- a/actionview/test/template/digestor_test.rb
+++ b/actionview/test/template/digestor_test.rb
@@ -1,23 +1,20 @@
-require 'abstract_unit'
-require 'fileutils'
-require 'action_view/dependency_tracker'
+# 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, [])
+class FixtureFinder < ActionView::LookupContext
+ FIXTURES_DIR = File.expand_path("../fixtures/digestor", __dir__)
+
+ def self.build(details = {})
+ new(ActionView::PathSet.new(["digestor", "digestor/api"]), details, [])
end
end
-class FixtureFinder < ActionView::LookupContext
- FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor"
-
- def initialize(details = {})
- super(ActionView::PathSet.new(['digestor']), details, [])
+class ActionView::Digestor::Node
+ def flatten
+ [self] + children.flat_map(&:flatten)
end
end
@@ -61,25 +58,21 @@ class TemplateDigestorTest < ActionView::TestCase
end
def test_explicit_dependency_wildcard_picks_up_added_file
- old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false
-
- assert_digest_difference("events/index") do
- add_template("events/_uncompleted")
+ disable_resolver_caching do
+ assert_digest_difference("events/index") do
+ add_template("events/_uncompleted")
+ end
end
- ensure
- remove_template("events/_uncompleted")
- ActionView::Resolver.caching = old_caching
end
def test_explicit_dependency_wildcard_picks_up_removed_file
- old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false
- add_template("events/_subscribers_changed")
+ disable_resolver_caching do
+ add_template("events/_subscribers_changed")
- assert_digest_difference("events/index") do
- remove_template("events/_subscribers_changed")
+ assert_digest_difference("events/index") do
+ remove_template("events/_subscribers_changed")
+ end
end
- ensure
- ActionView::Resolver.caching = old_caching
end
def test_second_level_dependency
@@ -119,25 +112,25 @@ 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')
+ 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')
+ doubly_nested = [{ "comments/comments" => ["comments/comment"] }, "messages/message"]
+ assert_equal doubly_nested, nested_dependencies("messages/peek")
end
def test_nested_template_directory
@@ -147,10 +140,34 @@ class TemplateDigestorTest < ActionView::TestCase
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"]
+ 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
+ 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 = finder.with_prepended_formats([:json])
+ 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
+ assert_equal ["comments/comment"], dependencies("comments/show")
+ end
+
+ def test_template_digest_with_fallback_from_js_to_html_format
+ 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
@@ -177,7 +194,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
@@ -198,14 +215,14 @@ class TemplateDigestorTest < ActionView::TestCase
def test_details_are_included_in_cache_key
# Cache the template digest.
- @finder = FixtureFinder.new({:formats => [:html]})
+ @finder = FixtureFinder.build(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 = FixtureFinder.new
+ @finder = FixtureFinder.build
# The cache is busted.
assert_not_equal old_digest, digest("events/_event")
@@ -262,6 +279,13 @@ class TemplateDigestorTest < ActionView::TestCase
assert_not_equal digest_phone, digest_fridge_phone
end
+ 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
first_digest = digest("level/_recursion")
second_digest = digest("level/_recursion")
@@ -273,21 +297,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
@@ -306,19 +326,29 @@ class TemplateDigestorTest < ActionView::TestCase
def assert_digest_difference(template_name, options = {})
previous_digest = digest(template_name, options)
+ finder.view_paths.each(&:clear_cache)
finder.digest_cache.clear
yield
assert_not_equal previous_digest, digest(template_name, options), "digest didn't change"
finder.digest_cache.clear
+ finder.view_paths.each(&:clear_cache)
end
def digest(template_name, options = {})
options = options.dup
+ finder_options = options.extract!(:variants, :format)
+
+ finder.variants = finder_options[:variants] || []
- finder.variants = options.delete(:variants) || []
- ActionView::Digestor.digest(name: template_name, finder: finder, dependencies: (options[:dependencies] || []))
+ finder_with_formats = if finder_options[:format]
+ finder.with_prepended_formats(Array(finder_options[:format]))
+ else
+ finder
+ end
+
+ ActionView::Digestor.digest(name: template_name, format: finder_options[:format], finder: finder_with_formats, dependencies: (options[:dependencies] || []))
end
def dependencies(template_name)
@@ -331,8 +361,20 @@ class TemplateDigestorTest < ActionView::TestCase
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.map(&:format)
+ end
+
+ def disable_resolver_caching
+ old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false
+ yield
+ ensure
+ ActionView::Resolver.caching = old_caching
+ end
+
def finder
- @finder ||= FixtureFinder.new
+ @finder ||= FixtureFinder.build
end
def change_template(template_name, variant = nil)
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/fallback_file_system_resolver_test.rb b/actionview/test/template/fallback_file_system_resolver_test.rb
new file mode 100644
index 0000000000..304cdb8a03
--- /dev/null
+++ b/actionview/test/template/fallback_file_system_resolver_test.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class FallbackFileSystemResolverTest < ActiveSupport::TestCase
+ def setup
+ @root_resolver = ActionView::FallbackFileSystemResolver.new("/")
+ end
+
+ def test_should_have_no_virtual_path
+ templates = @root_resolver.find_all("hello_world.erb", "#{FIXTURE_LOAD_PATH}/test", false, locale: [], formats: [:html], variants: [], handlers: [:erb])
+ assert_equal 1, templates.size
+ assert_equal "Hello world!", templates[0].source
+ assert_nil templates[0].virtual_path
+ end
+end
diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb
index 4f7ea88169..ca117d4a30 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,185 @@ 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
+ 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 generates labels for non-English values correctly" do
+ with_collection_radio_buttons :user, :title, ["ГоÑподин", "ГоÑпожа"], :to_s, :to_s
+
+ assert_select "input[type=radio]#user_title_гоÑподин"
+ assert_select "label[for=user_title_гоÑподин]", "ГоÑподин"
+ end
- assert_select 'label[for=user_active_yes]', 'Yes'
- assert_select 'label[for=user_active_no]', 'No'
+ 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_0_99]", "$0.99"
+ assert_select "label[for=user_name_1_99]", "$1.99"
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 correctly builds unique DOM IDs for float values" do
+ with_collection_radio_buttons :user, :name, [1.0, 10], :to_s, :to_s
+ assert_select "label[for=user_name_1_0]", "1.0"
+ assert_select "label[for=user_name_10]", "10"
+ assert_select 'input#user_name_1_0[type=radio][value="1.0"]'
+ assert_select 'input#user_name_10[type=radio][value="10"]'
end
- test 'collection radio accepts checked item' do
- 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,186 +204,201 @@ 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]'
+ 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')]
+ 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')]
+ 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 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 }
+ 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')]
+ 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
+ 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 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
+ 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')]
+ 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
+
+ assert_select "label[for=user_active_yes]", "Yes"
+ assert_select "label[for=user_active_no]", "No"
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 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_0_99]", "$0.99"
+ assert_select "label[for=user_name_1_99]", "$1.99"
+ end
- assert_select 'label[for=user_active_yes]', 'Yes'
- assert_select 'label[for=user_active_no]', 'No'
+ test "collection check boxes correctly builds unique DOM IDs for float values" do
+ with_collection_check_boxes :user, :name, [1.0, 10], :to_s, :to_s
+ assert_select "label[for=user_name_1_0]", "1.0"
+ assert_select "label[for=user_name_10]", "10"
+ assert_select 'input#user_name_1_0[type=checkbox][value="1.0"]'
+ assert_select 'input#user_name_10[type=checkbox][value="10"]'
end
- test 'collection check 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 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'}]]
+ 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 propagates input id to the label for attribute' do
- collection = [[1, 'Category 1', {id: 'foo'}], [2, 'Category 2', {id: '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]'
+ 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'}]]
+ 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]'
@@ -375,70 +406,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
@@ -446,67 +477,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..42069340f1
--- /dev/null
+++ b/actionview/test/template/form_helper/form_with_test.rb
@@ -0,0 +1,2360 @@
+# 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
+ I18n.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
+ I18n.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
+ I18n.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
+ I18n.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)
+ I18n.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
+end
diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb
index 310d0ce514..91052e5ae2 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
@@ -189,31 +203,31 @@ class FormHelperTest < ActionView::TestCase
end
def test_label_with_locales_strings
- with_locale :label do
+ I18n.with_locale :label do
assert_dom_equal('<label for="post_body">Write entire text here</label>', label("post", "body"))
end
end
def test_label_with_human_attribute_name
- with_locale :label do
+ I18n.with_locale :label do
assert_dom_equal('<label for="post_cost">Total cost</label>', label(:post, :cost))
end
end
def test_label_with_human_attribute_name_and_options
- with_locale :label do
+ I18n.with_locale :label do
assert_dom_equal('<label for="post_language_spanish">Espanol</label>', label(:post, :language, value: "spanish"))
end
end
def test_label_with_locales_symbols
- with_locale :label do
+ I18n.with_locale :label do
assert_dom_equal('<label for="post_body">Write entire text here</label>', label(:post, :body))
end
end
def test_label_with_locales_and_options
- with_locale :label do
+ I18n.with_locale :label do
assert_dom_equal(
'<label for="post_body" class="post_body">Write entire text here</label>',
label(:post, :body, class: "post_body")
@@ -222,14 +236,14 @@ class FormHelperTest < ActionView::TestCase
end
def test_label_with_locales_and_value
- with_locale :label do
+ I18n.with_locale :label do
assert_dom_equal('<label for="post_color_red">Rojo</label>', label(:post, :color, value: "red"))
end
end
def test_label_with_locales_and_nested_attributes
- with_locale :label do
- form_for(@post, html: { id: 'create-post' }) do |f|
+ I18n.with_locale :label do
+ form_for(@post, html: { id: "create-post" }) do |f|
f.fields_for(:comments) do |cf|
concat cf.label(:body)
end
@@ -244,8 +258,8 @@ class FormHelperTest < ActionView::TestCase
end
def test_label_with_locales_fallback_and_nested_attributes
- with_locale :label do
- form_for(@post, html: { id: 'create-post' }) do |f|
+ I18n.with_locale :label do
+ 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
@@ -344,7 +358,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_label_with_block_and_builder
- with_locale :label do
+ I18n.with_locale :label do
assert_dom_equal(
'<label for="post_body"><b>Write entire text here</b></label>',
label(:post, :body) { |b| raw("<b>#{b.translation}</b>") }
@@ -367,7 +381,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_label_with_to_model_and_overridden_model_name
- with_locale :label do
+ I18n.with_locale :label do
assert_dom_equal(
%{<label for="post_delegator_title">Delegate model_name title</label>},
label(:post_delegator, :title)
@@ -376,19 +390,19 @@ class FormHelperTest < ActionView::TestCase
end
def test_text_field_placeholder_without_locales
- with_locale :placeholder do
+ I18n.with_locale :placeholder do
assert_dom_equal('<input id="post_body" name="post[body]" placeholder="Body" type="text" value="Back to the hill and over it again!" />', text_field(:post, :body, placeholder: true))
end
end
def test_text_field_placeholder_with_locales
- with_locale :placeholder do
+ I18n.with_locale :placeholder do
assert_dom_equal('<input id="post_title" name="post[title]" placeholder="What is this about?" type="text" value="Hello World" />', text_field(:post, :title, placeholder: true))
end
end
def test_text_field_placeholder_with_locales_and_to_model
- with_locale :placeholder do
+ I18n.with_locale :placeholder do
assert_dom_equal(
'<input id="post_delegator_title" name="post_delegator[title]" placeholder="Delegate model_name title" type="text" value="Hello World" />',
text_field(:post_delegator, :title, placeholder: true)
@@ -397,7 +411,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_text_field_placeholder_with_human_attribute_name
- with_locale :placeholder do
+ I18n.with_locale :placeholder do
assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Total cost" type="text" />', text_field(:post, :cost, placeholder: true))
end
end
@@ -410,26 +424,26 @@ class FormHelperTest < ActionView::TestCase
end
def test_text_field_placeholder_with_string_value
- with_locale :placeholder do
+ I18n.with_locale :placeholder do
assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="HOW MUCH?" type="text" />', text_field(:post, :cost, placeholder: "HOW MUCH?"))
end
end
def test_text_field_placeholder_with_human_attribute_name_and_value
- with_locale :placeholder do
+ I18n.with_locale :placeholder do
assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Pounds" type="text" />', text_field(:post, :cost, placeholder: :uk))
end
end
def test_text_field_placeholder_with_locales_and_value
- with_locale :placeholder do
+ I18n.with_locale :placeholder do
assert_dom_equal('<input id="post_written_on" name="post[written_on]" placeholder="Escrito en" type="text" value="2004-06-15" />', text_field(:post, :written_on, placeholder: :spanish))
end
end
def test_text_field_placeholder_with_locales_and_nested_attributes
- with_locale :placeholder do
- form_for(@post, html: { id: 'create-post' }) do |f|
+ I18n.with_locale :placeholder do
+ form_for(@post, html: { id: "create-post" }) do |f|
f.fields_for(:comments) do |cf|
concat cf.text_field(:body, placeholder: true)
end
@@ -444,8 +458,8 @@ class FormHelperTest < ActionView::TestCase
end
def test_text_field_placeholder_with_locales_fallback_and_nested_attributes
- with_locale :placeholder do
- form_for(@post, html: { id: 'create-post' }) do |f|
+ I18n.with_locale :placeholder do
+ 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
@@ -842,7 +861,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_text_area_placeholder_without_locales
- with_locale :placeholder do
+ I18n.with_locale :placeholder do
assert_dom_equal(
%{<textarea id="post_body" name="post[body]" placeholder="Body">\nBack to the hill and over it again!</textarea>},
text_area(:post, :body, placeholder: true)
@@ -851,7 +870,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_text_area_placeholder_with_locales
- with_locale :placeholder do
+ I18n.with_locale :placeholder do
assert_dom_equal(
%{<textarea id="post_title" name="post[title]" placeholder="What is this about?">\nHello World</textarea>},
text_area(:post, :title, placeholder: true)
@@ -860,7 +879,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_text_area_placeholder_with_human_attribute_name
- with_locale :placeholder do
+ I18n.with_locale :placeholder do
assert_dom_equal(
%{<textarea id="post_cost" name="post[cost]" placeholder="Total cost">\n</textarea>},
text_area(:post, :cost, placeholder: true)
@@ -869,7 +888,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_text_area_placeholder_with_string_value
- with_locale :placeholder do
+ I18n.with_locale :placeholder do
assert_dom_equal(
%{<textarea id="post_cost" name="post[cost]" placeholder="HOW MUCH?">\n</textarea>},
text_area(:post, :cost, placeholder: "HOW MUCH?")
@@ -878,7 +897,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_text_area_placeholder_with_human_attribute_name_and_value
- with_locale :placeholder do
+ I18n.with_locale :placeholder do
assert_dom_equal(
%{<textarea id="post_cost" name="post[cost]" placeholder="Pounds">\n</textarea>},
text_area(:post, :cost, placeholder: :uk)
@@ -887,7 +906,7 @@ class FormHelperTest < ActionView::TestCase
end
def test_text_area_placeholder_with_locales_and_value
- with_locale :placeholder do
+ I18n.with_locale :placeholder do
assert_dom_equal(
%{<textarea id="post_written_on" name="post[written_on]" placeholder="Escrito en">\n2004-06-15</textarea>},
text_area(:post, :written_on, placeholder: :spanish)
@@ -896,8 +915,8 @@ class FormHelperTest < ActionView::TestCase
end
def test_text_area_placeholder_with_locales_and_nested_attributes
- with_locale :placeholder do
- form_for(@post, html: { id: 'create-post' }) do |f|
+ I18n.with_locale :placeholder do
+ form_for(@post, html: { id: "create-post" }) do |f|
f.fields_for(:comments) do |cf|
concat cf.text_area(:body, placeholder: true)
end
@@ -912,8 +931,8 @@ class FormHelperTest < ActionView::TestCase
end
def test_text_area_placeholder_with_locales_fallback_and_nested_attributes
- with_locale :placeholder do
- form_for(@post, html: { id: 'create-post' }) do |f|
+ I18n.with_locale :placeholder do
+ 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,76 +1157,60 @@ 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" />}
- assert_deprecated do
- assert_dom_equal(expected, datetime_field("post", "written_on"))
- end
+ 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_deprecated do
- assert_dom_equal(expected, datetime_field("post", "written_on"))
- end
+ 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)
step = 60
- assert_deprecated do
- assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step))
- end
+ assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step))
end
def test_datetime_field_with_value_attr
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2013-06-29T13:37:00+00:00" />}
- value = DateTime.new(2013,6,29,13,37)
- assert_deprecated do
- assert_dom_equal(expected, datetime_field("post", "written_on", value: value))
- end
+ 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')
- assert_deprecated do
- assert_dom_equal(expected, datetime_field("post", "written_on"))
- end
+ 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_deprecated do
- assert_dom_equal(expected, datetime_field("post", "written_on"))
- end
+ 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"
- assert_deprecated do
- assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
- end
+ 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"
- assert_deprecated do
- assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
- end
+ assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
end
def test_datetime_local_field
@@ -1215,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"))
@@ -1288,9 +1245,9 @@ 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
@@ -1323,9 +1280,9 @@ class FormHelperTest < ActionView::TestCase
end
def test_week_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="week" value="2004-W25" />}
- @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, week_field("post", "written_on"))
ensure
Time.zone = previous_time_zone
@@ -1434,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),
@@ -1484,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
@@ -1516,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>},
@@ -1526,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")
@@ -1550,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)
@@ -1562,52 +1519,84 @@ class FormHelperTest < ActionView::TestCase
def test_form_for_requires_block
error = assert_raises(ArgumentError) do
- form_for(@post, html: { id: 'create-post' })
+ 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.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' data-disable-with='Create post' type='submit' value='Create post' />" +
- "<button name='button' type='submit'>Create post</button>" +
+ "<label for='post_title'>The Title</label>" \
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" \
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" \
+ "<button name='button' type='submit'>Create post</button>" \
"<button name='button' type='submit'><span>Create post</span></button>"
end
assert_dom_equal expected, output_buffer
end
+ def test_form_for_is_not_affected_by_form_with_generates_ids
+ old_value = ActionView::Helpers::FormHelper.form_with_generates_ids
+ ActionView::Helpers::FormHelper.form_with_generates_ids = false
+
+ form_for(@post, html: { id: "create-post" }) do |f|
+ concat f.label(:title) { "The Title" }
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ concat f.submit("Create post")
+ concat f.button("Create post")
+ concat f.button {
+ concat content_tag(:span, "Create post")
+ }
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ "<label for='post_title'>The Title</label>" \
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \
+ "<input name='post[secret]' type='hidden' value='0' />" \
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" \
+ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" \
+ "<button name='button' type='submit'>Create post</button>" \
+ "<button name='button' type='submit'><span>Create post</span></button>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ ensure
+ ActionView::Helpers::FormHelper.form_with_generates_ids = old_value
+ end
+
def test_form_for_with_collection_radio_buttons
post = Post.new
def post.active; false; end
@@ -1616,10 +1605,10 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<input type='hidden' name='post[active]' value='' />" +
- "<input id='post_active_true' name='post[active]' type='radio' value='true' />" +
- "<label for='post_active_true'>true</label>" +
- "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
+ "<input type='hidden' name='post[active]' value='' />" \
+ "<input id='post_active_true' name='post[active]' type='radio' value='true' />" \
+ "<label for='post_active_true'>true</label>" \
+ "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" \
"<label for='post_active_false'>false</label>"
end
@@ -1638,12 +1627,12 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<input type='hidden' name='post[active]' value='' />" +
- "<label for='post_active_true'>"+
- "<input id='post_active_true' name='post[active]' type='radio' value='true' />" +
- "true</label>" +
- "<label for='post_active_false'>"+
- "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
+ "<input type='hidden' name='post[active]' value='' />" \
+ "<label for='post_active_true'>" \
+ "<input id='post_active_true' name='post[active]' type='radio' value='true' />" \
+ "true</label>" \
+ "<label for='post_active_false'>" \
+ "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" \
"false</label>"
end
@@ -1664,13 +1653,13 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post_1", "new_post") do
- "<input type='hidden' name='post[active]' value='' />" +
- "<label for='post_active_true'>"+
- "<input id='post_active_true' name='post[active]' type='radio' value='true' />" +
- "true</label>" +
- "<label for='post_active_false'>"+
- "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
- "false</label>"+
+ "<input type='hidden' name='post[active]' value='' />" \
+ "<label for='post_active_true'>" \
+ "<input id='post_active_true' name='post[active]' type='radio' value='true' />" \
+ "true</label>" \
+ "<label for='post_active_false'>" \
+ "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" \
+ "false</label>" \
"<input id='post_id' name='post[id]' type='hidden' value='1' />"
end
@@ -1681,15 +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 type='hidden' name='post[active]' value='' />" +
- "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" +
- "<label for='foo_post_active_true'>true</label>" +
- "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" +
+ "<input type='hidden' name='post[active]' value='' />" \
+ "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" \
+ "<label for='foo_post_active_true'>true</label>" \
+ "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" \
"<label for='foo_post_active_false'>false</label>"
end
@@ -1700,15 +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 type='hidden' name='post[1][active]' value='' />" +
- "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" +
- "<label for='post_1_active_true'>true</label>" +
- "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" +
+ "<input type='hidden' name='post[1][active]' value='' />" \
+ "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" \
+ "<label for='post_1_active_true'>true</label>" \
+ "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" \
"<label for='post_1_active_false'>false</label>"
end
@@ -1724,12 +1713,12 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<input name='post[tag_ids][]' type='hidden' value='' />" +
- "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
- "<label for='post_tag_ids_1'>Tag 1</label>" +
- "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" +
- "<label for='post_tag_ids_2'>Tag 2</label>" +
- "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" +
+ "<input name='post[tag_ids][]' type='hidden' value='' />" \
+ "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \
+ "<label for='post_tag_ids_1'>Tag 1</label>" \
+ "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" \
+ "<label for='post_tag_ids_2'>Tag 2</label>" \
+ "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" \
"<label for='post_tag_ids_3'>Tag 3</label>"
end
@@ -1748,15 +1737,15 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post", "new_post") do
- "<input name='post[tag_ids][]' type='hidden' value='' />" +
- "<label for='post_tag_ids_1'>" +
- "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
- "Tag 1</label>" +
- "<label for='post_tag_ids_2'>" +
- "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" +
- "Tag 2</label>" +
- "<label for='post_tag_ids_3'>" +
- "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" +
+ "<input name='post[tag_ids][]' type='hidden' value='' />" \
+ "<label for='post_tag_ids_1'>" \
+ "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \
+ "Tag 1</label>" \
+ "<label for='post_tag_ids_2'>" \
+ "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" \
+ "Tag 2</label>" \
+ "<label for='post_tag_ids_3'>" \
+ "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" \
"Tag 3</label>"
end
@@ -1778,16 +1767,16 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form("/posts", "new_post_1", "new_post") do
- "<input name='post[tag_ids][]' type='hidden' value='' />"+
- "<label for='post_tag_ids_1'>" +
- "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
- "Tag 1</label>" +
- "<label for='post_tag_ids_2'>" +
- "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" +
- "Tag 2</label>" +
- "<label for='post_tag_ids_3'>" +
- "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" +
- "Tag 3</label>" +
+ "<input name='post[tag_ids][]' type='hidden' value='' />" \
+ "<label for='post_tag_ids_1'>" \
+ "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \
+ "Tag 1</label>" \
+ "<label for='post_tag_ids_2'>" \
+ "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" \
+ "Tag 2</label>" \
+ "<label for='post_tag_ids_3'>" \
+ "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" \
+ "Tag 3</label>" \
"<input id='post_id' name='post[id]' type='hidden' value='1' />"
end
@@ -1799,13 +1788,13 @@ 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 name='post[tag_ids][]' type='hidden' value='' />" +
- "<input checked='checked' id='foo_post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
+ "<input name='post[tag_ids][]' type='hidden' value='' />" \
+ "<input checked='checked' id='foo_post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \
"<label for='foo_post_tag_ids_1'>Tag 1</label>"
end
@@ -1817,13 +1806,13 @@ 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 name='post[1][tag_ids][]' type='hidden' value='' />" +
- "<input checked='checked' id='post_1_tag_ids_1' name='post[1][tag_ids][]' type='checkbox' value='1' />" +
+ "<input name='post[1][tag_ids][]' type='hidden' value='' />" \
+ "<input checked='checked' id='post_1_tag_ids_1' name='post[1][tag_ids][]' type='checkbox' value='1' />" \
"<label for='post_1_tag_ids_1'>Tag 1</label>"
end
@@ -1831,22 +1820,18 @@ class FormHelperTest < ActionView::TestCase
end
def test_form_for_with_file_field_generate_multipart
- Post.send :attr_accessor, :file
-
- form_for(@post, html: { id: 'create-post' }) do |f|
+ 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)
@@ -1854,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
@@ -1865,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
@@ -1877,32 +1862,32 @@ 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='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' />" +
+ 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
@@ -1913,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
@@ -1941,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
@@ -1964,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
@@ -1972,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
@@ -2012,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
@@ -2038,10 +2051,10 @@ class FormHelperTest < ActionView::TestCase
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' />" +
+ 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
@@ -2050,16 +2063,16 @@ class FormHelperTest < ActionView::TestCase
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
@@ -2074,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
@@ -2092,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
@@ -2104,14 +2117,14 @@ 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>" +
+ 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
@@ -2122,14 +2135,14 @@ 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>" +
+ 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
@@ -2138,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
@@ -2151,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
@@ -2168,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
@@ -2190,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
@@ -2202,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
@@ -2228,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|
@@ -2237,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
@@ -2247,14 +2260,14 @@ class FormHelperTest < ActionView::TestCase
end
def test_submit_with_object_as_new_record_and_locale_strings
- with_locale :submit do
+ I18n.with_locale :submit do
@post.persisted = false
@post.stub(:to_key, nil) do
form_for(@post) do |f|
concat f.submit
end
- expected = whole_form('/posts', 'new_post', 'new_post') do
+ expected = whole_form("/posts", "new_post", "new_post") do
"<input name='commit' data-disable-with='Create Post' type='submit' value='Create Post' />"
end
@@ -2264,13 +2277,13 @@ class FormHelperTest < ActionView::TestCase
end
def test_submit_with_object_as_existing_record_and_locale_strings
- with_locale :submit do
+ I18n.with_locale :submit do
form_for(@post) do |f|
concat f.submit
end
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
- "<input name='commit' data-disable-with='Confirm Post changes' type='submit' value='Confirm Post changes' />"
+ 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
@@ -2278,27 +2291,42 @@ class FormHelperTest < ActionView::TestCase
end
def test_submit_without_object_and_locale_strings
- with_locale :submit do
+ I18n.with_locale :submit do
form_for(: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' />"
+ "<input name='commit' class='extra' data-disable-with='Save changes' type='submit' value='Save changes' />"
end
assert_dom_equal expected, output_buffer
end
end
- def test_submit_with_object_and_nested_lookup
- with_locale :submit do
+ def test_submit_with_object_which_is_overwritten_by_as_option
+ I18n.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' data-disable-with='Update your Post' 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)
+ I18n.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
@@ -2306,14 +2334,14 @@ 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
@@ -2323,10 +2351,10 @@ class FormHelperTest < ActionView::TestCase
def test_deep_nested_fields_for
@comment.save
form_for(:posts) do |f|
- f.fields_for('post[]', @post) do |f2|
+ f.fields_for("post[]", @post) do |f2|
f2.text_field(:id)
@post.comments.each do |comment|
- concat f2.fields_for('comment[]', comment) { |c|
+ concat f2.fields_for("comment[]", comment) { |c|
concat c.text_field(:name)
}
end
@@ -2340,18 +2368,19 @@ class FormHelperTest < ActionView::TestCase
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
@@ -2360,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
@@ -2380,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
@@ -2394,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
@@ -2408,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
@@ -2422,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
@@ -2436,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
@@ -2456,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
@@ -2475,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
@@ -2502,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
@@ -2521,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
@@ -2540,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
@@ -2558,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
@@ -2576,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
@@ -2596,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
@@ -2617,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
@@ -2644,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
@@ -2671,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
@@ -2697,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
@@ -2720,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
@@ -2744,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
@@ -2767,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
@@ -2788,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
@@ -2806,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
@@ -2823,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
@@ -2844,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
@@ -2858,7 +2887,7 @@ 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) }
- params = 11.times.map { ['post.comments.body', default: [:"comment.body", ''], scope: "helpers.label"] }
+ 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|
@@ -2879,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
@@ -2902,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
@@ -2917,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
@@ -2934,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
@@ -2957,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
@@ -2977,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
@@ -2991,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
@@ -3004,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
@@ -3014,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
@@ -3040,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)
@@ -3048,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
@@ -3075,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
@@ -3090,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
@@ -3106,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
@@ -3122,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
@@ -3138,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
@@ -3154,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
@@ -3170,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
@@ -3184,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
@@ -3195,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)
@@ -3214,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
@@ -3225,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)
@@ -3234,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
@@ -3250,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
@@ -3274,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
@@ -3293,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
@@ -3312,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
@@ -3353,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
@@ -3366,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
@@ -3377,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
@@ -3391,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
@@ -3405,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
@@ -3413,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
@@ -3459,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
@@ -3474,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
@@ -3495,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
@@ -3521,53 +3550,54 @@ 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]
- def hidden_fields(options = {})
- method = options[:method]
+ if options.fetch(:enforce_utf8, true)
+ txt = +%{<input name="utf8" type="hidden" value="&#x2713;" />}
+ else
+ txt = +""
+ end
- if options.fetch(:enforce_utf8, true)
- txt = %{<input name="utf8" type="hidden" value="&#x2713;" />}
- else
- txt = ''
- end
+ 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 = {})
+ contents = block_given? ? yield : ""
- def whole_form(action = "/", id = nil, html_class = nil, options = {})
- contents = block_given? ? yield : ""
+ method, remote, multipart = options.values_at(:method, :remote, :multipart)
- method, remote, multipart = options.values_at(:method, :remote, :multipart)
+ form_text(action, id, html_class, remote, multipart, method) + hidden_fields(options.slice :method, :enforce_utf8) + contents + "</form>"
+ end
- form_text(action, id, html_class, remote, multipart, method) + hidden_fields(options.slice :method, :enforce_utf8) + contents + "</form>"
- end
+ def protect_against_forgery?
+ false
+ 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 26ede09a5f..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,15 +16,15 @@ class FormOptionsHelperI18nTests < ActionView::TestCase
end
def test_select_with_prompt_true_translates_prompt_message
- assert_called_with(I18n, :translate, ['helpers.select.prompt', { :default => 'Please select' }]) do
- 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 c5b63d33f1..4ccd3ae336 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,20 +8,35 @@ 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)
+ 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
+ def =~(_re); end
end
module ClassMethods
@@ -57,6 +74,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(
@@ -66,44 +90,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
@@ -124,7 +148,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
@@ -147,8 +171,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
@@ -157,8 +181,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
@@ -171,37 +195,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
@@ -213,18 +237,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
@@ -259,64 +283,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
@@ -327,8 +351,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
@@ -336,9 +376,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
@@ -347,8 +387,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
@@ -357,116 +397,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
@@ -481,7 +521,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
@@ -495,9 +545,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)
)
@@ -513,9 +563,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)
)
@@ -561,7 +611,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
@@ -591,7 +641,7 @@ class FormOptionsHelperTest < ActionView::TestCase
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(
@@ -619,7 +669,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(
@@ -629,7 +679,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
@@ -637,7 +687,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
@@ -645,7 +695,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
@@ -653,7 +703,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
@@ -665,14 +715,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
@@ -681,7 +731,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
@@ -690,7 +740,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
@@ -699,7 +749,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
@@ -708,7 +758,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
@@ -717,7 +767,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
@@ -725,7 +775,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
@@ -734,16 +784,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
@@ -759,51 +818,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
@@ -812,7 +871,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
@@ -821,7 +880,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
@@ -833,14 +892,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
@@ -849,7 +908,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
@@ -858,7 +917,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
@@ -866,7 +925,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
@@ -874,7 +933,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
@@ -883,7 +942,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
@@ -896,6 +955,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"
@@ -924,7 +991,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
@@ -955,7 +1022,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
@@ -965,7 +1032,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
@@ -976,10 +1043,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
@@ -988,7 +1055,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
@@ -998,7 +1065,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
@@ -1022,13 +1089,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
@@ -1041,12 +1108,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
)
@@ -1055,17 +1122,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
)
@@ -1080,12 +1147,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
)
@@ -1093,28 +1160,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
@@ -1122,64 +1189,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
@@ -1192,13 +1259,13 @@ class FormOptionsHelperTest < ActionView::TestCase
end
html = time_zone_select("firm", "time_zone", /A|D/)
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>" +
- "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"E\">E</option>" +
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>" \
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
html
end
@@ -1213,42 +1280,61 @@ class FormOptionsHelperTest < ActionView::TestCase
end
html = time_zone_select("firm", "time_zone", /A|D/)
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \
+ "<option value=\"A\">A</option>\n" \
+ "<option value=\"B\">B</option>\n" \
+ "<option value=\"C\">C</option>\n" \
+ "<option value=\"D\" selected=\"selected\">D</option>\n" \
+ "<option value=\"E\">E</option>" \
"</select>",
html
end
- 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
@@ -1256,86 +1342,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>},
@@ -1348,7 +1434,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
@@ -1357,13 +1443,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)
@@ -1377,14 +1463,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 7b93c8dc29..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
@@ -65,20 +77,20 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_check_box_tag_disabled
- actual = check_box_tag "admin","1", false, disabled: true
+ 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
+ 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
@@ -88,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
@@ -150,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
@@ -165,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
@@ -173,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
@@ -183,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)
@@ -210,7 +267,7 @@ 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
@@ -235,12 +292,12 @@ class FormTagHelperTest < ActionView::TestCase
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", raw("<option>Home</option><option>Work</option><option>Pub</option>"), :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
@@ -251,37 +308,37 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_select_tag_with_include_blank_string
- actual = select_tag "places", raw("<option>Home</option><option>Work</option><option>Pub</option>"), 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", raw("<option>Home</option><option>Work</option><option>Pub</option>"), :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", raw("<option>Home</option><option>Work</option><option>Pub</option>"), :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", 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=""></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
@@ -293,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
@@ -340,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!" />)
@@ -352,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
@@ -370,20 +433,20 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_text_field_tag_with_placeholder_option
- actual = text_field_tag "title", "Hello!", placeholder: 'Enter search term...'
+ 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
@@ -412,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
@@ -425,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", 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)
+ 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
@@ -447,7 +510,7 @@ 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
@@ -471,42 +534,49 @@ class FormTagHelperTest < ActionView::TestCase
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?" })
+ 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?" })
+ 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 } })
+ 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?" data-disable-with="Save" />),
- submit_tag("Save", :data => { :confirm => "Are you sure?" })
+ 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?" })
+ 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
@@ -517,7 +587,6 @@ class FormTagHelperTest < ActionView::TestCase
)
end
-
def test_button_tag
assert_dom_equal(
%(<button name="button" type="submit">Button</button>),
@@ -528,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
@@ -590,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
@@ -621,10 +690,8 @@ class FormTagHelperTest < ActionView::TestCase
end
def test_datetime_field_tag
- expected = %{<input id="appointment" name="appointment" type="datetime" />}
- assert_deprecated do
- assert_dom_equal(expected, datetime_field_tag("appointment"))
- end
+ expected = %{<input id="appointment" name="appointment" type="datetime-local" />}
+ assert_dom_equal(expected, datetime_field_tag("appointment"))
end
def test_datetime_local_field_tag
@@ -654,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
@@ -695,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?
@@ -730,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..17f21cbbc5 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).format
end
- test 'formats returns string for recognized MIME type when MIME does not have symbol' do
- foo = Mime::Type.lookup("foo")
+ test "formats returns string for recognized MIME type when MIME does not have symbol" do
+ foo = Mime::Type.lookup("text/foo")
assert_nil foo.to_sym
- assert_equal ['foo'], ActionView::Template::HTML.new('', foo).formats
+ assert_equal "text/foo", ActionView::Template::HTML.new("", foo).format
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").format
end
end
diff --git a/actionview/test/template/javascript_helper_test.rb b/actionview/test/template/javascript_helper_test.rb
index 9f1535ef53..f974e5ae0c 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
+ # 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 7683444bf0..8b160a7336 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,11 +10,16 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
def setup
super
+
+ ActionView::LookupContext::DetailsKey.clear
+
view_paths = ActionController::Base.view_paths
+
lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"])
- renderer = ActionView::Renderer.new(lookup_context)
- @view = ActionView::Base.new(renderer, {})
+ @view = ActionView::Base.with_empty_template_cache.new(lookup_context, {})
+
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.
@@ -21,18 +28,43 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
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_root
+ Rails.instance_eval { undef :root } if defined?(@defined_root)
end
def set_logger(logger)
ActionView::Base.logger = logger
end
+ def set_cache_controller
+ controller = ActionController::Base.new
+ controller.perform_caching = true
+ controller.cache_store = ActiveSupport::Cache::MemoryStore.new
+ @view.controller = controller
+ end
+
+ 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_template_template
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ @view.render(template: "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
+
def test_render_file_template
Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
- @view.render(:file => "test/hello_world")
+ @view.render(file: "#{FIXTURE_LOAD_PATH}/test/hello_world.erb")
wait
assert_equal 2, @logger.logged(:info).size
@@ -43,7 +75,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
def test_render_text_template
Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
- @view.render(:text => "TEXT")
+ @view.render(plain: "TEXT")
wait
assert_equal 2, @logger.logged(:info).size
@@ -54,7 +86,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
def test_render_inline_template
Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
- @view.render(:inline => "<%= 'TEXT' %>")
+ @view.render(inline: "<%= 'TEXT' %>")
wait
assert_equal 2, @logger.logged(:info).size
@@ -63,29 +95,106 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
end
end
- def test_render_partial_template
+ def test_render_partial_with_implicit_path
Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
- @view.render(:partial => "test/customer")
+ @view.render(Customer.new("david"), greeting: "hi")
wait
assert_equal 1, @logger.logged(:info).size
- assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last)
+ assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last)
end
end
- def test_render_partial_with_implicit_path
+ def test_render_partial_with_cache_missed
Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
- @view.render(Customer.new("david"), :greeting => "hi")
+ 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 customers\/_customer\.html\.erb/, @logger.logged(:info).last)
+ assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache miss\]/, @logger.logged(:info).last)
+ end
+ end
+
+ 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
+
+ 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
Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
- @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ])
+ @view.render(partial: "test/customer", collection: [ Customer.new("david"), Customer.new("mary") ])
wait
assert_equal 1, @logger.logged(:info).size
@@ -95,7 +204,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
def test_render_collection_with_implicit_path
Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
- @view.render([ Customer.new("david"), Customer.new("mary") ], :greeting => "hi")
+ @view.render([ Customer.new("david"), Customer.new("mary") ], greeting: "hi")
wait
assert_equal 1, @logger.logged(:info).size
@@ -105,7 +214,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
def test_render_collection_template_without_path
Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
- @view.render([ GoodCustomer.new("david"), Customer.new("mary") ], :greeting => "hi")
+ @view.render([ GoodCustomer.new("david"), Customer.new("mary") ], greeting: "hi")
wait
assert_equal 1, @logger.logged(:info).size
@@ -115,11 +224,10 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
def test_render_collection_with_cached_set
Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
- def @view.view_cache_dependencies; []; end
- def @view.fragment_cache_key(*); 'ahoy `controller` dependency'; end
+ set_view_cache_dependencies
- @view.render(partial: 'customers/customer', collection: [ Customer.new('david'), Customer.new('mary') ], cached: true,
- locals: { greeting: 'hi' })
+ @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
diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb
index 2e3a3f9bae..3e357fe1a7 100644
--- a/actionview/test/template/lookup_context_test.rb
+++ b/actionview/test/template/lookup_context_test.rb
@@ -1,27 +1,41 @@
+# frozen_string_literal: true
+
require "abstract_unit"
require "abstract_controller/rendering"
class LookupContextTest < ActiveSupport::TestCase
def setup
- @lookup_context = ActionView::LookupContext.new(FIXTURE_LOAD_PATH, {})
+ @lookup_context = build_lookup_context(FIXTURE_LOAD_PATH, {})
ActionView::LookupContext::DetailsKey.clear
end
+ def build_lookup_context(paths, details)
+ ActionView::LookupContext.new(paths, details)
+ end
+
def teardown
I18n.locale = :en
end
- test "allows to override default_formats with ActionView::Base.default_formats" do
- begin
- formats = ActionView::Base.default_formats
- ActionView::Base.default_formats = [:foo, :bar]
+ test "rendered_format is deprecated" do
+ assert_deprecated do
+ @lookup_context.rendered_format = "foo"
+ end
- assert_equal [:foo, :bar], ActionView::LookupContext.new([]).default_formats
- ensure
- ActionView::Base.default_formats = formats
+ assert_deprecated do
+ assert_equal "foo", @lookup_context.rendered_format
end
end
+ test "allows to override default_formats with ActionView::Base.default_formats" do
+ formats = ActionView::Base.default_formats
+ ActionView::Base.default_formats = [:foo, :bar]
+
+ assert_equal [:foo, :bar], ActionView::LookupContext.new([]).default_formats
+ ensure
+ ActionView::Base.default_formats = formats
+ end
+
test "process view paths on initialization" do
assert_kind_of ActionView::PathSet, @lookup_context.view_paths
end
@@ -33,7 +47,7 @@ class LookupContextTest < ActiveSupport::TestCase
test "allows me to freeze and retrieve frozen formats" do
@lookup_context.formats.freeze
- assert @lookup_context.formats.frozen?
+ assert_predicate @lookup_context.formats, :frozen?
end
test "provides getters and setters for variants" do
@@ -53,7 +67,7 @@ class LookupContextTest < ActiveSupport::TestCase
test "handles explicitly defined */* formats fallback to :js" do
@lookup_context.formats = [:js, Mime::ALL]
- assert_equal [:js, *Mime::SET.symbols], @lookup_context.formats
+ assert_equal [:js, *Mime::SET.symbols].uniq, @lookup_context.formats
end
test "adds :html fallback to :js formats" do
@@ -61,6 +75,14 @@ class LookupContextTest < ActiveSupport::TestCase
assert_equal [:js, :html], @lookup_context.formats
end
+ test "raises on invalid format assignment" do
+ ex = assert_raises ArgumentError do
+ @lookup_context.formats = [:html, :invalid, "also bad"]
+ end
+
+ assert_equal 'Invalid formats: :invalid, "also bad"', ex.message
+ end
+
test "provides getters and setters for locale" do
@lookup_context.locale = :pt
assert_equal :pt, @lookup_context.locale
@@ -107,30 +129,43 @@ class LookupContextTest < ActiveSupport::TestCase
assert_equal "Hello texty phone!", template.source
end
- test "found templates respects given formats if one cannot be found from template or handler" do
+ test "found templates have nil format if one cannot be found from template or handler" do
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
+ assert_nil template.format
end
end
test "adds fallbacks to view paths when required" do
assert_equal 1, @lookup_context.view_paths.size
- @lookup_context.with_fallbacks do
- assert_equal 3, @lookup_context.view_paths.size
- assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new(""))
- assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new("/"))
+ assert_deprecated do
+ @lookup_context.with_fallbacks do
+ assert_equal 3, @lookup_context.view_paths.size
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("")
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/")
+ end
end
+
+ @lookup_context = @lookup_context.with_fallbacks
+
+ assert_equal 3, @lookup_context.view_paths.size
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("")
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/")
end
test "add fallbacks just once in nested fallbacks calls" do
- @lookup_context.with_fallbacks do
+ assert_deprecated do
@lookup_context.with_fallbacks do
- assert_equal 3, @lookup_context.view_paths.size
+ @lookup_context.with_fallbacks do
+ assert_equal 3, @lookup_context.view_paths.size
+ end
end
end
+
+ @lookup_context = @lookup_context.with_fallbacks.with_fallbacks
+ assert_equal 3, @lookup_context.view_paths.size
end
test "generates a new details key for each details hash" do
@@ -156,13 +191,13 @@ class LookupContextTest < ActiveSupport::TestCase
end
test "gives the key forward to the resolver, so it can be used as cache key" do
- @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo")
+ @lookup_context = build_lookup_context(ActionView::FixtureResolver.new("test/_foo.erb" => "Foo"), {})
template = @lookup_context.find("foo", %w(test), true)
assert_equal "Foo", template.source
# Now we are going to change the template, but it won't change the returned template
# since we will hit the cache.
- @lookup_context.view_paths.first.hash["test/_foo.erb"] = "Bar"
+ @lookup_context.view_paths.first.data["test/_foo.erb"] = "Bar"
template = @lookup_context.find("foo", %w(test), true)
assert_equal "Foo", template.source
@@ -185,7 +220,7 @@ class LookupContextTest < ActiveSupport::TestCase
end
test "can disable the cache on demand" do
- @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo")
+ @lookup_context = build_lookup_context(ActionView::FixtureResolver.new("test/_foo.erb" => "Foo"), {})
old_template = @lookup_context.find("foo", %w(test), true)
template = @lookup_context.find("foo", %w(test), true)
@@ -193,7 +228,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
@@ -208,56 +243,6 @@ class LookupContextTest < ActiveSupport::TestCase
end
end
-class LookupContextWithFalseCaching < ActiveSupport::TestCase
- def setup
- @resolver = ActionView::FixtureResolver.new("test/_foo.erb" => ["Foo", Time.utc(2000)])
- @lookup_context = ActionView::LookupContext.new(@resolver, {})
- end
-
- test "templates are always found in the resolver but timestamp is checked before being compiled" do
- 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
- 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)
- end
- end
- end
-
- test "if no template was cached in the first lookup, retrieval should work in the second call" do
- ActionView::Resolver.stub(:caching?, false) do
- @resolver.hash.clear
- 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
- end
- end
-end
-
class TestMissingTemplate < ActiveSupport::TestCase
def setup
@lookup_context = ActionView::LookupContext.new("/Path/to/views", {})
@@ -279,10 +264,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 ace3e950b8..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,23 +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 '₹ 12,30,000.00', number_to_currency(1230000, delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/, unit: '₹', format: "%u %n")
+ 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: ",")
@@ -43,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)
@@ -57,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)
@@ -71,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
@@ -116,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 8de0ae2f6f..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
@@ -26,10 +28,92 @@ class OutputSafetyHelperTest < ActionView::TestCase
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 b417d1ebfa..15c41051de 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -1,147 +1,187 @@
-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' }
- @view = Class.new(ActionView::Base) do
- def view_cache_dependencies; end
+ @assigns = { secret: "in the sauce" }
+
+ @view = Class.new(ActionView::Base.with_empty_template_cache) do
+ def view_cache_dependencies; []; end
- def fragment_cache_key(key)
- ActiveSupport::Cache.expand_cache_key(key, :views)
+ def combined_fragment_cache_key(key)
+ [ :views, key ]
end
- end.new(paths, @assigns)
+ end.with_view_paths(paths, @assigns)
- @controller_view = TestController.new.view_context
+ controller = TestController.new
+
+ @controller_view = controller.view_context_class.with_empty_template_cache.new(
+ controller.lookup_context,
+ controller.view_assigns,
+ controller)
# 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
end
+ def test_implicit_format_comes_from_parent_template
+ rendered_templates = JSON.parse(@controller_view.render(template: "test/mixing_formats"))
+ assert_equal({ "format" => "HTML",
+ "children" => ["XML", "HTML"] }, rendered_templates)
+ end
+
+ def test_implicit_format_comes_from_parent_template_cascading
+ rendered_templates = JSON.parse(@controller_view.render(template: "test/mixing_formats_deep"))
+ assert_equal({ "format" => "HTML",
+ "children" => [
+ { "format" => "XML", "children" => ["XML"] },
+ { "format" => "HTML", "children" => ["HTML"] },
+ ] }, rendered_templates)
+ end
+
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_template
+ assert_equal "Hello world!", @view.render(template: "test/hello_world")
end
+
def test_render_file
- assert_equal "Hello world!", @view.render(:file => "test/hello_world")
+ assert_equal "Hello world!", assert_deprecated { @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>", assert_deprecated { @view.render(file: "comments/empty", formats: [:html]) }
+ assert_match "<error>No Comment</error>", assert_deprecated { @view.render(file: "comments/empty", formats: [:xml]) }
+ assert_match "<error>No Comment</error>", assert_deprecated { @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])
- end
-
- def test_rendered_format_without_format
- @view.render(:inline => "test")
- assert_equal :html, @view.lookup_context.rendered_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 "<error>No Comment</error>", @view.render(template: "comments/empty", formats: :xml)
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 HTML 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, :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>", assert_deprecated { @view.render(file: "comments/empty", locale: [:de]) }
+ assert_equal "<h1>Kein Kommentar</h1>", assert_deprecated { @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", assert_deprecated { @view.render(file: "comments/empty", handlers: [:builder]) }
+ assert_equal "<h1>No Comment</h1>\n", assert_deprecated { @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(template: "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
+ def test_render_template_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(template: "test/hello_world")
ensure
@view.locale = old_locale
end
- def test_render_file_with_dashed_locale
+ def test_render_template_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(template: "test/hello_world")
ensure
@view.locale = old_locale
end
- def test_render_file_at_top_level
- assert_equal 'Elastica', @view.render(:file => '/shared')
+ def test_render_template_at_top_level
+ assert_equal "Elastica", @view.render(template: "/shared")
+ end
+
+ def test_render_file_with_full_path_no_extension
+ template_path = File.expand_path("../fixtures/test/hello_world", __dir__)
+ assert_equal "Hello world!", assert_deprecated { @view.render(file: template_path) }
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.erb", __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", assert_deprecated { @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", assert_deprecated { @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", assert_deprecated { @view.render(file: "test/dot.directory/render_file_with_ivar") }
end
def test_render_partial_from_default
@@ -149,90 +189,104 @@ module RenderTestCases
end
def test_render_outside_path
- assert File.exist?(File.join(File.dirname(__FILE__), '../../test/abstract_unit.rb'))
+ assert File.exist?(File.expand_path("../../test/abstract_unit.rb", __dir__))
assert_raises ActionView::MissingTemplate do
- @view.render(:template => "../\\../test/abstract_unit.rb")
+ @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_template_with_syntax_error
+ e = assert_raises(ActionView::Template::Error) { @view.render(template: "test/syntax_error") }
+ assert_match %r!syntax!, e.message
+ assert_equal "1: <%= foo(", e.annotated_source_code[0].strip
+ 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[0].strip
+ assert_equal "1: <%= doesnt_exist %>", e.annotated_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
+ e = assert_raises(ActionView::Template::Error) { @view.render(partial: "test/raise_indentation") }
+ error_lines = e.annotated_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
@@ -240,126 +294,164 @@ 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) { assert_deprecated { @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[0].strip
+ assert_equal "1: <%= doesnt_exist %>", e.annotated_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 "FalseClass", @view.render(:partial => "test/klass", :object => false)
- assert_equal "NilClass", @view.render(:partial => "test/klass", :object => nil)
+ 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") ])
+ @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_deprecated_constructor
+ assert_deprecated do
+ ActionView::Base.new
+ end
+
+ assert_deprecated do
+ ActionView::Base.new ["/a"]
+ end
+
+ assert_deprecated do
+ ActionView::Base.new ActionView::PathSet.new ["/a"]
+ end
+ end
+
+ def test_without_compiled_method_container_is_deprecated
+ view = ActionView::Base.with_view_paths(ActionController::Base.view_paths)
+ assert_deprecated("ActionView::Base instances must implement `compiled_method_container`") do
+ assert_equal "Hello world!", view.render(template: "test/hello_world")
+ end
end
def test_render_partial_without_object_does_not_put_partial_name_to_local_assigns
- assert_equal 'false', @view.render(partial: 'test/partial_name_in_local_assigns')
+ 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
@@ -374,44 +466,60 @@ module RenderTestCases
assert_equal :partial_name_local_variable, exception.cause.name
end
- # TODO: The reason for this test is unclear, improve documentation
- def test_render_partial_and_fallback_to_layout
- 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 << 'source: #{template.source.inspect}'\n"
+ CustomHandler = lambda do |template, source|
+ "@output_buffer = ''.dup\n" \
+ "@output_buffer << 'source: #{source.inspect}'\n"
end
def test_render_inline_with_render_from_to_proc
- ActionView::Template.register_template_handler :ruby_handler, :source.to_proc
- assert_equal '3', @view.render(inline: "(1 + 2).to_s", type: :ruby_handler)
+ ActionView::Template.register_template_handler :ruby_handler, lambda { |_, source| source }
+ assert_equal "3", @view.render(inline: "(1 + 2).to_s", type: :ruby_handler)
+ ensure
+ ActionView::Template.unregister_template_handler :ruby_handler
+ end
+
+ def test_render_inline_with_render_from_to_proc_deprecated
+ assert_deprecated do
+ ActionView::Template.register_template_handler :ruby_handler, :source.to_proc
+ end
+ assert_equal "3", @view.render(inline: "(1 + 2).to_s", type: :ruby_handler)
+ ensure
+ ActionView::Template.unregister_template_handler :ruby_handler
+ end
+
+ def test_optional_second_arg_works_without_deprecation
+ assert_not_deprecated do
+ ActionView::Template.register_template_handler :ruby_handler, ->(view, source = nil) { source }
+ end
+ assert_equal "3", @view.render(inline: "(1 + 2).to_s", type: :ruby_handler)
ensure
ActionView::Template.unregister_template_handler :ruby_handler
end
@@ -431,17 +539,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
@@ -456,57 +564,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(template: "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(template: "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(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),
- @view.render(:file => "test/hello_world", :layout => "layouts/yield_with_render_partial_inside")
+ @view.render(template: "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")
+ @view.render(template: "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
@@ -516,42 +622,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(template: "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(template: "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(template: "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
@@ -566,6 +672,7 @@ class CachedViewRenderTest < ActiveSupport::TestCase
# Ensure view path cache is primed
def setup
+ ActionView::LookupContext::DetailsKey.clear
view_paths = ActionController::Base.view_paths
assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class
setup_view(view_paths)
@@ -583,6 +690,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase
# Test the same thing as above, but make sure the view path
# is not eager loaded
def setup
+ ActionView::LookupContext::DetailsKey.clear
path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
view_paths = ActionView::PathSet.new([path])
assert_equal ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH), view_paths.first
@@ -596,7 +704,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(template: "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
@@ -604,7 +712,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(template: "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
@@ -612,15 +720,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.cause.message
+ e = assert_raises(ActionView::Template::Error) { @view.render(template: "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.cause.message
+ e = assert_raises(ActionView::Template::Error) { @view.render(template: "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
@@ -640,6 +748,8 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase
# Ensure view path cache is primed
setup do
+ ActionView::LookupContext::DetailsKey.clear
+
view_paths = ActionController::Base.view_paths
assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class
@@ -649,15 +759,22 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase
end
teardown do
- GC.start
I18n.reload!
end
+ test "template body written to cache" do
+ customer = Customer.new("david", 1)
+ key = cache_key(customer, "test/_customer")
+ assert_nil ActionView::PartialRenderer.collection_cache.read(key)
+ @view.render(partial: "test/customer", collection: [customer], cached: true)
+ assert_equal "Hello: david", ActionView::PartialRenderer.collection_cache.read(key)
+ end
+
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, 'Cached')
+ ActionView::PartialRenderer.collection_cache.write(key, "Cached")
assert_not_equal "Cached",
@view.render(partial: "test/customer", collection: [customer])
@@ -667,7 +784,7 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase
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/customer", collection: [customer], cached: true)
@@ -677,15 +794,26 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase
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", collection: [customer], cached: true)
end
+ test "collection caching does not work on multi-partials" do
+ a = Object.new
+ b = Object.new
+ def a.to_partial_path; "test/partial_iteration_1"; end
+ def b.to_partial_path; "test/partial_iteration_2"; end
+
+ assert_raises(NotImplementedError) do
+ @controller_view.render(partial: [a, b], cached: true)
+ end
+ end
+
private
def cache_key(*names, virtual_path)
- digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: []
- @view.fragment_cache_key([ *names, digest ])
+ digest = ActionView::Digestor.digest name: virtual_path, format: :html, finder: @view.lookup_context, dependencies: []
+ @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..90b61a2aa1
--- /dev/null
+++ b/actionview/test/template/resolver_cache_test.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class ResolverCacheTest < ActiveSupport::TestCase
+ def test_inspect_shields_cache_internals
+ ActionView::LookupContext::DetailsKey.clear
+ assert_match %r(#<ActionView::Resolver:0x[0-9a-f]+ @cache=#<ActionView::Resolver::Cache:0x[0-9a-f]+ keys=0 queries=0>>), ActionView::Resolver.new.inspect
+ end
+end
diff --git a/actionview/test/template/resolver_patterns_test.rb b/actionview/test/template/resolver_patterns_test.rb
index 1a091bd692..22815c8dbe 100644
--- a/actionview/test/template/resolver_patterns_test.rb
+++ b/actionview/test/template/resolver_patterns_test.rb
@@ -1,27 +1,32 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class ResolverPatternsTest < ActiveSupport::TestCase
def setup
- path = File.expand_path("../../fixtures/", __FILE__)
+ path = File.expand_path("../fixtures", __dir__)
pattern = ":prefix/{:formats/,}:action{.:formats,}{+:variants,}{.:handlers,}"
- @resolver = ActionView::FileSystemResolver.new(path, pattern)
+
+ assert_deprecated do
+ @resolver = ActionView::FileSystemResolver.new(path, pattern)
+ end
end
def test_should_return_empty_list_for_unknown_path
- templates = @resolver.find_all("unknown", "custom_pattern", false, {locale: [], formats: [:html], variants: [], 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], variants: [], 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
- assert_equal [:html], templates.first.formats
+ assert_nil templates.first.format
end
def test_should_return_all_templates_when_ambiguous_pattern
- templates = @resolver.find_all("another", "custom_pattern", false, {locale: [], formats: [:html], variants: [], 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
@@ -30,7 +35,7 @@ class ResolverPatternsTest < ActiveSupport::TestCase
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]})
+ 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
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..a5e673e71e 100644
--- a/actionview/test/template/streaming_render_test.rb
+++ b/actionview/test/template/streaming_render_test.rb
@@ -1,13 +1,18 @@
-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
+ ActionView::LookupContext::DetailsKey.clear
+
view_paths = ActionController::Base.view_paths
- @assigns = { :secret => 'in the sauce', :name => nil }
- @view = ActionView::Base.new(view_paths, @assigns)
+
+ @assigns = { secret: "in the sauce", name: nil }
+ @view = ActionView::Base.with_empty_template_cache.with_view_paths(view_paths, @assigns)
@controller_view = TestController.new.view_context
end
@@ -17,16 +22,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 +47,68 @@ class FiberedTest < ActiveSupport::TestCase
end
def test_render_file
- assert_equal "Hello world!", buffered_render(:file => "test/hello_world")
+ assert_equal "Hello world!", assert_deprecated { 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", assert_deprecated { 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 +116,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 f3956a31f6..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"])
+ assert_equal "<p class=\"song play\">limelight</p>", str
+
+ 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 = content_tag('p', "limelight", :class => ["song", "play"])
+ str = tag.p "limelight", class: ["song", "play"]
assert_equal "<p class=\"song play\">limelight</p>", str
- str = content_tag('p', "limelight", :class => ["song", ["play"]])
+ 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,51 +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>', raw('play>')])
- 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 => [raw('song>'), '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
+ 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 54c1d53b60..c4dc88e4aa 100644
--- a/actionview/test/template/template_error_test.rb
+++ b/actionview/test/template/template_error_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "abstract_unit"
class TemplateErrorTest < ActiveSupport::TestCase
diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb
index 533c1c3219..71fb99115b 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"
@@ -16,8 +18,9 @@ class TestERBTemplate < ActiveSupport::TestCase
attr_accessor :formats
end
- class Context
- def initialize
+ class Context < ActionView::Base
+ def initialize(*)
+ super
@output_buffer = "original"
@virtual_path = nil
end
@@ -35,7 +38,9 @@ class TestERBTemplate < ActiveSupport::TestCase
"<%= @virtual_path %>",
"partial",
ERBHandler,
- :virtual_path => "partial"
+ virtual_path: "partial",
+ format: :html,
+ locals: []
)
end
@@ -52,8 +57,9 @@ class TestERBTemplate < ActiveSupport::TestCase
end
end
- def new_template(body = "<%= hello %>", details = { format: :html })
- ActionView::Template.new(body, "hello template", details.fetch(:handler) { ERBHandler }, {:virtual_path => "hello"}.merge!(details))
+ def new_template(body = "<%= hello %>", details = {})
+ details = { format: :html, locals: [] }.merge details
+ ActionView::Template.new(body.dup, "hello template", details.delete(:handler) || ERBHandler, { virtual_path: "hello" }.merge!(details))
end
def render(locals = {})
@@ -61,7 +67,8 @@ class TestERBTemplate < ActiveSupport::TestCase
end
def setup
- @context = Context.new
+ @context = Context.with_empty_template_cache.empty
+ super
end
def test_basic_template
@@ -80,7 +87,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,15 +98,14 @@ 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
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")
+ @template = new_template("<%= my_local %>", locals: [:my_local])
+ assert_equal "I am a local", render(my_local: "I am a local")
end
def test_restores_buffer
@@ -116,23 +122,21 @@ class TestERBTemplate < ActiveSupport::TestCase
end
def test_refresh_with_templates
- @template = new_template("Hello", :virtual_path => "test/foo/bar")
- @template.locals = [:key]
- assert_called_with(@context.lookup_context, :find_template,["bar", %w(test/foo), false, [:key]], returns: "template") do
+ @template = new_template("Hello", virtual_path: "test/foo/bar", locals: [:key])
+ 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.locals = [:key]
- assert_called_with(@context.lookup_context, :find_template,[ "foo", %w(test), true, [:key]], returns: "partial") do
+ @template = new_template("Hello", virtual_path: "test/_foo", locals: [:key])
+ 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
@@ -171,14 +175,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
@@ -186,10 +190,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)
+ # 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)
@@ -200,4 +213,14 @@ class TestERBTemplate < ActiveSupport::TestCase
ensure
silence_warnings { Encoding.default_external = old }
end
+
+ def test_short_identifier
+ @template = new_template("hello")
+ assert_equal "hello template", @template.short_identifier
+ end
+
+ def test_template_inspect
+ @template = new_template("hello")
+ assert_equal "#<ActionView::Template hello template locals=[]>", @template.inspect
+ end
end
diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb
index d69d5819b6..0b2a2a9911 100644
--- a/actionview/test/template/test_case_test.rb
+++ b/actionview/test/template/test_case_test.rb
@@ -1,20 +1,21 @@
-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
@@ -23,11 +24,16 @@ module ActionView
DeveloperStruct = Struct.new(:name)
module SharedTests
+ def setup
+ ActionView::LookupContext::DetailsKey.clear
+ super
+ end
+
def self.included(test_case)
test_case.class_eval do
test "helpers defined on ActionView::TestCase are available" do
- 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
@@ -51,22 +57,22 @@ module ActionView
end
test "retrieve non existing config values" do
- assert_equal nil, ActionView::Base.new.config.something_odd
+ assert_nil ActionView::Base.empty.config.something_odd
end
test "works without testing a helper module" do
- assert_equal 'Eloy', render('developers/developer', :developer => DeveloperStruct.new('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
@@ -86,7 +92,7 @@ module ActionView
end
test "uses controller lookup context" do
- assert_equal self.lookup_context, @controller.lookup_context
+ assert_equal lookup_context, @controller.lookup_context
end
end
@@ -97,44 +103,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 = [DeveloperStruct.new('Eloy'), DeveloperStruct.new('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
@@ -148,21 +154,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
@@ -174,10 +180,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
@@ -191,9 +197,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
@@ -202,30 +207,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', {}, '/foo', [])
- 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
@@ -236,7 +247,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
@@ -244,7 +255,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
@@ -257,21 +270,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 => DeveloperStruct.new('Eloy'))
- assert_equal 'Eloy', render(:partial => 'developers/developer',
- :locals => { :developer => DeveloperStruct.new('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 = [DeveloperStruct.new('Eloy'), DeveloperStruct.new('Manfred')]
- assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list'))
+ @customers = [DeveloperStruct.new("Eloy"), DeveloperStruct.new("Manfred")]
+ assert_match(/Hello: EloyHello: Manfred/, render(template: "test/list"))
end
test "is able to render partials from templates and also use instance variables after view has been referenced" do
@@ -279,37 +292,44 @@ module ActionView
view
- @customers = [DeveloperStruct.new('Eloy'), DeveloperStruct.new('Manfred')]
- assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list'))
+ @customers = [DeveloperStruct.new("Eloy"), DeveloperStruct.new("Manfred")]
+ assert_match(/Hello: EloyHello: Manfred/, render(template: "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 e1ff639979..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,7 +43,7 @@ class PeopleHelperTest < ActionView::TestCase
extend ActiveModel::Naming
def to_model; self; end
def persisted?; true; end
- def self.name; 'Minitest::Mock'; end
+ def self.name; "Minitest::Mock"; end
}.new "David"
the_model = nil
@@ -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..afb6686dac 100644
--- a/actionview/test/template/testing/fixture_resolver_test.rb
+++ b/actionview/test/template/testing/fixture_resolver_test.rb
@@ -1,18 +1,20 @@
-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
- assert_equal [:html], templates.first.formats
+ assert_nil templates.first.format
end
end
diff --git a/actionview/test/template/testing/null_resolver_test.rb b/actionview/test/template/testing/null_resolver_test.rb
index 55ec36e753..c7c78804c0 100644
--- a/actionview/test/template/testing/null_resolver_test.rb
+++ b/actionview/test/template/testing/null_resolver_test.rb
@@ -1,12 +1,14 @@
-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
- assert_equal [:html], templates.first.formats
+ assert_nil templates.first.format
end
end
diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb
index 03c7597505..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,14 +34,14 @@ 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
@@ -47,20 +49,20 @@ class TextHelperTest < ActionView::TestCase
end
def test_simple_format_should_sanitize_input_when_sanitize_option_is_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)
+ 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,11 +191,11 @@ 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
@@ -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,89 +280,93 @@ 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
@@ -373,12 +379,14 @@ class TextHelperTest < ActionView::TestCase
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"))
@@ -394,7 +402,7 @@ class TextHelperTest < ActionView::TestCase
I18n.locale = :de
ActiveSupport::Inflector.inflections(:de) do |inflect|
- inflect.irregular 'region', 'regionen'
+ inflect.irregular "region", "regionen"
end
assert_equal("1 region", pluralize(1, "region"))
@@ -405,12 +413,6 @@ class TextHelperTest < ActionView::TestCase
end
end
- def test_deprecated_plural_as_positional_argument
- assert_deprecated do
- pluralize(2, 'count', 'counters')
- end
- end
-
def test_cycle_class
value = Cycle.new("one", 2, "3")
assert_equal("one", value.to_s)
@@ -458,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
@@ -490,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
@@ -518,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..c837c53587 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 "format always return :text" do
+ assert_equal :text, ActionView::Template::Text.new("").format
+ 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 38b9284767..9afdc3c68f 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,26 +17,29 @@ 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>"
}
}
)
- @view = ::ActionView::Base.new(ActionController::Base.view_paths, {})
+ view_paths = ActionController::Base.view_paths
+ view_paths.each(&:clear_cache)
+ ActionView::LookupContext.fallbacks.each(&:clear_cache)
+ @view = ::ActionView::Base.with_empty_template_cache.with_view_paths(view_paths, {})
end
teardown do
@@ -42,8 +47,8 @@ class TranslationHelperTest < ActiveSupport::TestCase
end
def test_delegates_setting_to_i18n
- assert_called_with(I18n, :translate, [:foo, :locale => 'en', :raise => true], returns: "") do
- translate :foo, :locale => 'en'
+ assert_called_with(I18n, :translate, [:foo, locale: "en", raise: true], returns: "") do
+ translate :foo, locale: "en"
end
end
@@ -58,7 +63,7 @@ class TranslationHelperTest < ActiveSupport::TestCase
old_value = ActionView::Base.debug_missing_translation
ActionView::Base.debug_missing_translation = false
- expected = 'translation missing: en.translations.missing'
+ expected = "translation missing: en.translations.missing"
assert_equal expected, translate(:"translations.missing")
ensure
ActionView::Base.debug_missing_translation = old_value
@@ -73,15 +78,15 @@ class TranslationHelperTest < ActiveSupport::TestCase
def test_returns_missing_translation_message_with_unescaped_interpolation
expected = '<span class="translation_missing" title="translation missing: en.translations.missing, name: Kir, year: 2015, vulnerable: &amp;quot; onclick=&amp;quot;alert()&amp;quot;">Missing</span>'
assert_equal expected, translate(:"translations.missing", name: "Kir", year: "2015", vulnerable: %{" onclick="alert()"})
- assert translate(:"translations.missing").html_safe?
+ assert_predicate translate(:"translations.missing"), :html_safe?
end
def test_returns_missing_translation_message_does_filters_out_i18n_options
expected = '<span class="translation_missing" title="translation missing: en.translations.missing, year: 2015">Missing</span>'
- assert_equal expected, translate(:"translations.missing", year: '2015', default: [])
+ 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))
+ assert_equal expected, translate(:"translations.missing", year: "2015", scope: %i(scoped))
end
def test_raises_missing_translation_message_with_raise_config_option
@@ -96,14 +101,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
@@ -111,7 +116,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
@@ -122,20 +127,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(template: "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(template: "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(template: "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(template: "translations/templates/missing").strip
end
def test_translate_does_not_mark_plain_text_as_safe_html
@@ -143,37 +148,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
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>"))
+ 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?
@@ -181,31 +189,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
@@ -214,13 +222,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 ab56d80de3..632b32f09f 100644
--- a/actionview/test/template/url_helper_test.rb
+++ b/actionview/test/template/url_helper_test.rb
@@ -1,15 +1,15 @@
-require 'abstract_unit'
+# 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
@@ -17,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
@@ -43,62 +47,88 @@ class UrlHelperTest < ActiveSupport::TestCase
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)
+ assert_equal "javascript:history.back()", url_for(:back)
end
def test_url_for_with_back_and_javascript_referer
- referer = 'javascript:alert(document.cookie)'
+ 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)
+ assert_equal "javascript:history.back()", url_for(:back)
end
def test_url_for_with_invalid_referer
- referer = 'THIS IS NOT A URL'
+ 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)
+ 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')
+ [{ 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' })
+ [{ 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'])
+ [{ 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')
+ [{ name: "country[name]", value: "Denmark" }],
+ to_form_params({ name: "Denmark" }, "country")
)
end
+ def test_button_to_without_protect_against_forgery_method
+ self.class.undef_method(:protect_against_forgery?)
+ assert_dom_equal(
+ %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>},
+ button_to("Hello", "http://www.example.com")
+ )
+ ensure
+ self.class.define_method(:protect_against_forgery?) { request_forgery }
+ end
+
def test_button_to_with_straight_url
assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com")
end
@@ -122,11 +152,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
@@ -211,7 +241,7 @@ 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
@@ -222,17 +252,48 @@ class UrlHelperTest < ActiveSupport::TestCase
)
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' } })
+ 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'] })
+ button_to("Hello", "http://www.example.com", params: { foo: ["bar"] })
)
end
@@ -241,13 +302,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
@@ -261,15 +322,15 @@ 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
@@ -329,7 +390,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
@@ -350,14 +411,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
@@ -371,24 +432,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
@@ -452,7 +513,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
@@ -470,6 +531,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")
@@ -477,22 +553,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
@@ -500,7 +576,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
@@ -521,7 +609,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")
@@ -564,8 +652,8 @@ class UrlHelperTest < ActiveSupport::TestCase
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%7E@example.org">#!$%&amp;&#39;*+-/=?^_`{}|~@example.org</a>},
- mail_to("#!$%&'*+-/=?^_`{}|~@example.org")
+ %{<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
@@ -577,13 +665,13 @@ 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', raw('<img src="/feedback.png" />'))
+ mail_to("feedback@example.com", raw('<img src="/feedback.png" />'))
end
def test_mail_to_with_html_safe_string
@@ -601,27 +689,27 @@ class UrlHelperTest < ActiveSupport::TestCase
end
def test_mail_to_returns_html_safe_string
- assert mail_to("david@loudthinking.com").html_safe?
+ assert_predicate mail_to("david@loudthinking.com"), :html_safe?
end
def test_mail_to_with_block
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(*args)
@@ -631,46 +719,39 @@ class UrlHelperTest < ActiveSupport::TestCase
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
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
@@ -685,88 +766,93 @@ class UrlHelperControllerTest < ActionController::TestCase
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
+ 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
@@ -778,9 +864,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
@@ -788,6 +874,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
@@ -820,7 +911,7 @@ class Session
end
class WorkshopsController < ActionController::Base
- test_routes do
+ ROUTES = test_routes do
resources :workshops do
resources :sessions
end
@@ -835,10 +926,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
@@ -865,6 +961,11 @@ class SessionsController < ActionController::Base
end
class PolymorphicControllerTest < ActionController::TestCase
+ def setup
+ super
+ @routes = WorkshopsController::ROUTES
+ end
+
def test_new_resource
@controller = WorkshopsController.new
@@ -879,6 +980,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
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..fb033491f9
--- /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 document should be parsed', 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 instanceof HTMLDocument, 'returned data should be an HTML document')
+ })
+})
+
+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..10b8870171
--- /dev/null
+++ b/actionview/test/ujs/public/test/data-disable-with.js
@@ -0,0 +1,434 @@
+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('a[data-remote][data-disable-with] within a form disables and re-enables', 6, function() {
+ var form = $('form:not([data-remote])'),
+ link = $('<a data-remote="true" data-disable-with="clicking...">Click me</a>')
+ form.append(link)
+
+ 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')
+ link.remove()
+ start()
+ }, 15)
+ })
+ .triggerNative('click')
+})
+
+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 disable 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('right/mouse-wheel-clicking on a link does not disable the link', 10, function() {
+ var link = $('a[data-disable-with]')
+
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click', { button: 1 })
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click', { button: 1 })
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click', { button: 2 })
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click', { button: 2 })
+ 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..9f84c4647e
--- /dev/null
+++ b/actionview/test/ujs/public/test/data-disable.js
@@ -0,0 +1,358 @@
+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('right/mouse-wheel-clicking on a link does not disable the link', 10, function() {
+ var link = $('a[data-disable]')
+
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click', { button: 1 })
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click', { button: 1 })
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click', { button: 2 })
+ App.checkEnabledState(link, 'Click me')
+
+ link.triggerNative('click', { button: 2 })
+ 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..9e41067549
--- /dev/null
+++ b/actionview/test/ujs/public/test/data-remote.js
@@ -0,0 +1,479 @@
+(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('right/mouse-wheel-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', { button: 1 })
+ link.triggerNative('click', { button: 2 })
+
+ 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('right/mouse-wheel-clicking on a button with data-remote attribute does not fire ajaxyness', 0, function() {
+ var button = $('button[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.
+ button
+ .removeAttr('data-params')
+ .bindNative('ajax:beforeSend', function() {
+ ok(false, 'ajax should not be triggered')
+ })
+
+ button.triggerNative('click', { button: 1 })
+ button.triggerNative('click', { button: 2 })
+
+ setTimeout(function() { start() }, 13)
+})
+
+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..ff2b057012
--- /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, options.button, 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..d7a6271587
--- /dev/null
+++ b/actionview/test/ujs/server.rb
@@ -0,0 +1,92 @@
+# 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.hosts << proc { true }
+
+ 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 efe46ce5ab..138c8a8ef4 100644
--- a/activejob/CHANGELOG.md
+++ b/activejob/CHANGELOG.md
@@ -1,160 +1,136 @@
-* Enable class reloading prior to job dispatch, and ensure Active Record
- connections are returned to the pool when jobs are run in separate threads.
+* Make job argument assertions with `Time`, `ActiveSupport::TimeWithZone`, and `DateTime` work by dropping microseconds. Microsecond precision is lost during serialization.
- *Matthew Draper*
+ *Gannon McGibbon*
-* Tune the async adapter for low-footprint dev/test usage. Use a single
- thread pool for all queues and limit to 0 to #CPU total threads, down from
- 2 to 10*#CPU per queue.
- *Jeremy Daer*
+## Rails 6.0.0.beta3 (March 11, 2019) ##
+* No changes.
-## Rails 5.0.0.beta3 (February 24, 2016) ##
-* Change the default adapter from inline to async. It's a better default as tests will then not mistakenly
- come to rely on behavior happening synchronously. This is especially important with things like jobs kicked off
- in Active Record lifecycle callbacks.
+## Rails 6.0.0.beta2 (February 25, 2019) ##
- *DHH*
+* No changes.
-## Rails 5.0.0.beta2 (February 01, 2016) ##
+## Rails 6.0.0.beta1 (January 18, 2019) ##
-* No changes.
+* Return false instead of the job instance when `enqueue` is aborted.
+ This will be the behavior in Rails 6.1 but it can be controlled now with
+ `config.active_job.return_false_on_aborted_enqueue`.
-## Rails 5.0.0.beta1 (December 18, 2015) ##
+ *Kir Shatrov*
-* Fixed serializing `:at` option for `assert_enqueued_with`
- and `assert_performed_with`.
+* Keep executions for each specific declaration
- *Wojciech Wnętrzak*
+ Each `retry_on` declaration has now its own specific executions counter. Before it was
+ shared between all executions of a job.
-* Support passing array to `assert_enqueued_jobs` in `:only` option.
+ *Alberto Almagro*
- *Wojciech Wnętrzak*
+* Allow all assertion helpers that have a `only` and `except` keyword to accept
+ Procs.
-* Add job priorities to Active Job.
+ *Edouard Chin*
- *wvengen*
+* Restore `HashWithIndifferentAccess` support to `ActiveJob::Arguments.deserialize`.
-* Implement a simple `AsyncJob` processor and associated `AsyncAdapter` that
- queue jobs to a `concurrent-ruby` thread pool.
+ *Gannon McGibbon*
- *Jerry D'Antonio*
+* Include deserialized arguments in job instances returned from
+ `assert_enqueued_with` and `assert_performed_with`
-* Implement `provider_job_id` for `queue_classic` adapter. This requires the
- latest, currently unreleased, version of queue_classic.
+ *Alan Wu*
- *Yves Senn*
+* 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.
-* `assert_enqueued_with` and `assert_performed_with` now returns the matched
- job instance for further assertions.
+ *Edouard Chin*
- *Jean Boussier*
+* `ActionDispatch::IntegrationTest` includes `ActiveJob::TestHelper` module by default.
-* Include `I18n.locale` into job serialization/deserialization and use it around
- `perform`.
+ *Ricardo Díaz*
- Fixes #20799.
+* Added `enqueue_retry.active_job`, `retry_stopped.active_job`, and `discard.active_job` hooks.
- *Johannes Opper*
+ *steves*
-* Allow `DelayedJob`, `Sidekiq`, `qu`, and `que` to report the job id back to
- `ActiveJob::Base` as `provider_job_id`.
+* Allow `assert_performed_with` to be called without a block.
- Fixes #18821.
+ *bogdanvlviv*
- *Kevin Deisz*, *Jeroen van Baarsen*
+* Execution of `assert_performed_jobs`, and `assert_no_performed_jobs`
+ without a block should respect passed `:except`, `:only`, and `:queue` options.
-* `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.
+ *bogdanvlviv*
- *y-yagi*
+* Allow `:queue` option to job assertions and helpers.
-* A generated job now inherits from `app/jobs/application_job.rb` by default.
+ *bogdanvlviv*
- *Jeroen van Baarsen*
+* Allow `perform_enqueued_jobs` to be called without a block.
-* Add an `:only` option to `perform_enqueued_jobs` to filter jobs based on
- type.
+ Performs all of the jobs that have been enqueued up to this point in the test.
- This allows specific jobs to be tested, while preventing others from
- being performed unnecessarily.
+ *Kevin Deisz*
- Example:
+* Move `enqueue`/`enqueue_at` notifications to an around callback.
+
+ Improves timing accuracy over the old after callback by including
+ time spent writing to the adapter's IO implementation.
- def test_hello_job
- assert_performed_jobs 1, only: HelloJob do
- HelloJob.perform_later('jeremy')
- LoggingJob.perform_later
- end
- end
+ *Zach Kemp*
- An array may also be specified, to support testing multiple jobs.
+* 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')
- 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
+ MyJob.set(wait_until: Date.tomorrow.noon).perform_later
+ assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon)
+ end
+ ```
- Fixes #18802.
+ *bogdanvlviv*
- *Michael Ryan*
+* Allow passing multiple exceptions to `retry_on`, and `discard_on`.
-* Allow keyword arguments to be used with Active Job.
+ *George Claghorn*
- Fixes #18741.
+* Pass the error instance as the second parameter of block executed by `discard_on`.
- *Sean Griffin*
+ Fixes #32853.
-* Add `:only` option to `assert_enqueued_jobs`, to check the number of times
- a specific kind of job is enqueued.
+ *Yuji Yaginuma*
- Example:
+* Remove support for Qu gem.
- def test_logging_job
- assert_enqueued_jobs 1, only: LoggingJob do
- LoggingJob.perform_later
- HelloJob.perform_later('jeremy')
- end
- end
+ 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
- *George Claghorn*
+ *Alberto Almagro*
-* `ActiveJob::Base.deserialize` delegates to the job class.
+* Add support for timezones to Active Job.
- 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.
+ 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.
- Example:
+ *Andrew White*
+
+* Rails 6 requires Ruby 2.5.0 or newer.
- class DeliverWebhookJob < ActiveJob::Base
- def serialize
- super.merge('attempt_number' => (@attempt_number || 0) + 1)
- end
+ *Jeremy Daer*, *Kasper Timm Hansen*
- 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 a3ffb46b3f..aedc21bca2 100644
--- a/activejob/MIT-LICENSE
+++ b/activejob/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2014-2016 David Heinemeier Hansson
+Copyright (c) 2014-2019 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 8becac7753..462d319992 100644
--- a/activejob/README.md
+++ b/activejob/README.md
@@ -1,7 +1,7 @@
-# Active Job -- Make work happen later
+# 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.
@@ -17,12 +17,13 @@ about API differences between Delayed Job and Resque. Picking your queuing
backend becomes more of an operational concern, then. And you'll be able to
switch between them without having to rewrite your jobs.
+You can read more about Active Job in the [Active Job Basics](https://edgeguides.rubyonrails.org/active_job_basics.html) guide.
## Usage
-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).
+[ActiveJob::QueueAdapters](https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
Declare a job like so:
@@ -39,7 +40,7 @@ end
Enqueue a job like so:
```ruby
-MyJob.perform_later record # Enqueue a job to be performed as soon as the queueing system is free.
+MyJob.perform_later record # Enqueue a job to be performed as soon as the queuing system is free.
```
```ruby
@@ -82,11 +83,17 @@ 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).
+see the API Documentation for [ActiveJob::QueueAdapters](https://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
@@ -100,7 +107,7 @@ The latest version of Active Job can be installed with RubyGems:
$ gem install activejob
```
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/activejob
@@ -108,16 +115,16 @@ 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
API documentation is at:
-* http://api.rubyonrails.org
+* https://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 2a853b4b6b..037e84fca9 100644
--- a/activejob/Rakefile
+++ b/activejob/Rakefile
@@ -1,62 +1,63 @@
-require 'rake/testtask'
+# frozen_string_literal: true
-ACTIVEJOB_ADAPTERS = %w(async 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
-task "package:clean"
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"
+ ENV["SKIP_REQUIRE_WEBPACKER"] = "true"
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
@@ -67,11 +68,9 @@ def run_without_aborting(tasks)
errors = []
tasks.each do |task|
- begin
- Rake::Task[task].invoke
- rescue Exception
- errors << task
- end
+ Rake::Task[task].invoke
+ rescue Exception
+ errors << task
end
abort "Errors running #{errors.join(', ')}" if errors.any?
diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec
index e97bb40abf..aeffe55af6 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.5.0"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "https://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://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.6'
+ 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 4b9065cf50..5f20ef9b9f 100644
--- a/activejob/lib/active_job.rb
+++ b/activejob/lib/active_job.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2014-2016 David Heinemeier Hansson
+# Copyright (c) 2014-2019 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 33bd5b4eb3..92eb58aaaf 100644
--- a/activejob/lib/active_job/arguments.rb
+++ b/activejob/lib/active_job/arguments.rb
@@ -1,51 +1,43 @@
-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 +cause+.
class DeserializationError < StandardError
- def initialize(e = nil) #:nodoc:
- if e
- ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \
- "Exceptions will automatically capture the original exception.", caller)
- end
-
+ def initialize #:nodoc:
super("Error while trying to deserialize arguments: #{$!.message}")
set_backtrace $!.backtrace
end
-
- # The original exception that was raised during deserialization of job
- # arguments.
- def original_exception
- ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller)
- cause
- end
end
# Raised when an unsupported argument type is set as a job argument. We
- # currently support NilClass, Fixnum, Float, String, TrueClass, FalseClass,
- # Bignum, BigDecimal, 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, BigDecimal ]
-
- # 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
@@ -53,47 +45,62 @@ module ActiveJob
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
def deserialize_argument(argument)
case argument
when String
- GlobalID::Locator.locate(argument) || argument
- when *TYPE_WHITELIST
+ argument
+ 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
@@ -103,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)
@@ -126,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
@@ -145,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 ff5c69ddc6..ed41fac4b8 100644
--- a/activejob/lib/active_job/base.rb
+++ b/activejob/lib/active_job/base.rb
@@ -1,12 +1,16 @@
-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/logging'
-require 'active_job/translation'
+# 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
@@ -36,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 as the queueing system is free:
+ # To enqueue a job to be performed as soon as the queuing system is free:
#
# ProcessPhotoJob.perform_later(photo)
#
@@ -62,7 +66,9 @@ module ActiveJob #:nodoc:
include Enqueuing
include Execution
include Callbacks
+ include Exceptions
include Logging
+ include Timezones
include Translation
ActiveSupport.run_load_hooks(:active_job, self)
diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb
index a6591c6a05..6b82ea6cab 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,6 +15,8 @@ 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
@@ -25,6 +29,9 @@ module ActiveJob
included do
define_callbacks :perform
define_callbacks :enqueue
+
+ class_attribute :return_false_on_aborted_enqueue, instance_accessor: false, instance_predicate: false
+ self.return_false_on_aborted_enqueue = false
end
# These methods will be included into any Active Job object, adding
@@ -126,8 +133,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 f7f882c998..283125698d 100644
--- a/activejob/lib/active_job/core.rb
+++ b/activejob/lib/active_job/core.rb
@@ -1,39 +1,54 @@
+# 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
- # Priority that the job will have (lower is more priority).
- attr_writer :priority
+ # Priority that the job will have (lower is more priority).
+ attr_writer :priority
- # ID optionally provided by adapter
- attr_accessor :provider_job_id
+ # ID optionally provided by adapter
+ attr_accessor :provider_job_id
- # I18n.locale to be used during the job.
- attr_accessor :locale
- end
+ # Number of times this job has been executed (which increments on every retry, like after an exception).
+ attr_accessor :executions
+
+ # Hash that contains the number of times this job handled errors for each specific retry_on declaration.
+ # Keys are the string representation of the exceptions listed in the retry_on declaration,
+ # while its associated value holds the number of executions where the corresponding retry_on
+ # declaration handled one of its listed exceptions.
+ attr_accessor :exception_executions
+
+ # I18n.locale to be used during the job.
+ attr_accessor :locale
+
+ # Timezone to be used during the job.
+ attr_accessor :timezone
+
+ # Track when a job was enqueued
+ attr_accessor :enqueued_at
# 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
@@ -56,7 +71,7 @@ module ActiveJob
# 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)
# VideoJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later(Video.last)
- def set(options={})
+ def set(options = {})
ConfiguredJob.new(self, options)
end
end
@@ -68,18 +83,25 @@ module ActiveJob
@job_id = SecureRandom.uuid
@queue_name = self.class.queue_name
@priority = self.class.priority
+ @executions = 0
+ @exception_executions = {}
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,
- 'priority' => priority,
- 'arguments' => serialize_arguments(arguments),
- 'locale' => I18n.locale.to_s
+ "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,
+ "exception_executions" => exception_executions,
+ "locale" => I18n.locale.to_s,
+ "timezone" => Time.zone.try(:name),
+ "enqueued_at" => Time.now.utc.iso8601
}
end
@@ -89,42 +111,65 @@ 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.priority = job_data['priority']
- self.serialized_arguments = job_data['arguments']
- self.locale = job_data['locale'] || I18n.locale.to_s
+ 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.exception_executions = job_data["exception_executions"]
+ self.locale = job_data["locale"] || I18n.locale.to_s
+ self.timezone = job_data["timezone"] || Time.zone.try(:name)
+ self.enqueued_at = job_data["enqueued_at"]
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 22154457fd..5609d13f5f 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,37 +22,12 @@ 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
- # * <tt>:priority</tt> - Enqueues the job with the specified priority
- #
- # ==== 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
@@ -64,19 +43,38 @@ module ActiveJob
# my_job_instance.enqueue queue: :important
# my_job_instance.enqueue wait_until: Date.tomorrow.midnight
# my_job_instance.enqueue priority: 10
- def enqueue(options={})
+ 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]
+ successfully_enqueued = false
+
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
+
+ successfully_enqueued = true
+ end
+
+ if successfully_enqueued
+ self
+ else
+ if self.class.return_false_on_aborted_enqueue
+ false
+ else
+ ActiveSupport::Deprecation.warn(
+ "Rails 6.0 will return false when the enqueuing is aborted. Make sure your code doesn't depend on it" \
+ " returning the instance of the job and set `config.active_job.return_false_on_aborted_enqueue = true`" \
+ " to remove the deprecations."
+ )
+
+ self
+ end
end
- self
end
end
end
diff --git a/activejob/lib/active_job/exceptions.rb b/activejob/lib/active_job/exceptions.rb
new file mode 100644
index 0000000000..9e00942a1c
--- /dev/null
+++ b/activejob/lib/active_job/exceptions.rb
@@ -0,0 +1,150 @@
+# 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.
+ # 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)
+ # * <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 ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
+ # retry_on Net::OpenTimeout, Timeout::Error, wait: :exponentially_longer, attempts: 10 # retries at most 10 times for Net::OpenTimeout and Timeout::Error combined
+ # # To retry at most 10 times for each individual exception:
+ # # retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10
+ # # retry_on Timeout::Error, wait: :exponentially_longer, attempts: 10
+ #
+ # retry_on(YetAnotherCustomAppException) do |job, error|
+ # ExceptionNotifier.caught(error)
+ # end
+ #
+ # 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 or Timeout::Error 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|
+ # Guard against jobs that were persisted before we started having individual executions counters per retry_on
+ self.exception_executions ||= {}
+ self.exception_executions[exceptions.to_s] = (exception_executions[exceptions.to_s] || 0) + 1
+
+ if exception_executions[exceptions.to_s] < 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 7c4151fc90..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
@@ -24,17 +26,20 @@ module ActiveJob
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 be4fabf545..5313a4ab9e 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 = "beta3"
diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb
index 605057d1e8..1134e718a8 100644
--- a/activejob/lib/active_job/logging.rb
+++ b/activejob/lib/active_job/logging.rb
@@ -1,24 +1,25 @@
-require 'active_support/core_ext/hash/transform_values'
-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
@@ -26,13 +27,13 @@ module ActiveJob
end
end
- after_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
@@ -41,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
@@ -51,70 +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)} enqueued at #{job.enqueued_at}" + 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| format(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 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
+ def discard(event)
+ job = event.payload[:job]
+ ex = event.payload[:error]
+
+ error do
+ "Discarded #{job.class} due to a #{ex.class}."
end
end
- def scheduled_at(event)
- Time.at(event.payload[:job].scheduled_at).utc
- end
+ private
+ def queue_name(event)
+ event.payload[:adapter].class.name.demodulize.remove("Adapter") + "(#{event.payload[:job].queue_name})"
+ end
- def logger
- ActiveJob::Base.logger
- end
- 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 72e4ebf935..954bfd1dd1 100644
--- a/activejob/lib/active_job/queue_adapter.rb
+++ b/activejob/lib/active_job/queue_adapter.rb
@@ -1,6 +1,6 @@
-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
@@ -9,6 +9,7 @@ module ActiveJob
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 = :async
end
@@ -21,43 +22,41 @@ module ActiveJob
_queue_adapter
end
+ # Returns string denoting the name of the configured queue adapter.
+ # By default returns +"async"+.
+ def queue_adapter_name
+ _queue_adapter_name
+ end
+
# Specify the backend queue provider. The default queue adapter
# is the +:async+ queue. See QueueAdapters for more
# information.
- def queue_adapter=(name_or_adapter_or_class)
- self._queue_adapter = interpret_adapter(name_or_adapter_or_class)
- end
-
- private
-
- def interpret_adapter(name_or_adapter_or_class)
- case name_or_adapter_or_class
+ 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 2c5039ef4d..a4b7eb86f1 100644
--- a/activejob/lib/active_job/queue_adapters.rb
+++ b/activejob/lib/active_job/queue_adapters.rb
@@ -1,19 +1,21 @@
+# 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]
- # * {Sidekiq}[http://sidekiq.org]
+ # * {Resque}[https://github.com/resque/resque]
+ # * {Sidekiq}[https://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]
+ # * {Active Job Async Job}[https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html]
+ # * {Active Job Inline}[https://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
#
@@ -21,7 +23,6 @@ module ActiveJob
# |-------------------|-------|--------|------------|------------|---------|---------|
# | Backburner | Yes | Yes | Yes | Yes | Job | Global |
# | Delayed Job | Yes | Yes | Yes | Job | Global | Global |
- # | Qu | Yes | Yes | No | No | No | Global |
# | Que | Yes | Yes | Yes | Job | No | Job |
# | queue_classic | Yes | Yes | Yes* | No | No | No |
# | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes |
@@ -33,7 +34,8 @@ module ActiveJob
#
# ==== 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.
#
@@ -50,7 +52,7 @@ 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 supports job scheduling since version 3.1.
@@ -72,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
#
@@ -111,7 +113,6 @@ module ActiveJob
autoload :InlineAdapter
autoload :BackburnerAdapter
autoload :DelayedJobAdapter
- autoload :QuAdapter
autoload :QueAdapter
autoload :QueueClassicAdapter
autoload :ResqueAdapter
@@ -120,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
index 922bc4afce..53a7e3d53e 100644
--- a/activejob/lib/active_job/queue_adapters/async_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/async_adapter.rb
@@ -1,7 +1,9 @@
-require 'securerandom'
-require 'concurrent/scheduled_task'
-require 'concurrent/executor/thread_pool_executor'
-require 'concurrent/utility/processor_counter'
+# 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
@@ -29,7 +31,7 @@ module ActiveJob
# jobs. Since jobs share a single thread pool, long-running jobs will block
# short-lived jobs. Fine for dev/test; bad for production.
class AsyncAdapter
- # See {Concurrent::ThreadPoolExecutor}[http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html] for executor options.
+ # 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
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 0a785fad3b..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
@@ -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 0496f8449e..ca04dc943c 100644
--- a/activejob/lib/active_job/queue_adapters/inline_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveJob
module QueueAdapters
# == Active Job Inline adapter
@@ -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 ab13689747..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
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 0ee41407d8..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
@@ -26,9 +28,9 @@ module ActiveJob
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
qc_job = queue.enqueue_at(timestamp, "#{JobWrapper.name}.perform", job.serialize)
job.provider_job_id = qc_job["id"] if qc_job.is_a?(Hash)
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 d78bdecdcb..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 'monitor'
+# 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 311109e958..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,11 +1,13 @@
-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.
- # This reduces the cost of of hosting on a service like Heroku along
+ # 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.
@@ -31,7 +33,7 @@ module ActiveJob
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.'
+ raise NotImplementedError, "sucker_punch 1.0 does not support `enqueued_at`. Please upgrade to version ~> 2.0.0 to enable this behavior."
end
end
diff --git a/activejob/lib/active_job/queue_adapters/test_adapter.rb b/activejob/lib/active_job/queue_adapters/test_adapter.rb
index 9b7b7139f4..c134257ebc 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,53 @@ 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
+ !filter_as_proc(filter).call(job)
+ elsif reject
+ filter_as_proc(reject).call(job)
+ end
+ end
+
+ def filter_as_proc(filter)
+ return filter if filter.is_a?(Proc)
+
+ ->(job) { Array(filter).include?(job.class) }
+ end
end
end
end
diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb
index 65786a49ff..de259261de 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,27 @@ module ActiveJob
# post.to_feed!
# end
# end
- def queue_as(part_name=nil, &block)
+ #
+ # Can be given a block that will evaluate in the context of the job
+ # allowing +self.arguments+ to be accessed so that a dynamic queue name
+ # can be applied:
+ #
+ # class PublishToFeedJob < ApplicationJob
+ # queue_as do
+ # post = self.arguments.first
+ #
+ # if post.paid?
+ # :paid_feeds
+ # else
+ # :feeds
+ # end
+ # end
+ #
+ # def perform(post)
+ # post.to_feed!
+ # end
+ # end
+ def queue_as(part_name = nil, &block)
if block_given?
self.queue_name = block
else
@@ -32,11 +54,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: -> { self.class.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 +65,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
index 01d84910ff..063bccdb01 100644
--- a/activejob/lib/active_job/queue_priority.rb
+++ b/activejob/lib/active_job/queue_priority.rb
@@ -1,10 +1,12 @@
+# 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)
+ mattr_accessor :default_priority
# Specifies the priority of the queue to create the job with.
#
@@ -17,7 +19,7 @@ module ActiveJob
# end
#
# Specify either an argument or a block.
- def queue_with_priority(priority=nil, &block)
+ def queue_with_priority(priority = nil, &block)
if block_given?
self.priority = block
else
@@ -27,9 +29,7 @@ module ActiveJob
end
included do
- class_attribute :priority, instance_accessor: false
-
- self.priority = default_priority
+ class_attribute :priority, instance_accessor: false, default: default_priority
end
# Returns the priority that the job will be created with
@@ -39,6 +39,5 @@ module ActiveJob
end
@priority
end
-
end
end
diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb
index a47caa4a7e..ecc0908d5f 100644
--- a/activejob/lib/active_job/railtie.rb
+++ b/activejob/lib/active_job/railtie.rb
@@ -1,21 +1,38 @@
-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 ||= :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
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 3feb82d432..e5e2b086bc 100644
--- a/activejob/lib/active_job/test_helper.rb
+++ b/activejob/lib/active_job/test_helper.rb
@@ -1,5 +1,6 @@
-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"
module ActiveJob
# Provides helper methods for testing Active Job
@@ -8,17 +9,36 @@ module ActiveJob
:performed_jobs, :performed_jobs=,
to: :queue_adapter
- def before_setup # :nodoc:
- test_adapter = ActiveJob::QueueAdapters::TestAdapter.new
-
- @old_queue_adapters = (ActiveJob::Base.subclasses << ActiveJob::Base).select do |klass|
- # only override explicitly set adapters, a quirk of `class_attribute`
- klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter)
- end.map do |klass|
- [klass, klass.queue_adapter].tap do
- klass.queue_adapter = test_adapter
+ module TestQueueAdapter
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :_test_adapter, instance_accessor: false, instance_predicate: false
+ end
+
+ module ClassMethods
+ def queue_adapter
+ self._test_adapter.nil? ? super : self._test_adapter
+ end
+
+ def disable_test_adapter
+ self._test_adapter = nil
+ end
+
+ def enable_test_adapter(test_adapter)
+ self._test_adapter = test_adapter
end
end
+ end
+
+ ActiveJob::Base.include(TestQueueAdapter)
+
+ def before_setup # :nodoc:
+ test_adapter = queue_adapter_for_test
+
+ queue_adapter_changed_jobs.each do |klass|
+ klass.enable_test_adapter(test_adapter)
+ end
clear_enqueued_jobs
clear_performed_jobs
@@ -27,9 +47,21 @@ module ActiveJob
def after_teardown # :nodoc:
super
- @old_queue_adapters.each do |(klass, adapter)|
- klass.queue_adapter = adapter
- end
+
+ queue_adapter_changed_jobs.each { |klass| klass.disable_test_adapter }
+ end
+
+ # Specifies the queue adapter to use with all Active Job test helpers.
+ #
+ # 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.
@@ -42,7 +74,7 @@ module ActiveJob
# assert_enqueued_jobs 2
# end
#
- # If a block is passed, that block should cause the specified number of
+ # If a block is passed, asserts that the block will cause the specified number of
# jobs to be enqueued.
#
# def test_jobs_again
@@ -56,7 +88,7 @@ module ActiveJob
# end
# end
#
- # The number of times a specific job is enqueued can be asserted.
+ # 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
@@ -64,16 +96,41 @@ module ActiveJob
# HelloJob.perform_later('jeremy')
# end
# end
- def assert_enqueued_jobs(number, only: nil)
+ #
+ # 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
+ #
+ # +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
+ # a hash containing the job's class and it's argument are passed as argument.
+ #
+ # 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_size(only: only)
+ original_count = enqueued_jobs_with(only: only, except: except, queue: queue)
+
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"
+
+ new_count = enqueued_jobs_with(only: only, except: except, queue: queue)
+
+ actual_count = new_count - original_count
else
- actual_count = enqueued_jobs_size(only: only)
- assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued"
+ actual_count = enqueued_jobs_with(only: only, except: except, queue: queue)
end
+
+ assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued"
end
# Asserts that no jobs have been enqueued.
@@ -84,7 +141,7 @@ module ActiveJob
# assert_enqueued_jobs 1
# end
#
- # If a block is passed, that block should not cause any job to be enqueued.
+ # 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
@@ -92,7 +149,7 @@ module ActiveJob
# end
# end
#
- # It can be asserted that no jobs of a specific kind are enqueued:
+ # 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
@@ -100,16 +157,35 @@ module ActiveJob
# 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
+ #
+ # +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
+ # a hash containing the job's class and it's argument are passed as argument.
+ #
+ # 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, &block)
- assert_enqueued_jobs 0, only: only, &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 the job call.
+ # must be called around or after the job call.
#
# def test_jobs
# assert_performed_jobs 0
@@ -119,13 +195,14 @@ module ActiveJob
# end
# assert_performed_jobs 1
#
- # perform_enqueued_jobs do
- # HelloJob.perform_later('yves')
- # assert_performed_jobs 2
- # end
+ # HelloJob.perform_later('yves')
+ #
+ # perform_enqueued_jobs
+ #
+ # assert_performed_jobs 2
# end
#
- # If a block is passed, that block should cause the specified number of
+ # If a block is passed, asserts that the block will cause the specified number of
# jobs to be performed.
#
# def test_jobs_again
@@ -139,7 +216,7 @@ module ActiveJob
# end
# end
#
- # The block form supports filtering. If the :only option is specified,
+ # This method also supports filtering. If the +:only+ option is specified,
# then only the listed job(s) will be performed.
#
# def test_hello_job
@@ -149,6 +226,16 @@ module ActiveJob
# 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
@@ -160,17 +247,42 @@ module ActiveJob
# end
# end
# end
- def assert_performed_jobs(number, only: nil)
+ #
+ # A proc may also be specified. When passed a Proc, the job's instance will be passed as argument.
+ #
+ # def test_hello_and_logging_jobs
+ # assert_nothing_raised do
+ # assert_performed_jobs(1, only: ->(job) { job.is_a?(HelloJob) }) 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) { yield }
+
+ perform_enqueued_jobs(only: only, except: except, queue: queue, &block)
+
new_count = performed_jobs.size
- assert_equal number, new_count - original_count,
- "#{number} jobs expected, but #{new_count - original_count} were performed"
+
+ performed_jobs_size = new_count - original_count
else
- performed_jobs_size = performed_jobs.size
- assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed"
+ performed_jobs_size = performed_jobs_with(only: only, except: except, queue: queue)
end
+
+ assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed"
end
# Asserts that no jobs have been performed.
@@ -184,7 +296,7 @@ module ActiveJob
# end
# end
#
- # If a block is passed, that block should not cause any job to be performed.
+ # 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
@@ -192,7 +304,7 @@ module ActiveJob
# end
# end
#
- # The block form supports filtering. If the :only option is specified,
+ # 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
@@ -201,14 +313,62 @@ module ActiveJob
# 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
+ #
+ # +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
+ # an instance of the job will be passed as argument.
+ #
+ # 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, &block)
- assert_performed_jobs 0, only: only, &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 passed in the block has been enqueued with the given arguments.
+ # 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
@@ -219,20 +379,71 @@ module ActiveJob
# MyJob.set(wait_until: Date.tomorrow.noon).perform_later
# end
# end
- def assert_enqueued_with(args = {})
- original_enqueued_jobs_count = enqueued_jobs.count
- args.assert_valid_keys(:job, :args, :at, :queue)
- serialized_args = serialize_args_for_assertion(args)
- yield
- in_block_jobs = enqueued_jobs.drop(original_enqueued_jobs_count)
- matching_job = in_block_jobs.find do |job|
- serialized_args.all? { |key, value| value == job[key] }
+ 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
+
+ jobs = enqueued_jobs.drop(original_enqueued_jobs_count)
+ else
+ jobs = enqueued_jobs
end
- assert matching_job, "No enqueued job found with #{args}"
+
+ 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
+ end
+
+ assert matching_job, "No enqueued job found with #{expected}"
instantiate_job(matching_job)
end
- # Asserts that the job passed in the block has been performed with the given arguments.
+ # 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
@@ -243,69 +454,223 @@ module ActiveJob
# MyJob.set(wait_until: Date.tomorrow.noon).perform_later
# end
# end
- def assert_performed_with(args = {})
- original_performed_jobs_count = performed_jobs.count
- args.assert_valid_keys(:job, :args, :at, :queue)
- serialized_args = serialize_args_for_assertion(args)
- perform_enqueued_jobs { yield }
- in_block_jobs = performed_jobs.drop(original_performed_jobs_count)
- matching_job = in_block_jobs.find do |job|
- serialized_args.all? { |key, value| value == job[key] }
+ 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
- assert matching_job, "No performed job found with #{args}"
+
+ 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
+ end
+
+ assert matching_job, "No performed job found with #{expected}"
instantiate_job(matching_job)
end
- def perform_enqueued_jobs(only: nil)
+ # 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
+ #
+ # +:only+ and +:except+ options accepts Class, Array of Class or Proc. When passed a Proc,
+ # an instance of the job will be passed as argument.
+ #
+ # 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.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 # :nodoc:
+ def clear_enqueued_jobs
enqueued_jobs.clear
end
- def clear_performed_jobs # :nodoc:
+ def clear_performed_jobs
performed_jobs.clear
end
- def enqueued_jobs_size(only: nil) # :nodoc:
- if only
- enqueued_jobs.count { |job| Array(only).include?(job.fetch(:job)) }
+ 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 filter_as_proc(only).call(job)
+ elsif except
+ next false if filter_as_proc(except).call(job)
+ 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 filter_as_proc(filter)
+ return filter if filter.is_a?(Proc)
+
+ ->(job) { Array(filter).include?(job.fetch(:job)) }
+ end
+
+ def enqueued_jobs_with(only: nil, except: nil, queue: nil, &block)
+ jobs_with(enqueued_jobs, only: only, except: except, queue: queue, &block)
+ end
+
+ def performed_jobs_with(only: nil, except: nil, queue: nil, &block)
+ jobs_with(performed_jobs, only: only, except: except, queue: queue, &block)
+ end
+
+ 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 prepare_args_for_assertion(args)
+ args.dup.tap do |arguments|
+ arguments[:at] = arguments[:at].to_f if arguments[:at]
+ arguments[:args] = round_time_arguments(arguments[:args]) if arguments[:args]
+ end
+ end
+
+ def round_time_arguments(argument)
+ case argument
+ when Time, ActiveSupport::TimeWithZone, DateTime
+ argument.change(usec: 0)
+ when Hash
+ argument.transform_values { |value| round_time_arguments(value) }
+ when Array
+ argument.map { |element| round_time_arguments(element) }
else
- enqueued_jobs.count
+ argument
end
end
- def serialize_args_for_assertion(args) # :nodoc:
- args.dup.tap do |serialized_args|
- serialized_args[:args] = ActiveJob::Arguments.serialize(serialized_args[:args]) if serialized_args[:args]
- serialized_args[:at] = serialized_args[:at].to_f if serialized_args[:at]
+ 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 instantiate_job(payload) # :nodoc:
- job = payload[:job].new(*payload[: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
+
+ 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
index 67e4cf4ab9..0fd9b9fc06 100644
--- a/activejob/lib/active_job/translation.rb
+++ b/activejob/lib/active_job/translation.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module ActiveJob
module Translation #:nodoc:
extend ActiveSupport::Concern
included do
- around_perform do |job, block, _|
+ around_perform do |job, block|
I18n.with_locale(job.locale, &block)
end
end
diff --git a/activejob/lib/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
index 08eb9658cd..a4fed7c2f7 100644
--- a/activejob/test/adapters/async.rb
+++ b/activejob/test/adapters/async.rb
@@ -1,2 +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 59dc3d7f78..da198abc0b 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, BigDecimal.new(5),
- [ 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,83 @@ 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 keep Global IDs strings as they are" do
+ assert_arguments_roundtrip [@person.to_gid.to_s]
+ end
+
+ 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 }
+ 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 }
+ string_key = { "a" => 1 }
indifferent_access = { a: 1 }.with_indifferent_access
assert_not_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([symbol_key]).first
@@ -54,26 +121,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
+ test "should maintain time with zone" do
+ Time.use_zone "Alaska" do
+ time_with_zone = Time.new(2002, 10, 31, 2, 2, 2).in_time_zone
+ assert_instance_of ActiveSupport::TimeWithZone, perform_round_trip([time_with_zone]).first
+ assert_arguments_unchanged time_with_zone
+ end
+ end
+
+ test "should disallow non-string/symbol hash keys" do
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [ { 1 => 2 } ]
end
assert_raises ActiveJob::SerializationError do
- ActiveJob::Arguments.serialize [ { :a => [{ 2 => 3 }] } ]
+ ActiveJob::Arguments.serialize [ { a: [{ 2 => 3 }] } ]
end
end
- test 'should not allow reserved hash keys' do
- ['_aj_globalid', :_aj_globalid, '_aj_symbol_keys', :_aj_symbol_keys,
- '_aj_hash_with_indifferent_access', :_aj_hash_with_indifferent_access].each do |key|
+ 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
@@ -83,17 +160,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..895edb34a5 100644
--- a/activejob/test/cases/callbacks_test.rb
+++ b/activejob/test/cases/callbacks_test.rb
@@ -1,10 +1,13 @@
-require 'helper'
-require 'jobs/callback_job'
+# frozen_string_literal: true
-require 'active_support/core_ext/object/inclusion'
+require "helper"
+require "jobs/callback_job"
+require "jobs/abort_before_enqueue_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,11 +16,35 @@ 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
assert "CallbackJob ran around_enqueue_start".in? enqueued_callback_job.history
assert "CallbackJob ran around_enqueue_stop".in? enqueued_callback_job.history
end
+
+ test "#enqueue returns false when before_enqueue aborts callback chain and return_false_on_aborted_enqueue = true" do
+ prev = ActiveJob::Base.return_false_on_aborted_enqueue
+ ActiveJob::Base.return_false_on_aborted_enqueue = true
+ assert_equal false, AbortBeforeEnqueueJob.new.enqueue
+ ensure
+ ActiveJob::Base.return_false_on_aborted_enqueue = prev
+ end
+
+ test "#enqueue returns self when before_enqueue aborts callback chain and return_false_on_aborted_enqueue = false" do
+ prev = ActiveJob::Base.return_false_on_aborted_enqueue
+ ActiveJob::Base.return_false_on_aborted_enqueue = false
+ job = AbortBeforeEnqueueJob.new
+ assert_deprecated do
+ assert_equal job, job.enqueue
+ end
+ ensure
+ ActiveJob::Base.return_false_on_aborted_enqueue = prev
+ end
+
+ test "#enqueue returns self when the job was enqueued" do
+ job = CallbackJob.new
+ assert_equal job, job.enqueue
+ end
end
diff --git a/activejob/test/cases/exceptions_test.rb b/activejob/test/cases/exceptions_test.rb
new file mode 100644
index 0000000000..c88162bf58
--- /dev/null
+++ b/activejob/test/cases/exceptions_test.rb
@@ -0,0 +1,170 @@
+# frozen_string_literal: true
+
+require "helper"
+require "jobs/retry_job"
+require "models/person"
+
+class ExceptionsTest < ActiveSupport::TestCase
+ setup do
+ JobBuffer.clear
+ skip if adapter_skips_scheduling?(ActiveJob::Base.queue_adapter)
+ end
+
+ test "successfully retry job throwing exception against defaults" 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
+
+ test "successfully retry job throwing exception against higher limit" do
+ RetryJob.perform_later "ShortWaitTenAttemptsError", 9
+ assert_equal 9, JobBuffer.values.count
+ end
+
+ test "keeps the same attempts counter for several exceptions listed in the same retry_on declaration" do
+ exceptions_to_raise = %w(FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo
+ SecondRetryableErrorOfTwo SecondRetryableErrorOfTwo)
+
+ assert_raises SecondRetryableErrorOfTwo do
+ RetryJob.perform_later(exceptions_to_raise, 5)
+
+ assert_equal [
+ "Raised FirstRetryableErrorOfTwo for the 1st time",
+ "Raised FirstRetryableErrorOfTwo for the 2nd time",
+ "Raised FirstRetryableErrorOfTwo for the 3rd time",
+ "Raised SecondRetryableErrorOfTwo for the 4th time",
+ "Raised SecondRetryableErrorOfTwo for the 5th time",
+ ], JobBuffer.values
+ end
+ end
+
+ test "keeps a separate attempts counter for each individual retry_on declaration" do
+ exceptions_to_raise = %w(DefaultsError DefaultsError DefaultsError DefaultsError
+ FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo)
+
+ assert_nothing_raised do
+ RetryJob.perform_later(exceptions_to_raise, 10)
+
+ 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",
+ "Raised FirstRetryableErrorOfTwo for the 5th time",
+ "Raised FirstRetryableErrorOfTwo for the 6th time",
+ "Raised FirstRetryableErrorOfTwo for the 7th time",
+ "Successfully completed job"
+ ], JobBuffer.values
+ end
+ end
+
+ test "failed retry job when exception kept occurring against defaults" do
+ RetryJob.perform_later "DefaultsError", 6
+ assert_equal "Raised DefaultsError for the 5th time", JobBuffer.last_value
+ rescue DefaultsError
+ pass
+ end
+
+ test "failed retry job when exception kept occurring against higher limit" do
+ RetryJob.perform_later "ShortWaitTenAttemptsError", 11
+ assert_equal "Raised ShortWaitTenAttemptsError for the 10th time", JobBuffer.last_value
+ rescue ShortWaitTenAttemptsError
+ pass
+ end
+
+ test "discard job" do
+ RetryJob.perform_later "DiscardableError", 2
+ assert_equal "Raised DiscardableError for the 1st time", JobBuffer.last_value
+ end
+
+ test "custom handling of discarded job" 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
+
+ test "custom handling of job that exceeds retry attempts" 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
+
+ test "long wait job" do
+ travel_to Time.now
+
+ RetryJob.perform_later "LongWaitError", 2, :log_scheduled_at
+
+ assert_equal [
+ "Raised LongWaitError for the 1st time",
+ "Next execution scheduled at #{(Time.now + 3600.seconds).to_f}",
+ "Successfully completed job"
+ ], JobBuffer.values
+ end
+
+ test "exponentially retrying job" do
+ travel_to Time.now
+
+ RetryJob.perform_later "ExponentialWaitTenAttemptsError", 5, :log_scheduled_at
+
+ assert_equal [
+ "Raised ExponentialWaitTenAttemptsError for the 1st time",
+ "Next execution scheduled at #{(Time.now + 3.seconds).to_f}",
+ "Raised ExponentialWaitTenAttemptsError for the 2nd time",
+ "Next execution scheduled at #{(Time.now + 18.seconds).to_f}",
+ "Raised ExponentialWaitTenAttemptsError for the 3rd time",
+ "Next execution scheduled at #{(Time.now + 83.seconds).to_f}",
+ "Raised ExponentialWaitTenAttemptsError for the 4th time",
+ "Next execution scheduled at #{(Time.now + 258.seconds).to_f}",
+ "Successfully completed job"
+ ], JobBuffer.values
+ end
+
+ test "custom wait retrying job" do
+ travel_to Time.now
+
+ RetryJob.perform_later "CustomWaitTenAttemptsError", 5, :log_scheduled_at
+
+ assert_equal [
+ "Raised CustomWaitTenAttemptsError for the 1st time",
+ "Next execution scheduled at #{(Time.now + 2.seconds).to_f}",
+ "Raised CustomWaitTenAttemptsError for the 2nd time",
+ "Next execution scheduled at #{(Time.now + 4.seconds).to_f}",
+ "Raised CustomWaitTenAttemptsError for the 3rd time",
+ "Next execution scheduled at #{(Time.now + 6.seconds).to_f}",
+ "Raised CustomWaitTenAttemptsError for the 4th time",
+ "Next execution scheduled at #{(Time.now + 8.seconds).to_f}",
+ "Successfully completed job"
+ ], JobBuffer.values
+ end
+
+ test "successfully retry job throwing one of two retryable exceptions" 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
+
+ test "discard job throwing one of two discardable exceptions" do
+ RetryJob.perform_later "SecondDiscardableErrorOfTwo", 2
+ assert_equal [ "Raised SecondDiscardableErrorOfTwo for the 1st time" ], JobBuffer.values
+ end
+
+ test "successfully retry job throwing DeserializationError" do
+ RetryJob.perform_later Person.new(404), 5
+ assert_equal ["Raised ActiveJob::DeserializationError for the 5 time"], JobBuffer.values
+ end
+
+ private
+ def adapter_skips_scheduling?(queue_adapter)
+ [
+ ActiveJob::QueueAdapters::InlineAdapter,
+ ActiveJob::QueueAdapters::AsyncAdapter,
+ ActiveJob::QueueAdapters::SneakersAdapter
+ ].include?(queue_adapter.class)
+ end
+end
diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb
index fa94209889..c1cec1f1d6 100644
--- a/activejob/test/cases/job_serialization_test.rb
+++ b/activejob/test/cases/job_serialization_test.rb
@@ -1,8 +1,10 @@
-require 'helper'
-require 'jobs/gid_job'
-require 'jobs/hello_job'
-require 'models/person'
-require 'json'
+# 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
@@ -10,38 +12,64 @@ 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']
+ test "serialize includes current locale" do
+ assert_equal "en", HelloJob.new.serialize["locale"]
end
- test 'serialize and deserialize are symmetric' do
+ test "serialize and deserialize are symmetric" do
# Round trip a job in memory only
- h1 = HelloJob.new
- h1.deserialize(h1.serialize)
+ 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(h1.serialize)
- h2 = HelloJob.new
- h2.deserialize(JSON.load(payload))
- assert_equal h1.serialize, h2.serialize
+ payload = JSON.dump(h2.serialize)
+ h3 = HelloJob.deserialize(JSON.load(payload))
+ assert_equal h2.serialize, h3.serialize
end
- test 'deserialize sets locale' do
+ test "deserialize sets locale" do
job = HelloJob.new
- job.deserialize 'locale' => 'es'
- assert_equal 'es', job.locale
+ job.deserialize "locale" => "es"
+ assert_equal "es", job.locale
end
- test 'deserialize sets default locale' do
+ test "deserialize sets default locale" do
job = HelloJob.new
job.deserialize({})
- assert_equal 'en', job.locale
+ 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
+
+ test "serialize stores the enqueued_at time" do
+ h1 = HelloJob.new
+ type = h1.serialize["enqueued_at"].class
+ assert_equal String, type
+
+ h2 = HelloJob.deserialize(h1.serialize)
+ # We should be able to parse a timestamp
+ type = Time.parse(h2.enqueued_at).class
+ assert_equal Time, type
end
end
diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb
index 820e9112de..acd37456c9 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,56 +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
- 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)
+ 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(/enqueued at /, @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
+ 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
+
+ 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..4b43c7c3c5 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
- assert_equal "default", HelloJob.queue_name
+ test "name derived from base" do
+ assert_equal "default", HelloJob.new.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,34 @@ class QueueNamingTest < ActiveSupport::TestCase
end
end
- test 'uses queue passed to #set' do
+ test "using a custom default_queue_name" do
+ original_default_queue_name = ActiveJob::Base.default_queue_name
+
+ begin
+ ActiveJob::Base.default_queue_name = "default_queue_name"
+
+ assert_equal "default_queue_name", HelloJob.new.queue_name
+ ensure
+ ActiveJob::Base.default_queue_name = original_default_queue_name
+ end
+ end
+
+ test "queue_name_prefix prepended to the default_queue_name" do
+ original_queue_name_prefix = ActiveJob::Base.queue_name_prefix
+ original_default_queue_name = ActiveJob::Base.default_queue_name
+
+ begin
+ ActiveJob::Base.queue_name_prefix = "prefix"
+ ActiveJob::Base.default_queue_name = "default_queue_name"
+
+ assert_equal "prefix_default_queue_name", HelloJob.new.queue_name
+ ensure
+ ActiveJob::Base.queue_name_prefix = original_queue_name_prefix
+ ActiveJob::Base.default_queue_name = original_default_queue_name
+ end
+ end
+
+ 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
index ca17b51dad..4b3006ae81 100644
--- a/activejob/test/cases/queue_priority_test.rb
+++ b/activejob/test/cases/queue_priority_test.rb
@@ -1,12 +1,14 @@
-require 'helper'
-require 'jobs/hello_job'
+# frozen_string_literal: true
+
+require "helper"
+require "jobs/hello_job"
class QueuePriorityTest < ActiveSupport::TestCase
- test 'priority unset by default' do
- assert_equal nil, HelloJob.priority
+ test "priority unset by default" do
+ assert_nil HelloJob.priority
end
- test 'uses given priority' do
+ test "uses given priority" do
original_priority = HelloJob.priority
begin
@@ -17,7 +19,7 @@ class QueuePriorityTest < ActiveSupport::TestCase
end
end
- test 'evals block given to priority to determine priority' do
+ test "evals block given to priority to determine priority" do
original_priority = HelloJob.priority
begin
@@ -28,19 +30,19 @@ class QueuePriorityTest < ActiveSupport::TestCase
end
end
- test 'can use arguments to determine priority in priority block' do
+ test "can use arguments to determine priority in priority block" do
original_priority = HelloJob.priority
begin
- HelloJob.queue_with_priority { self.arguments.first=='1' ? 99 : 11 }
- assert_equal 99, HelloJob.new('1').priority
- assert_equal 11, HelloJob.new('3').priority
+ 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
+ test "uses priority passed to #set" do
job = HelloJob.set(priority: 123).perform_later
assert_equal 123, job.priority
end
diff --git a/activejob/test/cases/queuing_test.rb b/activejob/test/cases/queuing_test.rb
index 0eeabbf693..e7bad83400 100644
--- a/activejob/test/cases/queuing_test.rb
+++ b/activejob/test/cases/queuing_test.rb
@@ -1,44 +1,40 @@
-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
- begin
- result = HelloJob.set(wait_until: 1.second.ago).perform_later "Jamie"
- assert result
- rescue NotImplementedError
- skip
- end
+ test "run queued job later" do
+ result = HelloJob.set(wait_until: 1.second.ago).perform_later "Jamie"
+ assert result
+ rescue NotImplementedError
+ skip
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
- 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
- rescue NotImplementedError
- skip
- end
+ test "job returned by perform_at has the timestamp available" do
+ job = HelloJob.set(wait_until: Time.utc(2014, 1, 1)).perform_later
+ assert_equal Time.utc(2014, 1, 1).to_f, job.scheduled_at
+ rescue NotImplementedError
+ skip
end
end
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 616454a4b6..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 'Attempting 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 f7ee763e8a..d6607cb6b6 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,94 @@ 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_only_option_as_proc
+ assert_nothing_raised do
+ assert_enqueued_jobs(1, only: ->(job) { job.fetch(:job).name == "HelloJob" }) do
+ 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_except_option_as_proc
+ assert_nothing_raised do
+ assert_enqueued_jobs(1, except: ->(job) { job.fetch(:job).name == "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 +209,30 @@ 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
@@ -130,26 +240,90 @@ class EnqueuedJobsTest < ActiveJob::TestCase
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
+
+ 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')
+ 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
@@ -158,10 +332,39 @@ 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
@@ -169,6 +372,17 @@ class EnqueuedJobsTest < ActiveJob::TestCase
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
@@ -177,32 +391,144 @@ class EnqueuedJobsTest < ActiveJob::TestCase
end
end
- def test_assert_enqueued_job
- assert_enqueued_with(job: LoggingJob, queue: 'default') do
+ 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_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_returns
+ 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)
+ 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], job.arguments
+ 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_job_failure
+ 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
@@ -210,7 +536,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
@@ -218,20 +558,94 @@ class EnqueuedJobsTest < ActiveJob::TestCase
end
end
- def test_assert_enqueued_job_with_at_option
+ 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_time
+ now = Time.now
+ args = [{ argument1: [now] }]
+
+ assert_enqueued_with(job: MultipleKwargsJob, args: args) do
+ MultipleKwargsJob.perform_later(argument1: [now])
+ end
+ end
+
+ def test_assert_enqueued_with_date_time
+ now = DateTime.now
+ args = [{ argument1: [now] }]
+
+ assert_enqueued_with(job: MultipleKwargsJob, args: args) do
+ MultipleKwargsJob.perform_later(argument1: [now])
+ end
+ end
+
+ def test_assert_enqueued_with_time_with_zone
+ now = Time.now.in_time_zone("Tokyo")
+ args = [{ argument1: [now] }]
+
+ assert_enqueued_with(job: MultipleKwargsJob, args: args) do
+ MultipleKwargsJob.perform_later(argument1: [now])
+ 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_job_with_global_id_args
+ 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
@@ -243,29 +657,206 @@ class EnqueuedJobsTest < ActiveJob::TestCase
assert_equal "No enqueued job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message
end
- def test_assert_enqueued_job_does_not_change_jobs_count
+ 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, ActiveJob::Base.queue_adapter.enqueued_jobs.count
+ 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
@@ -273,23 +864,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
@@ -299,15 +890,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
@@ -330,7 +921,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
@@ -340,8 +931,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
@@ -351,7 +942,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
@@ -361,22 +952,157 @@ 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_with_only_option_as_proc
+ assert_nothing_raised do
+ assert_performed_jobs(1, only: ->(job) { job.is_a?(HelloJob) }) do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("bogdan")
+ 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_as_proc
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ assert_performed_jobs(1, only: ->(job) { job.fetch(:job).name == "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_with_except_option_as_proc
+ assert_nothing_raised do
+ assert_performed_jobs(1, except: ->(job) { job.is_a?(HelloJob) }) do
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("bogdan")
+ 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_as_proc
+ HelloJob.perform_later("jeremy")
+ LoggingJob.perform_later("bogdan")
+
+ perform_enqueued_jobs
+
+ assert_performed_jobs(1, except: ->(job) { job.fetch(:job).name == "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
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_performed_jobs 1, only: HelloJob do
@@ -387,10 +1113,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
@@ -398,16 +1155,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
@@ -416,6 +1332,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
@@ -424,10 +1411,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
@@ -435,24 +1441,187 @@ 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
- NestedJob.perform_later
+ 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_performed_job_returns
- job = assert_performed_with(job: NestedJob, queue: 'default') do
+ 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_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
- assert_instance_of NestedJob, job
+ 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 [], job.arguments
- assert_equal 'default', job.queue_name
+ assert_equal [{ keyword: :sym }], job.arguments
+ assert_equal "default", job.queue_name
end
- def test_assert_performed_job_failure
+ def test_assert_performed_with_failure
assert_raise ActiveSupport::TestCase::Assertion do
assert_performed_with(job: LoggingJob) do
HelloJob.perform_later
@@ -460,13 +1629,29 @@ class PerformedJobsTest < ActiveJob::TestCase
end
assert_raise ActiveSupport::TestCase::Assertion do
- assert_performed_with(job: HelloJob, queue: 'low') do
- HelloJob.set(queue: 'important').perform_later
+ assert_performed_with(job: HelloJob, queue: "low") do
+ HelloJob.set(queue: "important").perform_later
end
end
end
- def test_assert_performed_job_with_at_option
+ 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
@@ -478,14 +1663,93 @@ class PerformedJobsTest < ActiveJob::TestCase
end
end
- def test_assert_performed_job_with_global_id_args
+ 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: MultipleKwargsJob, args: args) do
+ MultipleKwargsJob.perform_later(argument2: { b: 2, a: 1 }, argument1: 1)
+ end
+ end
+ end
+
+ def test_assert_performed_with_time
+ now = Time.now
+ args = [{ argument1: { now: now } }]
+
+ assert_enqueued_with(job: MultipleKwargsJob, args: args) do
+ MultipleKwargsJob.perform_later(argument1: { now: now })
+ end
+ end
+
+ def test_assert_performed_with_date_time
+ now = DateTime.now
+ args = [{ argument1: { now: now } }]
+
+ assert_enqueued_with(job: MultipleKwargsJob, args: args) do
+ MultipleKwargsJob.perform_later(argument1: { now: now })
+ end
+ end
+
+ def test_assert_performed_with_time_with_zone
+ now = Time.now.in_time_zone("Tokyo")
+ args = [{ argument1: { now: now } }]
+
+ assert_enqueued_with(job: MultipleKwargsJob, args: args) do
+ MultipleKwargsJob.perform_later(argument1: { now: now })
+ end
+ end
+
+ 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_block_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
@@ -497,7 +1761,19 @@ class PerformedJobsTest < ActiveJob::TestCase
assert_equal "No performed job found with {:job=>HelloJob, :args=>[#{wilma.inspect}]}", error.message
end
- def test_assert_performed_job_does_not_change_jobs_count
+ 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
@@ -506,6 +1782,53 @@ class PerformedJobsTest < ActiveJob::TestCase
HelloJob.perform_later
end
- assert_equal 2, ActiveJob::Base.queue_adapter.performed_jobs.count
+ 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
index d5e3aaf9e3..6a0d6d3e54 100644
--- a/activejob/test/cases/translation_test.rb
+++ b/activejob/test/cases/translation_test.rb
@@ -1,18 +1,20 @@
-require 'helper'
-require 'jobs/translated_hello_job'
+# 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')
+ @job = TranslatedHelloJob.new("Johannes")
end
teardown do
I18n.available_locales = [:en]
end
- test 'it performs the job in the given locale' do
+ 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
diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb
index 54b6076f09..694232d7ef 100644
--- a/activejob/test/helper.rb
+++ b/activejob/test/helper.rb
@@ -1,16 +1,18 @@
-require 'active_job'
-require 'support/job_buffer'
+# frozen_string_literal: true
-ActiveSupport.halt_callback_chains_on_return_false = false
-GlobalID.app = 'aj'
+require "active_job"
+require "support/job_buffer"
-@adapter = ENV['AJ_ADAPTER'] || 'inline'
+GlobalID.app = "aj"
-if ENV['AJ_INTEGRATION_TESTS']
- require 'support/integration/helper'
+@adapter = ENV["AJ_ADAPTER"] || "inline"
+puts "Using #{@adapter}"
+
+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 40f27500a5..1fa68a8ad5 100644
--- a/activejob/test/integration/queuing_test.rb
+++ b/activejob/test/integration/queuing_test.rb
@@ -1,16 +1,19 @@
-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
+ 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
@@ -24,51 +27,69 @@ 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 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)
- assert_not job_executed
- rescue NotImplementedError
- skip
+ 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 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)
- assert_not job_executed
- wait_for_jobs_to_finish_for(10.seconds)
- assert job_executed
- rescue NotImplementedError
- skip
- end
+ test "should supply a wrapped class name to DelayedJob" do
+ skip unless adapter_is?(:delayed_job)
+ ::HelloJob.perform_later
+ job = Delayed::Job.first
+ assert_match(/HelloJob \[[0-9a-f-]+\] from DelayedJob\(default\) with arguments: \[\]/, job.name)
+ end
+
+ test "resque JobWrapper should have instance variable queue" do
+ skip unless adapter_is?(:resque)
+ job = ::HelloJob.set(wait: 5.seconds).perform_later
+ 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
+ TestJob.set(wait: 10.minutes).perform_later @id
+ wait_for_jobs_to_finish_for(5.seconds)
+ assert_not job_executed
+ rescue NotImplementedError
+ skip
+ end
+
+ test "should run job enqueued in the future at the specified time" do
+ TestJob.set(wait: 5.seconds).perform_later @id
+ wait_for_jobs_to_finish_for(2.seconds)
+ assert_not job_executed
+ wait_for_jobs_to_finish_for(10.seconds)
+ assert job_executed
+ rescue NotImplementedError
+ skip
end
- test 'should supply a provider_job_id when available for immediate jobs' do
- skip unless adapter_is?(:async, :delayed_job, :sidekiq, :qu, :que, :queue_classic)
+ 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
- assert test_job.provider_job_id, '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
+ 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
- assert delayed_test_job.provider_job_id, '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
+ test "current locale is kept while running perform_later" do
skip if adapter_is?(:inline)
begin
@@ -78,14 +99,30 @@ class QueuingTest < ActiveSupport::TestCase
TestJob.perform_later @id
wait_for_jobs_to_finish_for(5.seconds)
assert job_executed
- assert_equal 'de', job_executed_in_locale
+ assert_equal "de", job_executed_in_locale
ensure
I18n.available_locales = [:en]
I18n.locale = :en
end
end
- test 'should run job with higher priority first' do
+ 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
@@ -96,4 +133,16 @@ class QueuingTest < ActiveSupport::TestCase
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/abort_before_enqueue_job.rb b/activejob/test/jobs/abort_before_enqueue_job.rb
new file mode 100644
index 0000000000..fd278eccf4
--- /dev/null
+++ b/activejob/test/jobs/abort_before_enqueue_job.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AbortBeforeEnqueueJob < ActiveJob::Base
+ before_enqueue { throw(:abort) }
+
+ def perform
+ raise "This should never be called"
+ 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
index 897aef52e5..0feee99e87 100644
--- a/activejob/test/jobs/queue_as_job.rb
+++ b/activejob/test/jobs/queue_as_job.rb
@@ -1,4 +1,6 @@
-require_relative '../support/job_buffer'
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
class QueueAsJob < ActiveJob::Base
MY_QUEUE = :low_priority
diff --git a/activejob/test/jobs/rescue_job.rb b/activejob/test/jobs/rescue_job.rb
index 4f6376c850..d142cec1ea 100644
--- a/activejob/test/jobs/rescue_job.rb
+++ b/activejob/test/jobs/rescue_job.rb
@@ -1,16 +1,18 @@
-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("rescued from DeserializationError")
JobBuffer.add("DeserializationError original exception was #{e.cause.class.name}")
end
@@ -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..112d672006
--- /dev/null
+++ b/activejob/test/jobs/retry_job.rb
@@ -0,0 +1,48 @@
+# 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, attempts: 4
+ 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}") }
+
+ before_enqueue do |job|
+ if job.arguments.include?(:log_scheduled_at) && job.scheduled_at
+ JobBuffer.add("Next execution scheduled at #{job.scheduled_at}")
+ end
+ end
+
+ def perform(raising, attempts, *)
+ raising = raising.shift if raising.is_a?(Array)
+ if raising && 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
index 9657cd3f54..a0a68b4040 100644
--- a/activejob/test/jobs/translated_hello_job.rb
+++ b/activejob/test/jobs/translated_hello_job.rb
@@ -1,8 +1,10 @@
-require_relative '../support/job_buffer'
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
class TranslatedHelloJob < ActiveJob::Base
def perform(greeter = "David")
- translations = { en: 'Hello', de: 'Guten Tag' }
+ translations = { en: "Hello", de: "Guten Tag" }
hello = translations[I18n.locale]
JobBuffer.add("#{greeter} says #{hello}")
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
index 44ab98437a..ba9674d7a1 100644
--- a/activejob/test/support/integration/adapters/async.rb
+++ b/activejob/test/support/integration/adapters/async.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module AsyncJobsManager
def setup
ActiveJob::Base.queue_adapter = :async
diff --git a/activejob/test/support/integration/adapters/backburner.rb b/activejob/test/support/integration/adapters/backburner.rb
index 2e82562948..0c248dda01 100644
--- a/activejob/test/support/integration/adapters/backburner.rb
+++ b/activejob/test/support/integration/adapters/backburner.rb
@@ -1,12 +1,16 @@
+# frozen_string_literal: true
+
module BackburnerJobsManager
def setup
ActiveJob::Base.queue_adapter = :backburner
Backburner.configure do |config|
+ config.beanstalk_url = ENV["BEANSTALK_URL"] if ENV["BEANSTALK_URL"]
config.logger = Rails.logger
end
unless can_run?
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 +27,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..cd129e72b2 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: ENV["REDIS_URL"] || "redis://127.0.0.1:6379/12", thread_safe: true)
Resque.logger = Rails.logger
unless can_run?
puts "Cannot run integration tests for resque. To be able to run integration tests for resque you need to install and start redis.\n"
- 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 2f19d7dacc..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
@@ -29,7 +31,7 @@ module SidekiqJobsManager
# Sidekiq is not warning-clean :(
$VERBOSE = false
- $stdin.reopen('/dev/null')
+ $stdin.reopen(File::NULL)
$stdout.sync = true
$stderr.sync = true
@@ -49,12 +51,12 @@ module SidekiqJobsManager
self_write.puts("TERM")
end
- 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,
- })
+ timeout: 1)
Sidekiq.average_scheduled_poll_interval = 0.5
Sidekiq.options[:poll_interval_average] = 1
begin
@@ -79,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..89dc61ca28 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: ENV["RABBITMQ_URL"] || "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 a0ef38b0b2..b56dd3e591 100644
--- a/activejob/test/support/integration/dummy_app_template.rb
+++ b/activejob/test/support/integration/dummy_app_template.rb
@@ -1,29 +1,31 @@
-if ENV['AJ_ADAPTER'] == 'delayed_job'
+# frozen_string_literal: true
+
+if ENV["AJ_ADAPTER"] == "delayed_job"
generate "delayed_job:active_record", "--quiet"
end
-rails_command("db:migrate")
-
-initializer 'activejob.rb', <<-CODE
-require "#{File.expand_path("../jobs_manager.rb", __FILE__)}"
+initializer "activejob.rb", <<-CODE
+require "#{File.expand_path("jobs_manager.rb", __dir__)}"
JobsManager.current_manager.setup
CODE
-initializer 'i18n.rb', <<-CODE
+initializer "i18n.rb", <<-CODE
I18n.available_locales = [:en, :de]
CODE
-file 'app/jobs/test_job.rb', <<-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}"), "wb+") do |f|
+ 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 4a1b0bfbcb..c5fa2b136f 100644
--- a/activejob/test/support/integration/helper.rb
+++ b/activejob/test/support/integration/helper.rb
@@ -1,12 +1,15 @@
-puts "\n\n*** 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 9897f76fd0..973ee07764 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,25 +29,23 @@ 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)
- begin
- Timeout.timeout(seconds) do
- while !job_executed do
- sleep 0.25
- end
+ def wait_for_jobs_to_finish_for(seconds = 60)
+ Timeout.timeout(seconds) do
+ while !job_executed do
+ sleep 0.25
end
- rescue Timeout::Error
end
+ rescue Timeout::Error
end
def job_file(id)
Dummy::Application.root.join("tmp/#{id}")
end
- def job_executed(id=@id)
+ def job_executed(id = @id)
job_file(id).exist?
end
@@ -54,11 +53,15 @@ module TestCaseHelpers
Marshal.load(File.binread(job_file(id)))
end
- def job_executed_at(id=@id)
+ def job_executed_at(id = @id)
job_data(id)["executed_at"]
end
- def job_executed_in_locale(id=@id)
+ 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 0950e52d28..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
@@ -9,6 +11,6 @@ Que::Job.class_eval do
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 fb7ab5cb40..ad87abfa3a 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,142 +1,150 @@
-## Rails 5.0.0.beta3 (February 24, 2016) ##
+* Type cast falsy boolean symbols on boolean attribute as false.
-* No changes.
+ Fixes #35676.
+ *Ryuta Kamizono*
-## Rails 5.0.0.beta2 (February 01, 2016) ##
+* Change how validation error translation strings are fetched: The new behavior
+ will first try the more specific keys, including doing locale fallback, then try
+ the less specific ones.
-* No changes.
+ For example, this is the order in which keys will now be tried for a `blank`
+ error on a `product`'s `title` attribute with current locale set to `en-US`:
+ en-US.activerecord.errors.models.product.attributes.title.blank
+ en-US.activerecord.errors.models.product.blank
+ en-US.activerecord.errors.messages.blank
-## Rails 5.0.0.beta1 (December 18, 2015) ##
+ en.activerecord.errors.models.product.attributes.title.blank
+ en.activerecord.errors.models.product.blank
+ en.activerecord.errors.messages.blank
-* Validate multiple contexts on `valid?` and `invalid?` at once.
+ en-US.errors.attributes.title.blank
+ en-US.errors.messages.blank
- Example:
+ en.errors.attributes.title.blank
+ en.errors.messages.blank
- class Person
- include ActiveModel::Validations
+ *Hugo Vacher*
- attr_reader :name, :title
- validates_presence_of :name, on: :create
- validates_presence_of :title, on: :update
- end
- person = Person.new
- person.valid?([:create, :update]) # => false
- person.errors.messages # => {:name=>["can't be blank"], :title=>["can't be blank"]}
+## Rails 6.0.0.beta3 (March 11, 2019) ##
- *Dmitry Polushkin*
+* No changes.
-* Add case_sensitive option for confirmation validator in models.
- *Akshat Sharma*
+## Rails 6.0.0.beta2 (February 25, 2019) ##
-* Ensure `method_missing` is called for methods passed to
- `ActiveModel::Serialization#serializable_hash` that don't exist.
+* Fix date value when casting a multiparameter date hash to not convert
+ from Gregorian date to Julian date.
- *Jay Elaraj*
+ Before:
-* Remove `ActiveModel::Serializers::Xml` from core.
+ Day.new({"day(1i)"=>"1", "day(2i)"=>"1", "day(3i)"=>"1"})
+ # => #<Day id: nil, day: "0001-01-03", created_at: nil, updated_at: nil>
- *Zachary Scott*
+ After:
-* 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.
+ Day.new({"day(1i)"=>"1", "day(2i)"=>"1", "day(3i)"=>"1"})
+ # => #<Day id: nil, day: "0001-01-01", created_at: nil, updated_at: nil>
- It makes the dirty-attributes query methods consistent before and after
- saving.
+ Fixes #28521.
- *Fernando Tapia Rico*
+ *Sayan Chakraborty*
-* Deprecate the `:tokenizer` option for `validates_length_of`, in favor of
- plain Ruby.
+* Fix year value when casting a multiparameter time hash.
- *Sean Griffin*
+ When assigning a hash to a time attribute that's missing a year component
+ (e.g. a `time_select` with `:ignore_date` set to `true`) then the year
+ defaults to 1970 instead of the expected 2000. This results in the attribute
+ changing as a result of the save.
-* Deprecate `ActiveModel::Errors#add_on_empty` and `ActiveModel::Errors#add_on_blank`
- with no replacement.
+ Before:
+ ```
+ event = Event.new(start_time: { 4 => 20, 5 => 30 })
+ event.start_time # => 1970-01-01 20:30:00 UTC
+ event.save
+ event.reload
+ event.start_time # => 2000-01-01 20:30:00 UTC
+ ```
- *Wojciech Wnętrzak*
+ After:
+ ```
+ event = Event.new(start_time: { 4 => 20, 5 => 30 })
+ event.start_time # => 2000-01-01 20:30:00 UTC
+ event.save
+ event.reload
+ event.start_time # => 2000-01-01 20:30:00 UTC
+ ```
-* Deprecate `ActiveModel::Errors#get`, `ActiveModel::Errors#set` and
- `ActiveModel::Errors#[]=` methods that have inconsistent behavior.
+ *Andrew White*
- *Wojciech Wnętrzak*
-* Allow symbol as values for `tokenize` of `LengthValidator`.
+## Rails 6.0.0.beta1 (January 18, 2019) ##
- *Kensuke Naito*
+* Add `ActiveModel::Errors#of_kind?`.
-* Assigning an unknown attribute key to an `ActiveModel` instance during initialization
- will now raise `ActiveModel::AttributeAssignment::UnknownAttributeError` instead of
- `NoMethodError`.
+ *bogdanvlviv*, *Rafael Mendonça França*
- Example:
+* Fix numericality equality validation of `BigDecimal` and `Float`
+ by casting to `BigDecimal` on both ends of the validation.
- User.new(foo: 'some value')
- # => ActiveModel::AttributeAssignment::UnknownAttributeError: unknown attribute 'foo' for User.
+ *Gannon McGibbon*
- *Eugene Gilburg*
+* Add `#slice!` method to `ActiveModel::Errors`.
-* Extracted `ActiveRecord::AttributeAssignment` to `ActiveModel::AttributeAssignment`
- allowing to use it for any object as an includable module.
+ *Daniel López Prat*
- Example:
+* Fix numericality validator to still use value before type cast except Active Record.
- class Cat
- include ActiveModel::AttributeAssignment
- attr_accessor :name, :status
- end
+ Fixes #33651, #33686.
- 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'
+ *Ryuta Kamizono*
- *Bogdan Gusiev*
+* Fix `ActiveModel::Serializers::JSON#as_json` method for timestamps.
-* Add `ActiveModel::Errors#details`
+ Before:
+ ```
+ contact = Contact.new(created_at: Time.utc(2006, 8, 1))
+ contact.as_json["created_at"] # => 2006-08-01 00:00:00 UTC
+ ```
- To be able to return type of used validator, one can now call `details`
- on errors instance.
+ After:
+ ```
+ contact = Contact.new(created_at: Time.utc(2006, 8, 1))
+ contact.as_json["created_at"] # => "2006-08-01T00:00:00.000Z"
+ ```
+
+ *Bogdan Gusiev*
+
+* 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_customize_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.5.0 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 Active Model `before_` callback had
- the side effect of halting the callback chain.
- This is not recommended anymore and, depending on the value of the
- `ActiveSupport.halt_callback_chains_on_return_false` option, will
- either not work at all or display a deprecation warning.
+ *Jeremy Daer*, *Kasper Timm Hansen*
- *claudiob*
-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 8573eb1225..ab7c27c209 100644
--- a/activemodel/MIT-LICENSE
+++ b/activemodel/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2016 David Heinemeier Hansson
+Copyright (c) 2004-2019 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 77f5761993..f9370d8de0 100644
--- a/activemodel/README.rdoc
+++ b/activemodel/README.rdoc
@@ -5,6 +5,8 @@ They allow for Action Pack helpers to interact with non-Active Record models,
for example. Active Model also helps with building custom ORMs for use outside of
the Rails framework.
+You can read more about Active Model in the {Active Model Basics}[https://edgeguides.rubyonrails.org/active_model_basics.html] guide.
+
Prior to Rails 3.0, if a plugin or gem developer wanted to have an object
interact with Action Pack helpers, it was required to either copy chunks of
code from Rails, or monkey patch entire helpers to make them handle objects
@@ -246,16 +248,16 @@ 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
+* https://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/activemodel/Rakefile b/activemodel/Rakefile
index 9982d49bcb..d39f50a962 100644
--- a/activemodel/Rakefile
+++ b/activemodel/Rakefile
@@ -1,15 +1,14 @@
-require 'rake/testtask'
+# frozen_string_literal: true
-dir = File.dirname(__FILE__)
+require "rake/testtask"
-task :default => :test
+task default: :test
task :package
-task "package:clean"
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)
@@ -17,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 1c3997b864..e681654439 100644
--- a/activemodel/activemodel.gemspec
+++ b/activemodel/activemodel.gemspec
@@ -1,22 +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.5.0"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "https://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://rubyonrails.org'
+ 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.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', '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 "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 b95c174d6e..c9140dc582 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2019 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
@@ -68,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 62014cd1cd..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.nil? || new_attributes.empty?
+ 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 cc6285f932..5c4670f393 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -1,5 +1,6 @@
-require 'concurrent/map'
-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,18 +328,15 @@ 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
# +matched_attribute_method+. The latter method iterates through an
@@ -349,11 +346,11 @@ module ActiveModel
# used to alleviate the GC, which ultimately also speeds up the app
# significantly (in our case our test suite finishes 10% faster with
# this cache).
- def attribute_method_matchers_cache #:nodoc:
+ def attribute_method_matchers_cache
@attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
end
- def attribute_method_matchers_matching(method_name) #:nodoc:
+ def attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix
# will match every time.
@@ -365,16 +362,16 @@ 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|"
end
- extra = (extra.map!(&:inspect) << "*args").join(", ".freeze)
+ 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}"
@@ -458,12 +455,11 @@ module ActiveModel
end
end
- protected
- def attribute_method?(attr_name) #:nodoc:
+ private
+ def attribute_method?(attr_name)
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
end
- private
# Returns a struct representing the matching attribute method.
# The struct's attributes are prefix, base and suffix.
def matched_attribute_method(method_name)
@@ -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.alias_method method_name, temp_method_name
+ mod.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..4679b33852
--- /dev/null
+++ b/activemodel/lib/active_model/attribute_set.rb
@@ -0,0 +1,106 @@
+# 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
+
+ def fetch_value(name, &block)
+ self[name].value(&block)
+ 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 0d6a3dc52d..ea2ed7dff7 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -1,4 +1,7 @@
-require 'active_support/core_ext/array/extract_options'
+# frozen_string_literal: true
+
+require "active_support/core_ext/array/extract_options"
+require "active_support/core_ext/hash/keys"
module ActiveModel
# == Active \Model \Callbacks
@@ -56,6 +59,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,12 +104,11 @@ 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 = {
- terminator: deprecated_false_terminator,
skip_after_callbacks_if_terminated: true,
scope: [:kind, :name],
only: [:before, :around, :after]
@@ -122,28 +127,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 6e2e5afd1b..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,8 +29,8 @@ module ActiveModel
#
# define_attribute_methods :name
#
- # def initialize(name)
- # @name = name
+ # def initialize
+ # @name = nil
# end
#
# def name
@@ -58,7 +61,7 @@ module ActiveModel
#
# A newly instantiated +Person+ object is unchanged:
#
- # person = Person.new("Uncle Bob")
+ # person = Person.new
# person.changed? # => false
#
# Change the name:
@@ -66,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:
#
@@ -80,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 # => {}
#
@@ -119,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
+
+ 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
- # Returns +true+ if any of the attributes have unsaved changes, +false+ otherwise.
+ # 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'
@@ -143,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>.
#
@@ -150,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.
@@ -161,46 +245,51 @@ module ActiveModel
# person.previous_changes # => {"name" => ["bob", "robert"]}
def previous_changes
@previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
+ @previously_changed.merge(mutations_before_last_save.changes)
end
- # Returns a hash of the attributes with unsaved changes indicating their original
- # values like <tt>attr => original value</tt>.
- #
- # person.name # => "bob"
- # person.name = 'robert'
- # person.changed_attributes # => {"name" => "bob"}
- def changed_attributes
- @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
+ def attribute_changed_in_place?(attr_name) # :nodoc:
+ mutations_from_database.changed_in_place?(attr_name)
end
- # Handles <tt>*_changed?</tt> for +method_missing+.
- def attribute_changed?(attr, 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?
@@ -210,39 +299,28 @@ module ActiveModel
previous_changes.include?(attr_name)
end
- # Removes current changes and makes them accessible through +previous_changes+.
- def changes_applied # :doc:
- @previously_changed = changes
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- end
-
- # Clears all dirty data: current changes and previous changes.
- def clear_changes_information # :doc:
- @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- end
-
# Handles <tt>*_change</tt> for +method_missing+.
def attribute_change(attr)
- [changed_attributes[attr], __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)
- previous_changes[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+.
@@ -253,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 836201535f..3a692a3e64 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -1,7 +1,9 @@
-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'
+# 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"
module ActiveModel
# == Active \Model \Errors
@@ -60,6 +62,11 @@ module ActiveModel
CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
MESSAGE_OPTIONS = [:message]
+ class << self
+ attr_accessor :i18n_customize_full_message # :nodoc:
+ end
+ self.i18n_customize_full_message = false
+
attr_reader :messages, :details
# Pass in the instance of the object that is using the errors object.
@@ -71,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:
@@ -93,6 +100,29 @@ module ActiveModel
@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
+
+ # Removes all errors except the given keys. Returns a hash containing the removed errors.
+ #
+ # person.errors.keys # => [:name, :age, :gender, :city]
+ # person.errors.slice!(:age, :gender) # => { :name=>["cannot be nil"], :city=>["cannot be nil"] }
+ # person.errors.keys # => [:age, :gender]
+ def slice!(*keys)
+ keys = keys.map(&:to_sym)
+ @details.slice!(*keys)
+ @messages.slice!(*keys)
+ end
+
# Clear the error messages.
#
# person.errors.full_messages # => ["name cannot be nil"]
@@ -110,49 +140,21 @@ module ActiveModel
# person.errors.include?(:name) # => true
# person.errors.include?(:age) # => false
def include?(attribute)
+ attribute = attribute.to_sym
messages.key?(attribute) && messages[attribute].present?
end
alias :has_key? :include?
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
@@ -160,33 +162,10 @@ module ActiveModel
#
# person.errors[:name] # => ["cannot be nil"]
# person.errors['name'] # => ["cannot be nil"]
- #
- # Note that, if you try to get errors of an attribute which has
- # no errors associated with it, this method will instantiate
- # an empty error list for it and +keys+ will return an array
- # of error keys which includes this attribute.
- #
- # person.errors.keys # => []
- # person.errors[:name] # => []
- # person.errors.keys # => [:name]
def [](attribute)
messages[attribute.to_sym]
end
- # 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.
@@ -223,7 +202,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.
@@ -231,7 +212,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.
@@ -255,7 +238,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
@@ -265,7 +248,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
@@ -276,11 +259,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
@@ -310,9 +293,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 # => {}
#
@@ -338,58 +321,51 @@ 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?).
+ # Returns +true+ if an error on the attribute with the given message is
+ # present, or +false+ otherwise. +message+ is treated the same as for +add+.
#
- # 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
+ # person.errors.add :name, :blank
+ # person.errors.added? :name, :blank # => true
+ # person.errors.added? :name, "can't be blank" # => true
+ #
+ # If the error message requires options, then it returns +true+ with
+ # the correct options, or +false+ with incorrect or missing options.
+ #
+ # 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)
- Array(attributes).each do |attribute|
- value = @base.send(:read_attribute_for_validation, attribute)
- add(attribute, :blank, options) if value.blank?
+ if message.is_a? Symbol
+ details[attribute.to_sym].include? normalize_detail(message, options)
+ else
+ self[attribute].include? message
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+.
- #
- # person.errors.add :name, :blank
- # person.errors.added? :name, :blank # => true
- def added?(attribute, message = :invalid, options = {})
+ # present, or +false+ otherwise. +message+ is treated the same as for +add+.
+ #
+ # person.errors.add :age
+ # person.errors.add :name, :too_long, { count: 25 }
+ # person.errors.of_kind? :age # => true
+ # person.errors.of_kind? :name # => false
+ # person.errors.of_kind? :name, :too_long # => true
+ # person.errors.of_kind? :name, "is too long (maximum is 25 characters)" # => true
+ # person.errors.of_kind? :name, :not_too_long # => false
+ # person.errors.of_kind? :name, "is too long" # => false
+ def of_kind?(attribute, message = :invalid)
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].map { |e| e[:error] }.include? message
+ else
+ self[attribute].include? message
+ end
end
# Returns all the full error messages in an array.
@@ -418,21 +394,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_customize_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
@@ -461,37 +479,58 @@ module ActiveModel
# * <tt>errors.messages.blank</tt>
def generate_message(attribute, type = :invalid, options = {})
type = options.delete(:message) if options[:message].is_a?(Symbol)
+ value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
+
+ options = {
+ model: @base.model_name.human,
+ attribute: @base.class.human_attribute_name(attribute),
+ value: value,
+ object: @base
+ }.merge!(options)
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}"
+
+ catch(:exception) do
+ translation = I18n.translate(defaults.first, options.merge(default: defaults.drop(1), throw: true))
+ return translation unless translation.nil?
+ end unless options[:message]
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)
-
- options = {
- default: defaults,
- model: @base.model_name.human,
- attribute: @base.class.human_attribute_name(attribute),
- value: value
- }.merge!(options)
+ options[:default] = defaults
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
@@ -505,6 +544,17 @@ module ActiveModel
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
@@ -525,7 +575,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 d2c6a89cc2..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,7 +17,7 @@ module ActiveModel
end
module ForbiddenAttributesProtection # :nodoc:
- protected
+ private
def sanitize_for_mass_assignment(attributes)
if attributes.respond_to?(:permitted?)
raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
index 62862aa4e9..1626aac468 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 = "beta3"
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 d86ef6224e..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)
@@ -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('/'.freeze, '_'.freeze)
- 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
@@ -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..eb7901c7e9 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_customize_full_message" do
+ ActiveModel::Errors.i18n_customize_full_message = config.active_model.delete(:i18n_customize_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 70e10fa06d..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
@@ -177,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 b64a8299e6..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, instance_writer: false
- 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/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
index 6ec3452478..1d7a26fff5 100644
--- a/activemodel/lib/active_model/type.rb
+++ b/activemodel/lib/active_model/type.rb
@@ -1,22 +1,21 @@
-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/decimal_without_scale'
-require 'active_model/type/float'
-require 'active_model/type/immutable_string'
-require 'active_model/type/integer'
-require 'active_model/type/string'
-require 'active_model/type/text'
-require 'active_model/type/time'
-require 'active_model/type/unsigned_integer'
-
-require 'active_model/type/registry'
+# 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
@@ -24,16 +23,8 @@ module ActiveModel
class << self
attr_accessor :registry # :nodoc:
- delegate :add_modifier, to: :registry
-
- # Add a new type to the registry, allowing it to be referenced as a
- # symbol by ActiveModel::Attributes::ClassMethods#attribute. If your
- # type is only meant to be used with a specific database adapter, you can
- # do so by passing +adapter: :postgresql+. If your type has the same
- # name as a native type for the current adapter, an exception will be
- # raised unless you specify an +:override+ option. +override: true+ will
- # cause your type to be used instead of the native type. +override:
- # false+ will cause the native type to be used over yours if one exists.
+
+ # Add a new type to the registry, allowing it to be gotten through ActiveModel::Type#lookup
def register(type_name, klass = nil, **options, &block)
registry.register(type_name, klass, **options, &block)
end
@@ -41,6 +32,10 @@ module ActiveModel
def lookup(*args, **kwargs) # :nodoc:
registry.lookup(*args, **kwargs)
end
+
+ def default_value # :nodoc:
+ @default_value ||= Value.new
+ end
end
register(:big_integer, Type::BigInteger)
@@ -53,7 +48,6 @@ module ActiveModel
register(:immutable_string, Type::ImmutableString)
register(:integer, Type::Integer)
register(:string, Type::String)
- register(:text, Type::Text)
register(:time, Type::Time)
end
end
diff --git a/activemodel/lib/active_model/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb
index 4168cbfce7..89e43bcc5f 100644
--- a/activemodel/lib/active_model/type/big_integer.rb
+++ b/activemodel/lib/active_model/type/big_integer.rb
@@ -1,13 +1,15 @@
-require 'active_model/type/integer'
+# frozen_string_literal: true
+
+require "active_model/type/integer"
module ActiveModel
module Type
class BigInteger < Integer # :nodoc:
private
- def max_value
- ::Float::INFINITY
- end
+ 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
index a0cc45b4c3..76203c5a88 100644
--- a/activemodel/lib/active_model/type/binary.rb
+++ b/activemodel/lib/active_model/type/binary.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class Binary < Value # :nodoc:
@@ -38,7 +40,7 @@ module ActiveModel
alias_method :to_str, :to_s
def hex
- @value.unpack('H*')[0]
+ @value.unpack1("H*")
end
def ==(other)
diff --git a/activemodel/lib/active_model/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb
index c1bce98c87..e64d2c793c 100644
--- a/activemodel/lib/active_model/type/boolean.rb
+++ b/activemodel/lib/active_model/type/boolean.rb
@@ -1,21 +1,47 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
- class Boolean < Value # :nodoc:
- FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set
+ # == 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", :"0",
+ "f", :f,
+ "F", :F,
+ "false", :false,
+ "FALSE", :FALSE,
+ "off", :off,
+ "OFF", :OFF,
+ ].to_set.freeze
- def type
+ 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)
+ def cast_value(value)
+ if value == ""
+ nil
+ else
+ !FALSE_VALUES.include?(value)
+ end
end
- end
end
end
end
diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb
index f74243a22c..c5fe926039 100644
--- a/activemodel/lib/active_model/type/date.rb
+++ b/activemodel/lib/active_model/type/date.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class Date < Value # :nodoc:
+ include Helpers::Timezone
include Helpers::AcceptsMultiparameterTime.new
def type
@@ -8,43 +11,43 @@ module ActiveModel
end
def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
+ 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
+ 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
- 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
+ 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
- end
- def fallback_string_to_date(string)
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
- 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
+ def new_date(year, mon, mday)
+ unless year.nil? || (year == 0 && mon == 0 && mday == 0)
+ ::Date.new(year, mon, mday) rescue nil
+ end
end
- end
- def value_from_multiparameter_assignment(*)
- time = super
- time && time.to_date
- end
+ def value_from_multiparameter_assignment(*)
+ time = super
+ time && new_date(time.year, time.mon, time.mday)
+ end
end
end
end
diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb
index 2f2df4320f..133410e821 100644
--- a/activemodel/lib/active_model/type/date_time.rb
+++ b/activemodel/lib/active_model/type/date_time.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class DateTime < Value # :nodoc:
+ include Helpers::Timezone
include Helpers::TimeValue
include Helpers::AcceptsMultiparameterTime.new(
defaults: { 4 => 0, 5 => 0 }
@@ -12,33 +15,33 @@ module ActiveModel
private
- def cast_value(value)
- return apply_seconds_precision(value) unless value.is_a?(::String)
- return if value.empty?
+ 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
+ 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
+ # '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)
+ 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
+ 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
+ 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
- super
- end
end
end
end
diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb
index 11ea327026..e8ee18c00e 100644
--- a/activemodel/lib/active_model/type/decimal.rb
+++ b/activemodel/lib/active_model/type/decimal.rb
@@ -1,9 +1,12 @@
+# frozen_string_literal: true
+
require "bigdecimal/util"
module ActiveModel
module Type
class Decimal < Value # :nodoc:
include Helpers::Numeric
+ BIGDECIMAL_PRECISION = 18
def type
:decimal
@@ -15,46 +18,53 @@ module ActiveModel
private
- def cast_value(value)
- casted_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
+ 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
+ apply_scale(casted_value)
+ end
- def convert_float_to_big_decimal(value)
- if precision
- BigDecimal(apply_scale(value), float_precision)
- else
- value.to_d
+ def convert_float_to_big_decimal(value)
+ if precision
+ BigDecimal(apply_scale(value), float_precision)
+ else
+ value.to_d
+ end
end
- end
- def float_precision
- if precision.to_i > ::Float::DIG + 1
- ::Float::DIG + 1
- else
- precision.to_i
+ def float_precision
+ if precision.to_i > ::Float::DIG + 1
+ ::Float::DIG + 1
+ else
+ precision.to_i
+ end
end
- end
- def apply_scale(value)
- if scale
- value.round(scale)
- else
- value
+ def apply_scale(value)
+ if scale
+ value.round(scale)
+ else
+ value
+ end
end
- end
end
end
end
diff --git a/activemodel/lib/active_model/type/decimal_without_scale.rb b/activemodel/lib/active_model/type/decimal_without_scale.rb
deleted file mode 100644
index 129baa0c10..0000000000
--- a/activemodel/lib/active_model/type/decimal_without_scale.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'active_model/type/big_integer'
-
-module ActiveModel
- module Type
- class DecimalWithoutScale < BigInteger # :nodoc:
- def type
- :decimal
- end
- end
- end
-end
diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb
index 0f925bc7e1..ea1987df7c 100644
--- a/activemodel/lib/active_model/type/float.rb
+++ b/activemodel/lib/active_model/type/float.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class Float < Value # :nodoc:
@@ -7,19 +9,26 @@ module ActiveModel
:float
end
- alias serialize cast
+ 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
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
+ 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
end
diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb
index a805a359ab..20145d5f0d 100644
--- a/activemodel/lib/active_model/type/helpers.rb
+++ b/activemodel/lib/active_model/type/helpers.rb
@@ -1,4 +1,7 @@
-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'
+# 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"
+require "active_model/type/helpers/timezone"
diff --git a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
index facea12704..e15d7b013f 100644
--- a/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
+++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
@@ -1,8 +1,14 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
- module Helpers
- class AcceptsMultiparameterTime < Module # :nodoc:
+ module Helpers # :nodoc: all
+ class AcceptsMultiparameterTime < Module
def initialize(defaults: {})
+ define_method(:serialize) do |value|
+ super(cast(value))
+ end
+
define_method(:cast) do |value|
if value.is_a?(Hash)
value_from_multiparameter_assignment(value)
@@ -19,6 +25,10 @@ module ActiveModel
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
diff --git a/activemodel/lib/active_model/type/helpers/mutable.rb b/activemodel/lib/active_model/type/helpers/mutable.rb
index 4dddbe4e5e..1cbea644c4 100644
--- a/activemodel/lib/active_model/type/helpers/mutable.rb
+++ b/activemodel/lib/active_model/type/helpers/mutable.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
- module Helpers
- module Mutable # :nodoc:
+ module Helpers # :nodoc: all
+ module Mutable
def cast(value)
deserialize(serialize(value))
end
diff --git a/activemodel/lib/active_model/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb
index c883010506..1d8171e25b 100644
--- a/activemodel/lib/active_model/type/helpers/numeric.rb
+++ b/activemodel/lib/active_model/type/helpers/numeric.rb
@@ -1,14 +1,21 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
- module Helpers
- module Numeric # :nodoc:
+ module Helpers # :nodoc: all
+ module Numeric
+ def serialize(value)
+ cast(value)
+ end
+
def cast(value)
- value = case value
- when true then 1
- when false then 0
- when ::String then value.presence
- else value
- end
+ value = \
+ case value
+ when true then 1
+ when false then 0
+ when ::String then value.presence
+ else value
+ end
super(value)
end
@@ -18,16 +25,19 @@ module ActiveModel
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 number_to_non_number?(old_value, new_value_before_type_cast)
+ old_value != nil && non_numeric_string?(new_value_before_type_cast.to_s)
+ 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
+ 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.
+ !NUMERIC_REGEX.match?(value)
+ end
+
+ NUMERIC_REGEX = /\A\s*[+-]?\d/
+ private_constant :NUMERIC_REGEX
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
index 63993c0d93..735b9a75a6 100644
--- a/activemodel/lib/active_model/type/helpers/time_value.rb
+++ b/activemodel/lib/active_model/type/helpers/time_value.rb
@@ -1,9 +1,12 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/string/zones"
require "active_support/core_ext/time/zones"
module ActiveModel
module Type
- module Helpers
- module TimeValue # :nodoc:
+ module Helpers # :nodoc: all
+ module TimeValue
def serialize(value)
value = apply_seconds_precision(value)
@@ -18,27 +21,15 @@ module ActiveModel
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 / round_power * round_power)
+ 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)}'"
+ value.to_s(:db).inspect
end
def user_input_in_time_zone(value)
@@ -47,30 +38,36 @@ module ActiveModel
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)
+ 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
+ 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
+ time -= offset
+ is_utc? ? time : time.getlocal
+ else
+ ::Time.public_send(default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ end
end
- end
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
+ 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 = ($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
+ # 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/helpers/timezone.rb b/activemodel/lib/active_model/type/helpers/timezone.rb
new file mode 100644
index 0000000000..cf87b9715b
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers/timezone.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/time/zones"
+
+module ActiveModel
+ module Type
+ module Helpers # :nodoc: all
+ module Timezone
+ def is_utc?
+ ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC"
+ end
+
+ def default_timezone
+ is_utc? ? :utc : :local
+ 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
index 20b8ca0cc4..826bd7038f 100644
--- a/activemodel/lib/active_model/type/immutable_string.rb
+++ b/activemodel/lib/active_model/type/immutable_string.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class ImmutableString < Value # :nodoc:
@@ -16,14 +18,15 @@ module ActiveModel
private
- def cast_value(value)
- result = case value
- when true then "t"
- when false then "f"
- else value.to_s
- end
- result.freeze
- end
+ 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
index 2f73ede009..1e1061ff60 100644
--- a/activemodel/lib/active_model/type/integer.rb
+++ b/activemodel/lib/active_model/type/integer.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
module ActiveModel
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.
+ # 4 bytes means an integer as opposed to smallint etc.
DEFAULT_LIMIT = 4
def initialize(*)
@@ -16,51 +18,36 @@ module ActiveModel
: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
+ return if value.is_a?(::String) && non_numeric_string?(value)
+ ensure_in_range(super)
end
- protected
-
- attr_reader :range
-
private
+ attr_reader :range
- def cast_value(value)
- case value
- when true then 1
- when false then 0
- else
+ def cast_value(value)
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}"
+ def ensure_in_range(value)
+ if value && !range.cover?(value)
+ raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
+ end
+ value
end
- end
- def max_value
- 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign
- 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 min_value
+ -max_value
+ end
- def _limit
- self.limit || DEFAULT_LIMIT
- 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
index adc88eb624..a19dc0f011 100644
--- a/activemodel/lib/active_model/type/registry.rb
+++ b/activemodel/lib/active_model/type/registry.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
# :stopdoc:
module Type
@@ -21,19 +23,16 @@ module ActiveModel
end
end
- protected
-
- attr_reader :registrations
-
private
+ attr_reader :registrations
- def registration_klass
- Registration
- end
+ def registration_klass
+ Registration
+ end
- def find_registration(symbol, *args)
- registrations.find { |r| r.matches?(symbol, *args) }
- end
+ def find_registration(symbol, *args)
+ registrations.find { |r| r.matches?(symbol, *args) }
+ end
end
class Registration
@@ -55,9 +54,8 @@ module ActiveModel
type_name == name
end
- protected
-
- attr_reader :name, :block
+ private
+ attr_reader :name, :block
end
end
# :startdoc:
diff --git a/activemodel/lib/active_model/type/string.rb b/activemodel/lib/active_model/type/string.rb
index 8a91410998..a9c9bfadb6 100644
--- a/activemodel/lib/active_model/type/string.rb
+++ b/activemodel/lib/active_model/type/string.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "active_model/type/immutable_string"
module ActiveModel
@@ -11,9 +13,14 @@ module ActiveModel
private
- def cast_value(value)
- ::String.new(super)
- end
+ 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/text.rb b/activemodel/lib/active_model/type/text.rb
deleted file mode 100644
index 1ad04daba4..0000000000
--- a/activemodel/lib/active_model/type/text.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'active_model/type/string'
-
-module ActiveModel
- module Type
- class Text < String # :nodoc:
- def type
- :text
- end
- end
- end
-end
diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb
index 34e09f0aba..61847a4ce7 100644
--- a/activemodel/lib/active_model/type/time.rb
+++ b/activemodel/lib/active_model/type/time.rb
@@ -1,9 +1,12 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class Time < Value # :nodoc:
+ include Helpers::Timezone
include Helpers::TimeValue
include Helpers::AcceptsMultiparameterTime.new(
- defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
+ defaults: { 1 => 2000, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
)
def type
@@ -16,6 +19,8 @@ module ActiveModel
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
@@ -25,22 +30,18 @@ module ActiveModel
private
- def cast_value(value)
- return value unless value.is_a?(::String)
- return if value.empty?
+ def cast_value(value)
+ return apply_seconds_precision(value) unless value.is_a?(::String)
+ return if value.empty?
- if value =~ /^2000-01-01/
- dummy_time_value = value
- else
- dummy_time_value = "2000-01-01 #{value}"
- end
+ dummy_time_value = value.sub(/\A(\d\d\d\d-\d\d-\d\d |)/, "2000-01-01 ")
- fast_string_to_time(dummy_time_value) || begin
- time_hash = ::Date._parse(dummy_time_value)
- return if time_hash[:hour].nil?
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
+ 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
end
diff --git a/activemodel/lib/active_model/type/unsigned_integer.rb b/activemodel/lib/active_model/type/unsigned_integer.rb
deleted file mode 100644
index 3f49f9f5f7..0000000000
--- a/activemodel/lib/active_model/type/unsigned_integer.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module ActiveModel
- module Type
- class UnsignedInteger < Integer # :nodoc:
- private
-
- def max_value
- super * 2
- end
-
- def min_value
- 0
- end
- end
- end
-end
diff --git a/activemodel/lib/active_model/type/value.rb b/activemodel/lib/active_model/type/value.rb
index 0d2d6873a8..b6914dd63c 100644
--- a/activemodel/lib/active_model/type/value.rb
+++ b/activemodel/lib/active_model/type/value.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveModel
module Type
class Value
@@ -84,6 +86,14 @@ module ActiveModel
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
@@ -105,12 +115,12 @@ module ActiveModel
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
+ # 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 8159b9b1d3..f18f9a601a 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -1,9 +1,8 @@
-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"
+module ActiveModel
# == Active \Model \Validations
#
# Provides a full validation framework to your objects.
@@ -50,8 +49,7 @@ module ActiveModel
private :validation_context=
define_callbacks :validate, scope: :name
- class_attribute :_validators, instance_writer: false
- self._validators = Hash.new { |h,k| h[k] = [] }
+ class_attribute :_validators, instance_writer: false, default: Hash.new { |h, k| h[k] = [] }
end
module ClassMethods
@@ -69,7 +67,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>)
@@ -135,7 +133,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>)
@@ -148,6 +146,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!
@@ -161,14 +162,14 @@ module ActiveModel
if options.key?(:on)
options = options.dup
+ options[:on] = Array(options[:on])
options[:if] = Array(options[:if])
options[:if].unshift ->(o) {
- !(Array(options[:on]) & Array(o.validation_context)).empty?
+ !(options[:on] & Array(o.validation_context)).empty?
}
end
- args << options
- set_callback(:validate, *args, &block)
+ set_callback(:validate, *args, options, &block)
end
# List all validators that are being used to validate the model using
@@ -304,8 +305,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
#
@@ -402,14 +401,14 @@ module ActiveModel
# end
alias :read_attribute_for_validation :send
- protected
+ private
- def run_validations! #:nodoc:
+ def run_validations!
_run_validate_callbacks
errors.empty?
end
- def raise_validation_error
+ def raise_validation_error # :doc:
raise(ValidationError.new(self))
end
end
@@ -435,4 +434,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 c5c0cd4636..6fd54270f2 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)
@@ -15,62 +16,56 @@ module ActiveModel
private
- def setup!(klass)
- klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes)))
- end
+ def setup!(klass)
+ klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes)))
+ end
- def acceptable_option?(value)
- Array(options[:accept]).include?(value)
- 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
+ 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)
+ 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
- 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
+ class AttributeDefinition
+ def initialize(attributes)
+ @attributes = attributes.map(&:to_s)
+ end
- protected
+ def matches?(method_name)
+ attr_name = convert_to_reader_name(method_name)
+ attributes.include?(attr_name)
+ end
- attr_reader :attributes
+ def define_on(klass)
+ attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
+ attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
+ klass.define_attribute_methods
+ klass.attr_reader(*attr_readers)
+ klass.attr_writer(*attr_writers)
+ end
- private
+ private
+ attr_reader :attributes
- def convert_to_reader_name(method_name)
- attr_name = method_name.to_s
- if attr_name.end_with?("=")
- attr_name = attr_name[0..-2]
- end
- attr_name
+ def convert_to_reader_name(method_name)
+ method_name.to_s.chomp("=")
+ end
end
- end
end
module HelperMethods
@@ -98,7 +93,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 52111e5442..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
@@ -23,14 +25,12 @@ module ActiveModel
included do
include ActiveSupport::Callbacks
define_callbacks :validation,
- terminator: deprecated_false_terminator,
skip_after_callbacks_if_terminated: true,
scope: [:kind, :name]
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
@@ -54,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
@@ -94,22 +96,25 @@ 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:
+ def run_validations!
_run_validation_callbacks { super }
end
end
diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb
index d49af603bb..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,7 +32,7 @@ module ActiveModel
@delimiter ||= options[:in] || options[:within]
end
- # In Ruby 2.2 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
+ # After Ruby 2.2, <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
# possible values in the range for equality, which is slower but more accurate.
# <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
# endpoints, which is fast but is only accurate on Numeric, Time, Date,
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
index 8f8ade90bb..b549755ba4 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -1,5 +1,6 @@
-module ActiveModel
+# frozen_string_literal: true
+module ActiveModel
module Validations
class ConfirmationValidator < EachValidator # :nodoc:
def initialize(options)
@@ -8,7 +9,7 @@ module ActiveModel
end
def validate_each(record, attribute, value)
- if (confirmed = record.send("#{attribute}_confirmation"))
+ unless (confirmed = record.send("#{attribute}_confirmation")).nil?
unless confirmation_value_equal?(record, attribute, value, confirmed)
human_attribute_name = record.class.human_attribute_name(attribute)
record.errors.add(:"#{attribute}_confirmation", :confirmation, options.except(:case_sensitive).merge!(attribute: human_attribute_name))
@@ -17,23 +18,23 @@ module ActiveModel
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.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.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
+ 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
end
module HelperMethods
@@ -70,7 +71,7 @@ module ActiveModel
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
def validates_confirmation_of(*attr_names)
validates_with ConfirmationValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb
index 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..02759b4ccb 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 non-negative 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 ad7012df48..51e224d5cd 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -1,5 +1,8 @@
-module ActiveModel
+# frozen_string_literal: true
+
+require "bigdecimal/util"
+module ActiveModel
module Validations
class NumericalityValidator < EachValidator # :nodoc:
CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
@@ -8,6 +11,8 @@ module ActiveModel
RESERVED_OPTIONS = CHECKS.keys + [:only_integer]
+ INTEGER_REGEX = /\A[+-]?\d+\z/
+
def check_validity!
keys = CHECKS.keys - [:odd, :even]
options.slice(*keys).each do |option, value|
@@ -18,17 +23,26 @@ 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) && record.send(before_type_cast) != value
+ 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 is_number?(raw_value)
record.errors.add(attr_name, :not_a_number, filtered_options(raw_value))
return
@@ -39,9 +53,7 @@ module ActiveModel
return
end
- unless raw_value.is_a?(Numeric)
- value = parse_raw_value_as_a_number(raw_value)
- end
+ value = parse_as_number(raw_value)
options.slice(*CHECKS.keys).each do |option, option_value|
case option
@@ -57,6 +69,8 @@ module ActiveModel
option_value = record.send(option_value)
end
+ option_value = parse_as_number(option_value)
+
unless value.send(CHECKS[option], option_value)
record.errors.add(attr_name, option, filtered_options(value).merge!(count: option_value))
end
@@ -64,20 +78,32 @@ module ActiveModel
end
end
- protected
+ private
def is_number?(raw_value)
- !parse_raw_value_as_a_number(raw_value).nil?
+ !parse_as_number(raw_value).nil?
rescue ArgumentError, TypeError
false
end
- def parse_raw_value_as_a_number(raw_value)
- Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
+ def parse_as_number(raw_value)
+ if raw_value.is_a?(Float)
+ raw_value.to_d
+ elsif raw_value.is_a?(Numeric)
+ raw_value
+ elsif is_integer?(raw_value)
+ raw_value.to_i
+ elsif !is_hexadecimal_literal?(raw_value)
+ Kernel.Float(raw_value).to_d
+ end
end
def is_integer?(raw_value)
- /\A[+-]?\d+\z/ === raw_value.to_s
+ INTEGER_REGEX.match?(raw_value.to_s)
+ end
+
+ def is_hexadecimal_literal?(raw_value)
+ /\A0[xX]/.match?(raw_value.to_s)
end
def filtered_options(value)
@@ -97,8 +123,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)
@@ -108,7 +132,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
@@ -120,7 +144,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.
@@ -139,7 +163,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 1da4df21e7..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?('::'.freeze) ? 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 109bf038b0..94d53b8dd1 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,13 +84,13 @@ 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
# def initialize(options={})
# super
- # options[:class].send :attr_accessor, :custom_attribute
+ # options[:class].attr_accessor :custom_attribute
# end
# end
class 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)
@@ -175,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 287bea719c..30e8419685 100644
--- a/activemodel/test/cases/attribute_assignment_test.rb
+++ b/activemodel/test/cases/attribute_assignment_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "active_support/core_ext/hash/indifferent_access"
require "active_support/hash_with_indifferent_access"
@@ -16,9 +18,8 @@ class AttributeAssignmentTest < ActiveModel::TestCase
raise ErrorFromAttributeWriter
end
- protected
-
- attr_writer :metadata
+ private
+ attr_writer :metadata
end
class ErrorFromAttributeWriter < StandardError
@@ -67,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
@@ -91,9 +100,11 @@ class AttributeAssignmentTest < ActiveModel::TestCase
end
test "an ArgumentError is raised if a non-hash-like object is passed" do
- assert_raises(ArgumentError) 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..ebb6cc542d 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,39 +96,37 @@ 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
- begin
- ModelWithAttributes.define_attribute_method(:foo)
+ test "#define_attribute_method generates attribute method" do
+ ModelWithAttributes.define_attribute_method(:foo)
- assert_respond_to ModelWithAttributes.new, :foo
- assert_equal "value of foo", ModelWithAttributes.new.foo
- ensure
- ModelWithAttributes.undefine_attribute_methods
- end
+ assert_respond_to ModelWithAttributes.new, :foo
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
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,43 +134,37 @@ 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
- begin
- ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b')
+ test "#define_attribute_method generates attribute method with invalid identifier characters" do
+ ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b')
- assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b'
- assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send('a?b')
- ensure
- ModelWithWeirdNamesAttributes.undefine_attribute_methods
- end
+ assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b'
+ assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send("a?b")
+ ensure
+ ModelWithWeirdNamesAttributes.undefine_attribute_methods
end
- test '#define_attribute_methods works passing multiple arguments' do
- begin
- ModelWithAttributes.define_attribute_methods(:foo, :baz)
+ test "#define_attribute_methods works passing multiple arguments" do
+ ModelWithAttributes.define_attribute_methods(:foo, :baz)
- assert_equal "value of foo", ModelWithAttributes.new.foo
- assert_equal "value of baz", ModelWithAttributes.new.baz
- ensure
- ModelWithAttributes.undefine_attribute_methods
- end
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ assert_equal "value of baz", ModelWithAttributes.new.baz
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
end
- test '#define_attribute_methods generates attribute methods' do
- begin
- ModelWithAttributes.define_attribute_methods(:foo)
+ test "#define_attribute_methods generates attribute methods" do
+ ModelWithAttributes.define_attribute_methods(:foo)
- assert_respond_to ModelWithAttributes.new, :foo
- assert_equal "value of foo", ModelWithAttributes.new.foo
- ensure
- ModelWithAttributes.undefine_attribute_methods
- end
+ assert_respond_to ModelWithAttributes.new, :foo
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
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,64 +173,58 @@ 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
- begin
- ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
+ test "#define_attribute_methods generates attribute methods with spaces in their names" do
+ ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
- assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar'
- assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar')
- ensure
- ModelWithAttributesWithSpaces.undefine_attribute_methods
- end
+ assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar'
+ assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar')
+ ensure
+ ModelWithAttributesWithSpaces.undefine_attribute_methods
end
- 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')
+ test "#alias_attribute works with attributes with spaces in their names" do
+ ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
+ ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar')
- assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar
- ensure
- ModelWithAttributesWithSpaces.undefine_attribute_methods
- end
+ assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar
+ ensure
+ ModelWithAttributesWithSpaces.undefine_attribute_methods
end
- test '#alias_attribute works with attributes named as a ruby keyword' do
- begin
- ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end])
- ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin)
- ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end)
+ test "#alias_attribute works with attributes named as a ruby keyword" do
+ ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end])
+ ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin)
+ ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end)
- assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from
- assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to
- ensure
- ModelWithRubyKeywordNamedAttributes.undefine_attribute_methods
- end
+ assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from
+ assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to
+ ensure
+ ModelWithRubyKeywordNamedAttributes.undefine_attribute_methods
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 +233,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 +252,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 +262,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..097db2e923
--- /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 cannot 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 e4ecc0adb4..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
- ActiveSupport::Deprecation.silence { 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 26ba6ba4fb..947f9bf99b 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,20 @@ 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
@@ -142,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")
@@ -176,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
@@ -188,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" }
@@ -206,17 +209,118 @@ class ErrorsTest < ActiveModel::TestCase
person.errors.add(:name, "cannot be blank")
person.errors.add(:name, "is invalid")
assert person.errors.added?(:name, "cannot be blank")
+ assert person.errors.added?(:name, "is invalid")
+ assert_not person.errors.added?(:name, "incorrect")
end
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 argument" 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 person.errors.added? :name, "is too long (maximum is 25 characters)"
+ 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)
+ assert person.errors.added?(:name, :wrong)
+ end
+
+ test "of_kind? returns false when checking for an error, but not providing message argument" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ assert_not person.errors.of_kind?(:name)
+ end
+
+ test "of_kind? 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_not person.errors.of_kind?(:name, "cannot be blank")
+ end
+
+ test "of_kind? returns false when no errors are present" do
+ person = Person.new
+ assert_not person.errors.of_kind?(:name)
+ end
+
+ test "of_kind? matches the given message when several errors are present for the same attribute" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "is invalid")
+ assert person.errors.of_kind?(:name, "cannot be blank")
+ assert person.errors.of_kind?(:name, "is invalid")
+ assert_not person.errors.of_kind?(:name, "incorrect")
+ end
+
+ test "of_kind? defaults message to :invalid" do
+ person = Person.new
+ person.errors.add(:name)
+ assert person.errors.of_kind?(:name)
+ end
+
+ test "of_kind? handles proc messages" do
+ person = Person.new
+ message = Proc.new { "cannot be blank" }
+ person.errors.add(:name, message)
+ assert person.errors.of_kind?(:name, message)
+ end
+
+ test "of_kind? returns true when string attribute is used with a symbol message" do
+ person = Person.new
+ person.errors.add(:name, :blank)
+ assert person.errors.of_kind?("name", :blank)
+ end
+
+ test "of_kind? handles symbol message" do
+ person = Person.new
+ person.errors.add(:name, :blank)
+ assert person.errors.of_kind?(:name, :blank)
+ end
+
+ test "of_kind? 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.of_kind?(:name, "cannot be blank")
+ assert person.errors.of_kind?("name", "cannot be blank")
+ end
+
+ test "of_kind? ignores options" do
+ person = Person.new
+ person.errors.add :name, :too_long, count: 25
+
+ assert person.errors.of_kind? :name, :too_long
+ assert person.errors.of_kind? :name, "is too long (maximum is 25 characters)"
+ end
+
+ test "of_kind? 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.of_kind?(:name, :used)
+ assert person.errors.of_kind?(:name, :wrong)
end
test "size calculates the number of error messages" do
@@ -244,6 +348,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")
@@ -251,7 +365,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")
@@ -263,6 +377,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
@@ -298,78 +413,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)
@@ -415,7 +464,7 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal 1, person.errors.details.count
person.errors.clear
- assert person.errors.details.empty?
+ assert_empty person.errors.details
end
test "copy errors" do
@@ -427,4 +476,69 @@ class ErrorsTest < ActiveModel::TestCase
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 "slice! removes all errors except the given keys" do
+ person = Person.new
+ person.errors.add(:name, "cannot be nil")
+ person.errors.add(:age, "cannot be nil")
+ person.errors.add(:gender, "cannot be nil")
+ person.errors.add(:city, "cannot be nil")
+
+ person.errors.slice!(:age, "gender")
+
+ assert_equal [:age, :gender], person.errors.keys
+ end
+
+ test "slice! returns the deleted errors" do
+ person = Person.new
+ person.errors.add(:name, "cannot be nil")
+ person.errors.add(:age, "cannot be nil")
+ person.errors.add(:gender, "cannot be nil")
+ person.errors.add(:city, "cannot be nil")
+
+ removed_errors = person.errors.slice!(:age, "gender")
+
+ assert_equal({ name: ["cannot be nil"], city: ["cannot be nil"] }, removed_errors)
+ 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 d8d757f52a..0fd0a2f8ee 100644
--- a/activemodel/test/cases/forbidden_attributes_protection_test.rb
+++ b/activemodel/test/cases/forbidden_attributes_protection_test.rb
@@ -1,6 +1,8 @@
-require 'cases/helper'
-require 'active_support/core_ext/hash/indifferent_access'
-require 'models/account'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "active_support/core_ext/hash/indifferent_access"
+require "models/account"
class ProtectedParams
attr_accessor :permitted
@@ -25,18 +27,18 @@ 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 44c26e4f10..138b1d1bb9 100644
--- a/activemodel/test/cases/helper.rb
+++ b/activemodel/test/cases/helper.rb
@@ -1,5 +1,6 @@
-require 'active_model'
-require 'active_support/core_ext/string/access'
+# frozen_string_literal: true
+
+require "active_model"
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
@@ -7,18 +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'
-
-# Skips the current run on Rubinius using Minitest::Assertions#skip
-def rubinius_skip(message = '')
- skip message if RUBY_ENGINE == 'rbx'
-end
-# Skips the current run on JRuby using Minitest::Assertions#skip
-def jruby_skip(message = '')
- skip message if defined?(JRUBY_VERSION)
-end
+require "active_support/testing/autorun"
+require "active_support/testing/method_call_assertions"
-class ActiveModel::TestCase
+class ActiveModel::TestCase < 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
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..95ee7cace3 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 customize full message defaults to false" do
+ @app.initialize!
+
+ assert_equal false, ActiveModel::Errors.i18n_customize_full_message
+ end
+
+ test "i18n customize full message can be disabled" do
+ @app.config.active_model.i18n_customize_full_message = false
+ @app.initialize!
+
+ assert_equal false, ActiveModel::Errors.i18n_customize_full_message
+ end
+
+ test "i18n customize full message can be enabled" do
+ @app.config.active_model.i18n_customize_full_message = true
+ @app.initialize!
+
+ assert_equal true, ActiveModel::Errors.i18n_customize_full_message
+ end
end
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index 6d56c8344a..bbf443290b 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
@@ -197,16 +206,14 @@ class SecurePasswordTest < ActiveModel::TestCase
end
test "Password digest cost honors bcrypt cost attribute when min_cost is false" do
- begin
- original_bcrypt_cost = BCrypt::Engine.cost
- ActiveModel::SecurePassword.min_cost = false
- BCrypt::Engine.cost = 5
-
- @user.password = "secret"
- assert_equal BCrypt::Engine.cost, @user.password_digest.cost
- ensure
- BCrypt::Engine.cost = original_bcrypt_cost
- end
+ original_bcrypt_cost = BCrypt::Engine.cost
+ ActiveModel::SecurePassword.min_cost = false
+ BCrypt::Engine.cost = 5
+
+ @user.password = "secret"
+ assert_equal BCrypt::Engine.cost, @user.password_digest.cost
+ ensure
+ BCrypt::Engine.cost = original_bcrypt_cost
end
test "Password digest cost can be set to bcrypt min cost to speed up tests" 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..84efc8de0d 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,26 +20,24 @@ 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
test "should include root in json if include_root_in_json is true" do
- begin
- original_include_root_in_json = Contact.include_root_in_json
- Contact.include_root_in_json = true
- json = @contact.to_json
-
- 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_match %r{"awesome":true}, json
- assert_match %r{"preferences":\{"shows":"anime"\}}, json
- ensure
- Contact.include_root_in_json = original_include_root_in_json
- end
+ original_include_root_in_json = Contact.include_root_in_json
+ Contact.include_root_in_json = true
+ json = @contact.to_json
+
+ assert_match %r{^\{"contact":\{}, json
+ assert_match %r{"name":"Konata Izumi"}, json
+ assert_match %r{"age":16}, json
+ 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
+ Contact.include_root_in_json = original_include_root_in_json
end
test "should include root in json (option) even if the default is set to false" do
@@ -53,12 +53,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 +68,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 +79,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 +89,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,20 +127,22 @@ 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
- Contact.include_root_in_json = true
- json = @contact.as_json
-
- assert_kind_of Hash, json
- assert_kind_of Hash, json['contact']
- %w(name age created_at awesome preferences).each do |field|
- assert_equal @contact.send(field), json['contact'][field]
- end
- ensure
- Contact.include_root_in_json = original_include_root_in_json
+ original_include_root_in_json = Contact.include_root_in_json
+ Contact.include_root_in_json = true
+ json = @contact.as_json
+
+ assert_kind_of Hash, json
+ assert_kind_of Hash, json["contact"]
+ %w(name age created_at awesome preferences).each do |field|
+ assert_equal @contact.send(field).as_json, json["contact"][field]
end
+ ensure
+ Contact.include_root_in_json = original_include_root_in_json
end
test "from_json should work without a root (class attribute)" do
diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb
index 2c89388f14..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,102 +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
+ 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..7f8490b2fe
--- /dev/null
+++ b/activemodel/test/cases/type/boolean_test.rb
@@ -0,0 +1,54 @@
+# 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")
+ 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)
+
+ # 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")
+ 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..2dd1a55616
--- /dev/null
+++ b/activemodel/test/cases/type/date_test.rb
@@ -0,0 +1,34 @@
+# 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")
+
+ now = ::Time.now.utc
+ values_hash = { 1 => now.year, 2 => now.mon, 3 => now.mday }
+ date_string = now.strftime("%F")
+ assert_equal date_string, type.cast(date_string).strftime("%F")
+ assert_equal date_string, type.cast(values_hash).strftime("%F")
+ end
+
+ def test_returns_correct_year
+ type = Type::Date.new
+
+ time = ::Time.utc(1, 1, 1)
+ date = ::Date.new(time.year, time.mon, time.mday)
+
+ values_hash_for_multiparameter_assignment = { 1 => 1, 2 => 1, 3 => 1 }
+
+ assert_equal date, type.cast(values_hash_for_multiparameter_assignment)
+ 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
index 1950566c0e..be60c4f7fa 100644
--- a/activemodel/test/cases/type/decimal_test.rb
+++ b/activemodel/test/cases/type/decimal_test.rb
@@ -1,19 +1,28 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
module Type
class DecimalTest < ActiveModel::TestCase
def test_type_cast_decimal
type = Decimal.new
- assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0"))
- assert_equal BigDecimal.new("123"), type.cast(123.0)
- assert_equal BigDecimal.new("1"), type.cast(:"1")
+ assert_equal BigDecimal("0"), type.cast(BigDecimal("0"))
+ assert_equal BigDecimal("123"), type.cast(123.0)
+ assert_equal BigDecimal("1"), type.cast(:"1")
+ end
+
+ def test_type_cast_decimal_from_invalid_string
+ type = Decimal.new
+ assert_nil type.cast("")
+ assert_equal BigDecimal("1"), type.cast("1ignore")
+ assert_equal BigDecimal("0"), type.cast("bad1")
+ assert_equal BigDecimal("0"), type.cast("bad")
end
def test_type_cast_decimal_from_float_with_large_precision
type = Decimal.new(precision: ::Float::DIG + 2)
- assert_equal BigDecimal.new("123.0"), type.cast(123.0)
+ assert_equal BigDecimal("123.0"), type.cast(123.0)
end
def test_type_cast_from_float_with_unspecified_precision
@@ -39,7 +48,7 @@ module ActiveModel
def test_type_cast_decimal_from_object_responding_to_d
value = Object.new
def value.to_d
- BigDecimal.new("1")
+ BigDecimal("1")
end
type = Decimal.new
assert_equal BigDecimal("1"), type.cast(value)
@@ -48,9 +57,12 @@ module ActiveModel
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')
+ 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
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
index dac922db42..6c02c01237 100644
--- a/activemodel/test/cases/type/integer_test.rb
+++ b/activemodel/test/cases/type/integer_test.rb
@@ -1,16 +1,19 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
+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")
+ 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)
@@ -19,8 +22,8 @@ module ActiveModel
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])
+ assert_nil type.cast(1 => 2)
assert_nil type.cast(1..2)
end
@@ -32,7 +35,7 @@ module ActiveModel
test "casting nan and infinity" do
type = Type::Integer.new
assert_nil type.cast(::Float::NAN)
- assert_nil type.cast(1.0/0.0)
+ assert_nil type.cast(1.0 / 0.0)
end
test "casting booleans for database" do
@@ -41,37 +44,62 @@ module ActiveModel
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 "casting string for database" do
+ type = Type::Integer.new
+ assert_nil type.serialize("wibble")
+ assert_equal 5, type.serialize("5wibble")
+ assert_equal 5, type.serialize(" +5")
+ assert_equal(-5, type.serialize(" -5"))
+ end
+
+ test "casting empty string" do
+ type = Type::Integer.new
+ assert_nil type.cast("")
+ assert_nil type.serialize("")
+ assert_nil type.deserialize("")
+ 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 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(::RangeError) 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(::RangeError) do
+ assert_raises(ActiveModel::RangeError) do
Integer.new.serialize(2147483648)
end
end
test "very small numbers are out of range" do
- assert_raises(::RangeError) do
+ assert_raises(ActiveModel::RangeError) do
Integer.new.serialize(-9999999999999999999999999999999)
end
end
test "very large numbers are out of range" do
- assert_raises(::RangeError) do
+ assert_raises(ActiveModel::RangeError) do
Integer.new.serialize(9999999999999999999999999999999)
end
end
@@ -96,10 +124,10 @@ module ActiveModel
assert_equal(9223372036854775807, type.serialize(9223372036854775807))
assert_equal(-9223372036854775808, type.serialize(-9223372036854775808))
- assert_raises(::RangeError) do
+ assert_raises(ActiveModel::RangeError) do
type.serialize(-9999999999999999999999999999999)
end
- assert_raises(::RangeError) do
+ assert_raises(ActiveModel::RangeError) do
type.serialize(9999999999999999999999999999999)
end
end
diff --git a/activemodel/test/cases/type/registry_test.rb b/activemodel/test/cases/type/registry_test.rb
index 2a48998a62..0633ea2538 100644
--- a/activemodel/test/cases/type/registry_test.rb
+++ b/activemodel/test/cases/type/registry_test.rb
@@ -1,39 +1,42 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
- 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
+ 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)
- test "a block can be registered" do
- registry = Type::Registry.new
- registry.register(:foo) do |*args|
- [*args, "block for foo"]
+ assert_equal "", registry.lookup(:foo)
+ assert_equal [], registry.lookup(:bar)
end
- registry.register(:bar) do |*args|
- [*args, "block for bar"]
+
+ 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
- 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
- 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
- e = assert_raises(ArgumentError) do
- registry.lookup(:foo)
+ assert_equal "Unknown type :foo", e.message
end
-
- assert_equal "Unknown type :foo", e.message
end
end
end
diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb
index 7b25a1ef74..9cc530e8db 100644
--- a/activemodel/test/cases/type/string_test.rb
+++ b/activemodel/test/cases/type/string_test.rb
@@ -1,27 +1,46 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require "active_model/type"
module ActiveModel
- class StringTypeTest < 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
+ 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 "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
+ test "type casting for database" do
+ type = Type::String.new
+ object, array, hash = Object.new, [true], { a: :b }
+ assert_equal object, type.serialize(object)
+ assert_equal array, type.serialize(array)
+ assert_equal hash, type.serialize(hash)
+ 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
- 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)
+ 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..5c6271241d
--- /dev/null
+++ b/activemodel/test/cases/type/time_test.rb
@@ -0,0 +1,38 @@
+# 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")
+ assert_equal ::Time.utc(2000, 1, 1, 16, 45, 54), type.cast(4 => 16, 5 => 45, 6 => 54)
+ 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/unsigned_integer_test.rb b/activemodel/test/cases/type/unsigned_integer_test.rb
deleted file mode 100644
index 16301b3ac0..0000000000
--- a/activemodel/test/cases/type/unsigned_integer_test.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require "cases/helper"
-require "active_model/type"
-
-module ActiveModel
- module Type
- class UnsignedIntegerTest < ActiveModel::TestCase
- test "unsigned int max value is in range" do
- assert_equal(4294967295, UnsignedInteger.new.serialize(4294967295))
- end
-
- test "minus value is out of range" do
- assert_raises(::RangeError) do
- UnsignedInteger.new.serialize(-1)
- 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/types_test.rb b/activemodel/test/cases/types_test.rb
deleted file mode 100644
index 558c56f157..0000000000
--- a/activemodel/test/cases/types_test.rb
+++ /dev/null
@@ -1,125 +0,0 @@
-require "cases/helper"
-require "active_model/type"
-require "active_support/core_ext/numeric/time"
-
-module ActiveModel
- class TypesTest < ActiveModel::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")
-
- 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_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", "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_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
-
- 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
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..9674068aff 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_false_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_combining_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_combining_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 c56bf1c0ad..7bf15e4bee 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,36 +55,33 @@ 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
def test_title_confirmation_with_i18n_attribute
- begin
- @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: { confirmation: "doesn't match %{attribute}" } },
- activemodel: { attributes: { topic: { title: 'Test Title'} } }
- })
-
- Topic.validates_confirmation_of(:title)
-
- t = Topic.new("title" => "We should be confirmed","title_confirmation" => "")
- assert t.invalid?
- assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation]
- ensure
- I18n.load_path.replace @old_load_path
- I18n.backend = @old_backend
- I18n.backend.reload!
- end
+ @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: { confirmation: "doesn't match %{attribute}" } },
+ activemodel: { attributes: { topic: { title: "Test Title" } } })
+
+ Topic.validates_confirmation_of(:title)
+
+ 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
+ I18n.backend = @old_backend
+ I18n.backend.reload!
end
test "does not override confirmation reader if present" do
@@ -109,13 +120,13 @@ class ConfirmationValidationTest < ActiveModel::TestCase
Topic.validates_confirmation_of(:title, case_sensitive: true)
t = Topic.new(title: "title", title_confirmation: "Title")
- assert t.invalid?
+ assert_predicate t, :invalid?
end
def test_title_confirmation_with_case_sensitive_option_false
Topic.validates_confirmation_of(:title, case_sensitive: false)
t = Topic.new(title: "title", title_confirmation: "Title")
- assert t.valid?
+ assert_predicate t, :valid?
end
end
diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb
index 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 ea4c2ee7df..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
@@ -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 09d7226b5a..eb03e837f1 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_customize_full_message = ActiveModel::Errors.i18n_customize_full_message
+ ActiveModel::Errors.i18n_customize_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_customize_full_message = @original_i18n_customize_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_customize_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_customize_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_customize_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_customize_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_customize_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_customize_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_customize_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_customize_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_customize_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_customize_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,7 +216,7 @@ 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, call) do
@person.valid?
@@ -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 9bd44175a6..daad76759f 100644
--- a/activemodel/test/cases/validations/inclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/inclusion_validation_test.rb
@@ -1,72 +1,73 @@
-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
range_begin = 1.year.ago
range_end = Time.now
Topic.validates_inclusion_of(:created_at, in: range_begin..range_end)
- assert Topic.new(title: 'aaa', created_at: 2.years.ago).invalid?
- assert Topic.new(title: 'aaa', created_at: 3.months.ago).valid?
- assert Topic.new(title: 'aaa', created_at: 37.weeks.from_now).invalid?
- assert Topic.new(title: 'aaa', created_at: range_begin).valid?
- assert Topic.new(title: 'aaa', created_at: range_end).valid?
+ assert_predicate Topic.new(title: "aaa", created_at: 2.years.ago), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: 3.months.ago), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.from_now), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid?
end
def test_validates_inclusion_of_date_range
range_begin = 1.year.until(Date.today)
range_end = Date.today
Topic.validates_inclusion_of(:created_at, in: range_begin..range_end)
- assert Topic.new(title: 'aaa', created_at: 2.years.until(Date.today)).invalid?
- assert Topic.new(title: 'aaa', created_at: 3.months.until(Date.today)).valid?
- assert Topic.new(title: 'aaa', created_at: 37.weeks.since(Date.today)).invalid?
- assert Topic.new(title: 'aaa', created_at: 1.year.until(Date.today)).valid?
- assert Topic.new(title: 'aaa', created_at: Date.today).valid?
- assert Topic.new(title: 'aaa', created_at: range_begin).valid?
- assert Topic.new(title: 'aaa', created_at: range_end).valid?
+ assert_predicate Topic.new(title: "aaa", created_at: 2.years.until(Date.today)), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: 3.months.until(Date.today)), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.since(Date.today)), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: 1.year.until(Date.today)), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: Date.today), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid?
end
def test_validates_inclusion_of_date_time_range
range_begin = 1.year.until(DateTime.current)
range_end = DateTime.current
Topic.validates_inclusion_of(:created_at, in: range_begin..range_end)
- assert Topic.new(title: 'aaa', created_at: 2.years.until(DateTime.current)).invalid?
- assert Topic.new(title: 'aaa', created_at: 3.months.until(DateTime.current)).valid?
- assert Topic.new(title: 'aaa', created_at: 37.weeks.since(DateTime.current)).invalid?
- assert Topic.new(title: 'aaa', created_at: range_begin).valid?
- assert Topic.new(title: 'aaa', created_at: range_end).valid?
+ assert_predicate Topic.new(title: "aaa", created_at: 2.years.until(DateTime.current)), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: 3.months.until(DateTime.current)), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: 37.weeks.since(DateTime.current)), :invalid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_begin), :valid?
+ assert_predicate Topic.new(title: "aaa", created_at: range_end), :valid?
end
def test_validates_inclusion_of
Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ))
- assert Topic.new("title" => "a!", "content" => "abc").invalid?
- assert Topic.new("title" => "a b", "content" => "abc").invalid?
- assert Topic.new("title" => nil, "content" => "def").invalid?
+ assert_predicate Topic.new("title" => "a!", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => "a b", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => nil, "content" => "def"), :invalid?
t = Topic.new("title" => "a", "content" => "I know you are but what am I?")
- assert t.valid?
+ assert_predicate t, :valid?
t.title = "uhoh"
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["is not included in the list"], t.errors[:title]
assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: nil) }
@@ -80,30 +81,30 @@ class InclusionValidationTest < ActiveModel::TestCase
def test_validates_inclusion_of_with_allow_nil
Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), allow_nil: true)
- assert Topic.new("title" => "a!", "content" => "abc").invalid?
- assert Topic.new("title" => "", "content" => "abc").invalid?
- assert Topic.new("title" => nil, "content" => "abc").valid?
+ assert_predicate Topic.new("title" => "a!", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => "", "content" => "abc"), :invalid?
+ assert_predicate Topic.new("title" => nil, "content" => "abc"), :valid?
end
def test_validates_inclusion_of_with_formatted_message
Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), message: "option %{value} is not in the list")
- assert Topic.new("title" => "a", "content" => "abc").valid?
+ assert_predicate Topic.new("title" => "a", "content" => "abc"), :valid?
t = Topic.new("title" => "uhoh", "content" => "abc")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
assert_equal ["option uhoh is not in the list"], t.errors[:title]
end
def test_validates_inclusion_of_with_within_option
Topic.validates_inclusion_of(:title, within: %w( a b c d e f g ))
- assert Topic.new("title" => "a", "content" => "abc").valid?
+ assert_predicate Topic.new("title" => "a", "content" => "abc"), :valid?
t = Topic.new("title" => "uhoh", "content" => "abc")
- assert t.invalid?
- assert t.errors[:title].any?
+ assert_predicate t, :invalid?
+ assert_predicate t.errors[:title], :any?
end
def test_validates_inclusion_of_for_ruby_class
@@ -111,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
@@ -143,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
@@ -153,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..37e10f783c 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.define_method(:max_title_length) { 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 74a048537d..16c44762cb 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -1,13 +1,14 @@
-require 'cases/helper'
+# frozen_string_literal: true
-require 'models/topic'
-require 'models/person'
+require "cases/helper"
-require 'bigdecimal'
-require 'active_support/core_ext/big_decimal'
+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
@@ -19,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
@@ -36,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
@@ -51,14 +59,14 @@ 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)
end
def test_validates_numericality_of_with_integer_only_and_proc_as_value
- Topic.send(:define_method, :allow_only_integers?, lambda { false })
+ Topic.define_method(:allow_only_integers?) { false }
Topic.validates_numericality_of :approved, only_integer: Proc.new(&:allow_only_integers?)
invalid!(NIL + BLANK + JUNK)
@@ -68,119 +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.new('97.18')
+ Topic.validates_numericality_of :approved, greater_than: BigDecimal("97.18")
- invalid!([-97.18, BigDecimal.new('97.18'), BigDecimal('-97.18')], 'must be greater than 97.18')
- valid!([97.18, 98, BigDecimal.new('98')]) # Notice the 97.18 as a float is greater than 97.18 as a BigDecimal due to floating point precision
+ 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'])
+ 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.new('97.18')
+ Topic.validates_numericality_of :approved, greater_than_or_equal_to: BigDecimal("97.18")
- invalid!([-97.18, 97.17, 97, BigDecimal.new('97.17'), BigDecimal.new('-97.18')], 'must be greater than or equal to 97.18')
- valid!([97.18, 98, BigDecimal.new('97.19')])
+ invalid!([-97.18, 97.17, 97, BigDecimal("97.17"), BigDecimal("-97.18")], "must be greater than or equal to 97.18")
+ valid!([97.18, 98, BigDecimal("97.19")])
end
def test_validates_numericality_with_greater_than_or_equal_using_string_value
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'])
+ 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.new('97.18')
+ Topic.validates_numericality_of :approved, equal_to: BigDecimal("97.18")
- invalid!([-97.18, 97.18], 'must be equal to 97.18')
- valid!([BigDecimal.new('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'])
+ 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.new('97.18')
+ Topic.validates_numericality_of :approved, less_than: BigDecimal("97.18")
- invalid!([97.18, BigDecimal.new('97.18')], 'must be less than 97.18')
- valid!([-97.0, 97.0, -97, 97, BigDecimal.new('-97'), BigDecimal.new('97')])
+ invalid!([97.18, BigDecimal("97.18")], "must be less than 97.18")
+ valid!([-97.0, 97.0, -97, 97, BigDecimal("-97"), BigDecimal("97")])
end
def test_validates_numericality_with_less_than_using_string_value
Topic.validates_numericality_of :approved, less_than: 10
- invalid!(['10', '10.1', '11'], 'must be less than 10')
- valid!(['-10', '9', '9.9'])
+ 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.new('97.18')
+ Topic.validates_numericality_of :approved, less_than_or_equal_to: BigDecimal("97.18")
- invalid!([97.18, 98], 'must be less than or equal to 97.18')
- valid!([-97.18, BigDecimal.new('-97.18'), BigDecimal.new('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'])
+ 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
@@ -201,41 +209,41 @@ class NumericalityValidationTest < ActiveModel::TestCase
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'])
+ 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.define_method(:min_approved) { 5 }
Topic.validates_numericality_of :approved, greater_than_or_equal_to: Proc.new(&:min_approved)
invalid!([3, 4])
valid!([5, 6])
ensure
- Topic.send(:remove_method, :min_approved)
+ Topic.remove_method :min_approved
end
def test_validates_numericality_with_symbol
- Topic.send(:define_method, :max_approved, lambda { 5 })
+ Topic.define_method(:max_approved) { 5 }
Topic.validates_numericality_of :approved, less_than_or_equal_to: :max_approved
invalid!([6])
valid!([4, 5])
ensure
- Topic.send(:remove_method, :max_approved)
+ Topic.remove_method :max_approved
end
def test_validates_numericality_with_numeric_message
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
@@ -244,45 +252,84 @@ 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_object_acting_as_numeric
+ klass = Class.new do
+ def to_f
+ 123.54
+ end
+ end
+
+ Topic.validates_numericality_of :price
+ topic = Topic.new(price: klass.new)
+
+ assert_predicate topic, :valid?
+ 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
+
+ def test_validates_numericality_equality_for_float_and_big_decimal
+ Topic.validates_numericality_of :approved, equal_to: BigDecimal("65.6")
+
+ invalid!([Float("65.5"), BigDecimal("65.7")], "must be equal to 65.6")
+ valid!([Float("65.6"), BigDecimal("65.6")])
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 with validation error: #{topic.errors[:approved].first}"
+ 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 b901a1523e..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
@@ -38,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
@@ -48,10 +50,10 @@ 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
@@ -62,7 +64,7 @@ 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, :context2]), "Validation did not run on context1 when 'on' is set to context1 and context2"
- assert topic.errors[:base].include?(ERROR_MESSAGE)
- assert topic.errors[:base].include?(ANOTHER_ERROR_MESSAGE)
+ 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 c73580138d..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,61 +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
@@ -138,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/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 113ab0bc1f..c40a6d6f0e 100644
--- a/activemodel/test/models/contact.rb
+++ b/activemodel/test/models/contact.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Contact
extend ActiveModel::Naming
include ActiveModel::Conversion
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 d59101d621..d73d9ade41 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,1995 +1,795 @@
-* Delegate `empty?`, `none?` and `one?`. Now they can be invoked as model class methods.
+* Assign all attributes before calling `build` to ensure the child record is visible in
+ `before_add` and `after_add` callbacks for `has_many :through` associations.
- Example:
+ Fixes #33249.
- # When no record is found on the table
- Topic.empty? # => true
- Topic.none? # => true
+ *Ryan H. Kerr*
- # When only one record is found on the table
- Topic.one? # => true
+* Add `ActiveRecord::Relation#extract_associated` for extracting associated records from a relation.
- *Kenta Shirai*
+ ```
+ account.memberships.extract_associated(:user)
+ # => Returns collection of User records
+ ```
-* The form builder now properly displays values when passing a proc form
- default to the attributes API.
+ *DHH*
- Fixes #24249.
+* Add `ActiveRecord::Relation#annotate` for adding SQL comments to its queries.
- *Sean Griffin*
+ For example:
-* The schema cache is now cleared after the `db:migrate` task is run.
+ ```
+ Post.where(id: 123).annotate("this is a comment").to_sql
+ # SELECT "posts".* FROM "posts" WHERE "posts"."id" = 123 /* this is a comment */
+ ```
- Closes #24273.
+ This can be useful in instrumentation or other analysis of issued queries.
- *Chris Arcand*
+ *Matt Yoho*
-* MySQL: strict mode respects other SQL modes rather than overwriting them.
- Setting `strict: true` adds `STRICT_ALL_TABLES` to `sql_mode`. Setting
- `strict: false` removes `STRICT_TRANS_TABLES`, `STRICT_ALL_TABLES`, and
- `TRADITIONAL` from `sql_mode`.
+* Support Optimizer Hints.
- *Ryuta Kamizono*
+ In most databases, a way to control the optimizer is by using optimizer hints,
+ which can be specified within individual statements.
-* Execute default_scope defined by abstract class in the context of subclass.
+ Example (for MySQL):
- Fixes #23413.
- Fixes #10658.
-
- *Mehmet Emin İNAÇ*
+ Topic.optimizer_hints("MAX_EXECUTION_TIME(50000)", "NO_INDEX_MERGE(topics)")
+ # SELECT /*+ MAX_EXECUTION_TIME(50000) NO_INDEX_MERGE(topics) */ `topics`.* FROM `topics`
-* Fix an issue when preloading associations with extensions.
- Previously every association with extension methods was transformed into an
- instance dependent scope. This is no longer the case.
+ Example (for PostgreSQL with pg_hint_plan):
- Fixes #23934.
+ Topic.optimizer_hints("SeqScan(topics)", "Parallel(topics 8)")
+ # SELECT /*+ SeqScan(topics) Parallel(topics 8) */ "topics".* FROM "topics"
- *Yves Senn*
+ See also:
-* Deprecate `{insert|update|delete}_sql` in `DatabaseStatements`.
- Use the `{insert|update|delete}` public methods instead.
+ * https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html
+ * https://pghintplan.osdn.jp/pg_hint_plan.html
+ * https://docs.oracle.com/en/database/oracle/oracle-database/12.2/tgsql/influencing-the-optimizer.html
+ * https://docs.microsoft.com/en-us/sql/t-sql/queries/hints-transact-sql-query?view=sql-server-2017
+ * https://www.ibm.com/support/knowledgecenter/en/SSEPGG_11.1.0/com.ibm.db2.luw.admin.perf.doc/doc/c0070117.html
*Ryuta Kamizono*
-* Added a configuration option to have active record raise an ArgumentError
- if the order or limit is ignored in a batch query, rather than logging a
- warning message.
-
- *Scott Ringwelski*
-
-* Honour the order of the joining model in a `has_many :through` association when eager loading.
-
- Example:
-
- The below will now follow the order of `by_lines` when eager loading `authors`.
+* Fix query attribute method on user-defined attribute to be aware of typecasted value.
- class Article < ActiveRecord::Base
- has_many :by_lines, -> { order(:position) }
- has_many :authors, through: :by_lines
- end
-
- Fixes #17864.
-
- *Yasyf Mohamedali*, *Joel Turkel*
-
-* Ensure that the Suppressor runs before validations.
-
- This moves the suppressor up to be run before validations rather than after
- validations. There's no reason to validate a record you aren't planning on saving.
+ For example, the following code no longer return false as casted non-empty string:
- *Eileen M. Uchitelle*
-
-## Rails 5.0.0.beta3 (February 24, 2016) ##
-
-* Save many-to-many objects based on association primary key.
+ ```
+ class Post < ActiveRecord::Base
+ attribute :user_defined_text, :text
+ end
- Fixes #20995.
+ Post.new(user_defined_text: "false").user_defined_text? # => true
+ ```
- *himesh-r*
+ *Yuji Kamijima*
-* Ensure that mutations of the array returned from `ActiveRecord::Relation#to_a`
- do not affect the original relation, by returning a duplicate array each time.
+* Quote empty ranges like other empty enumerables.
- This brings the behavior in line with `CollectionProxy#to_a`, which was
- already more careful.
+ *Patrick Rebsch*
- *Matthew Draper*
+* Add `insert_all`/`insert_all!`/`upsert_all` methods to `ActiveRecord::Persistence`,
+ allowing bulk inserts akin to the bulk updates provided by `update_all` and
+ bulk deletes by `delete_all`.
-* Fixed `where` for polymorphic associations when passed an array containing different types.
+ Supports skipping or upserting duplicates through the `ON CONFLICT` syntax
+ for Postgres (9.5+) and Sqlite (3.24+) and `ON DUPLICATE KEY UPDATE` syntax
+ for MySQL.
- Fixes #17011.
-
- Example:
+ *Bob Lail*
- PriceEstimate.where(estimate_of: [Treasure.find(1), Car.find(2)])
- # => SELECT "price_estimates".* FROM "price_estimates"
- WHERE (("price_estimates"."estimate_of_type" = 'Treasure' AND "price_estimates"."estimate_of_id" = 1)
- OR ("price_estimates"."estimate_of_type" = 'Car' AND "price_estimates"."estimate_of_id" = 2))
+* Add `rails db:seed:replant` that truncates tables of each database
+ for current environment and loads the seeds.
- *Philippe Huibonhoa*
+ *bogdanvlviv*, *DHH*
-* Fix a bug where using `t.foreign_key` twice with the same `to_table` within
- the same table definition would only create one foreign key.
+* Add `ActiveRecord::Base.connection.truncate` for SQLite3 adapter.
- *George Millo*
+ *bogdanvlviv*
-* Fix a regression on has many association, where calling a child from parent in child's callback
- results in same child records getting added repeatedly to target.
+* Deprecate mismatched collation comparison for uniqueness validator.
- Fixes #13387.
+ Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1.
+ To continue case sensitive comparison on the case insensitive column,
+ pass `case_sensitive: true` option explicitly to the uniqueness validator.
- *Bogdan Gusiev*, *Jon Hinson*
-
-* Rework `ActiveRecord::Relation#last`.
-
- 1. Never perform additional SQL on loaded relation
- 2. Use SQL reverse order instead of loading relation if relation doesn't have limit
- 3. Deprecated relation loading when SQL order can not be automatically reversed
-
- Topic.order("title").load.last(3)
- # before: SELECT ...
- # after: No SQL
-
- Topic.order("title").last
- # before: SELECT * FROM `topics`
- # after: SELECT * FROM `topics` ORDER BY `topics`.`title` DESC LIMIT 1
-
- Topic.order("coalesce(author, title)").last
- # before: SELECT * FROM `topics`
- # after: Deprecation Warning for irreversible order
-
- *Bogdan Gusiev*
-
-* Allow `joins` to be unscoped.
-
- Fixes #13775.
+ *Ryuta Kamizono*
- *Takashi Kokubun*
+* Add `reselect` method. This is a short-hand for `unscope(:select).select(fields)`.
-* Add ActiveRecord `#second_to_last` and `#third_to_last` methods.
+ Fixes #27340.
- *Brian Christian*
+ *Willian Gustavo Veiga*
-* Added `numeric` helper into migrations.
+* Add negative scopes for all enum values.
Example:
- create_table(:numeric_types) do |t|
- t.numeric :numeric_type, precision: 10, scale: 2
+ class Post < ActiveRecord::Base
+ enum status: %i[ drafted active trashed ]
end
- *Mehmet Emin İNAÇ*
-
-* Bumped the minimum supported version of PostgreSQL to >= 9.1.
- Both PG 9.0 and 8.4 are past their end of life date:
- http://www.postgresql.org/support/versioning/
-
- *Remo Mueller*
+ Post.not_drafted # => where.not(status: :drafted)
+ Post.not_active # => where.not(status: :active)
+ Post.not_trashed # => where.not(status: :trashed)
-## Rails 5.0.0.beta2 (February 01, 2016) ##
+ *DHH*
-* `ActiveRecord::Relation#reverse_order` throws `ActiveRecord::IrreversibleOrderError`
- when the order can not be reversed using current trivial algorithm.
- Also raises the same error when `#reverse_order` is called on
- relation without any order and table has no primary key:
+* Fix different `count` calculation when using `size` with manual `select` with DISTINCT.
- Topic.order("concat(author_name, title)").reverse_order
- # Before: SELECT `topics`.* FROM `topics` ORDER BY concat(author_name DESC, title) DESC
- # After: raises ActiveRecord::IrreversibleOrderError
- Edge.all.reverse_order
- # Before: SELECT `edges`.* FROM `edges` ORDER BY `edges`.`` DESC
- # After: raises ActiveRecord::IrreversibleOrderError
-
- *Bogdan Gusiev*
+ Fixes #35214.
-* Improve schema_migrations insertion performance by inserting all versions
- in one INSERT SQL.
+ *Juani Villarejo*
- *Akira Matsuda*, *Naoto Koshikawa*
-* Using `references` or `belongs_to` in migrations will always add index
- for the referenced column by default, without adding `index: true` option
- to generated migration file. Users can opt out of this by passing
- `index: false`.
+## Rails 6.0.0.beta3 (March 11, 2019) ##
- Fixes #18146.
+* No changes.
- *Matthew Draper*, *Prathamesh Sonpatki*
-* Run `type` attributes through attributes API type-casting before
- instantiating the corresponding subclass. This makes it possible to define
- custom STI mappings.
+## Rails 6.0.0.beta2 (February 25, 2019) ##
- Fixes #21986.
-
- *Yves Senn*
-
-* Don't try to quote functions or expressions passed to `:default` option if
- they are passed as procs.
-
- This will generate proper query with the passed function or expression for
- the default option, instead of trying to quote it in incorrect fashion.
-
- Example:
-
- create_table :posts do |t|
- t.datetime :published_at, default: -> { 'NOW()' }
- end
+* Fix prepared statements caching to be enabled even when query caching is enabled.
*Ryuta Kamizono*
-* Fix regression when loading fixture files with symbol keys.
-
- Fixes #22584.
-
- *Yves Senn*
-
-* Use `version` column as primary key for schema_migrations table because
- `schema_migrations` versions are guaranteed to be unique.
-
- This makes it possible to use `update_attributes` on models that do
- not have a primary key.
-
- *Richard Schneeman*
-
-* Add short-hand methods for text and blob types in MySQL.
-
- In Pg and Sqlite3, `:text` and `:binary` have variable unlimited length.
- But in MySQL, these have limited length for each types (ref #21591, #21619).
- This change adds short-hand methods for each text and blob types.
-
- Example:
-
- create_table :foos do |t|
- t.tinyblob :tiny_blob
- t.mediumblob :medium_blob
- t.longblob :long_blob
- t.tinytext :tiny_text
- t.mediumtext :medium_text
- t.longtext :long_text
- end
+* Ensure `update_all` series cares about optimistic locking.
*Ryuta Kamizono*
-* Take into account UTC offset when assigning string representation of
- timestamp with offset specified to attribute of time type.
-
- *Andrey Novikov*
-
-* When calling `first` with a `limit` argument, return directly from the
- `loaded?` records if available.
-
- *Ben Woosley*
-
-* Deprecate sending the `offset` argument to `find_nth`. Please use the
- `offset` method on relation instead.
-
- *Ben Woosley*
-
-## Rails 5.0.0.beta1 (December 18, 2015) ##
-
-* Limit record touching to once per transaction.
-
- If you have a parent/grand-parent relation like:
-
- Comment belongs_to :message, touch: true
- Message belongs_to :project, touch: true
- Project belongs_to :account, touch: true
-
- When the lowest entry(`Comment`) is saved, now, it won't repeat the touch
- call multiple times for the parent records.
-
- Related #18606.
-
- *arthurnn*
-
-* Order the result of `find(ids)` to match the passed array, if the relation
- has no explicit order defined.
-
- Fixes #20338.
-
- *Miguel Grazziotin*, *Matthew Draper*
-
-* Omit default limit values in dumped schema. It's tidier, and if the defaults
- change in the future, we can address that via Migration API Versioning.
-
- *Jean Boussier*
-
-* Support passing the schema name as a prefix to table name in
- `ConnectionAdapters::SchemaStatements#indexes`. Previously the prefix would
- be considered a full part of the index name, and only the schema in the
- current search path would be considered.
-
- *Grey Baker*
-
-* Ignore index name in `index_exists?` and `remove_index` when not passed a
- name to check for.
-
- *Grey Baker*
-
-* Extract support for the legacy `mysql` database adapter from core. It will
- live on in a separate gem for now, but most users should just use `mysql2`.
-
- *Abdelkader Boudih*
-
-* 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.
+* Don't allow `where` with non numeric string matches to 0 values.
- Newly generated applications have `app/models/application_record.rb`
- present by default.
-
- *Genadi Samokovarov*
-
-* 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.
-
- New migrations specify the Rails version they were written for:
-
- class AddStatusToOrders < ActiveRecord::Migration[5.0]
- def change
- # ...
- end
- end
-
- *Matthew Draper*, *Ravil Bayramgalin*
-
-* Use bind params for `limit` and `offset`. This will generate significantly
- fewer prepared statements for common tasks like pagination. To support this
- change, passing a string containing a comma to `limit` has been deprecated,
- and passing an Arel node to `limit` is no longer supported.
-
- Fixes #22250.
-
- *Sean Griffin*
-
-* Introduce after_{create,update,delete}_commit callbacks.
-
- Before:
-
- after_commit :add_to_index_later, on: :create
- after_commit :update_in_index_later, on: :update
- after_commit :remove_from_index_later, on: :destroy
+ *Ryuta Kamizono*
- After:
+* Introduce `ActiveRecord::Relation#destroy_by` and `ActiveRecord::Relation#delete_by`.
- after_create_commit :add_to_index_later
- after_update_commit :update_in_index_later
- after_destroy_commit :remove_from_index_later
+ `destroy_by` allows relation to find all the records matching the condition and perform
+ `destroy_all` on the matched records.
- Fixes #22515.
+ Example:
- *Genadi Samokovarov*
+ Person.destroy_by(name: 'David')
+ Person.destroy_by(name: 'David', rating: 4)
-* Respect the column default values for `inheritance_column` when
- instantiating records through the base class.
+ david = Person.find_by(name: 'David')
+ david.posts.destroy_by(id: [1, 2, 3])
- Fixes #17121.
+ `delete_by` allows relation to find all the records matching the condition and perform
+ `delete_all` on the matched records.
Example:
- # The schema of BaseModel has `t.string :type, default: 'SubType'`
- subtype = BaseModel.new
- assert_equals SubType, subtype.class
+ Person.delete_by(name: 'David')
+ Person.delete_by(name: 'David', rating: 4)
- *Kuldeep Aggarwal*
+ david = Person.find_by(name: 'David')
+ david.posts.delete_by(id: [1, 2, 3])
-* Fix `rake db:structure:dump` on Postgres when multiple schemas are used.
+ *Abhay Nikam*
- Fixes #22346.
+* Don't allow `where` with invalid value matches to nil values.
- *Nick Muerdter*, *ckoenig*
-
-* Add schema dumping support for PostgreSQL geometric data types.
+ Fixes #33624.
*Ryuta Kamizono*
-* Except keys of `build_record`'s argument from `create_scope` in `initialize_attributes`.
-
- Fixes #21893.
-
- *Yuichiro Kaneko*
-
-* Deprecate `connection.tables` on the SQLite3 and MySQL adapters.
- Also deprecate passing arguments to `#tables`.
- And deprecate `table_exists?`.
-
- The `#tables` method of some adapters (mysql, 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.
-
- 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.
-
- *Yuichiro Kaneko*
-
-* Improve support for non Active Record objects on `validates_associated`
-
- Skipping `marked_for_destruction?` when the associated object does not responds
- to it make easier to validate virtual associations built on top of Active Model
- objects and/or serialized objects that implement a `valid?` instance method.
-
- *Kassio Borges*, *Lucas Mazza*
-
-* Change connection management middleware to return a new response with
- a body proxy, rather than mutating the original.
-
- *Kevin Buchanan*
-
-* Make `db:migrate:status` to render `1_some.rb` format migrate files.
-
- These files are in `db/migrate`:
-
- * 1_valid_people_have_last_names.rb
- * 20150819202140_irreversible_migration.rb
- * 20150823202140_add_admin_flag_to_users.rb
- * 20150823202141_migration_tests.rb
- * 2_we_need_reminders.rb
- * 3_innocent_jointable.rb
-
- Before:
-
- $ bundle exec rake db:migrate:status
- ...
-
- Status Migration ID Migration Name
- --------------------------------------------------
- up 001 ********** NO FILE **********
- up 002 ********** NO FILE **********
- up 003 ********** NO FILE **********
- up 20150819202140 Irreversible migration
- up 20150823202140 Add admin flag to users
- up 20150823202141 Migration tests
-
- After:
-
- $ bundle exec rake db:migrate:status
- ...
-
- Status Migration ID Migration Name
- --------------------------------------------------
- up 001 Valid people have last names
- up 002 We need reminders
- up 003 Innocent jointable
- up 20150819202140 Irreversible migration
- up 20150823202140 Add admin flag to users
- up 20150823202141 Migration tests
-
- *Yuichiro Kaneko*
-
-* Define `ActiveRecord::Sanitization.sanitize_sql_for_order` and use it inside
- `preprocess_order_args`.
-
- *Yuichiro Kaneko*
-
-* Allow bigint with default nil for avoiding auto increment primary key.
+* SQLite3: Implement `add_foreign_key` and `remove_foreign_key`.
*Ryuta Kamizono*
-* Remove `DEFAULT_CHARSET` and `DEFAULT_COLLATION` in `MySQLDatabaseTasks`.
-
- We should omit the collation entirely rather than providing a default.
- Then the choice is the responsibility of the server and MySQL distribution.
+* Deprecate using class level querying methods if the receiver scope
+ regarded as leaked. Use `klass.unscoped` to avoid the leaking scope.
*Ryuta Kamizono*
-* Alias `ActiveRecord::Relation#left_joins` to
- `ActiveRecord::Relation#left_outer_joins`.
-
- *Takashi Kokubun*
-
-* Use advisory locking to raise a `ConcurrentMigrationError` instead of
- attempting to migrate when another migration is currently running.
-
- *Sam Davies*
-
-* Added `ActiveRecord::Relation#left_outer_joins`.
-
- Example:
-
- User.left_outer_joins(:posts)
- # => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON
- "posts"."user_id" = "users"."id"
-
- *Florian Thomas*
-
-* Support passing an array to `order` for SQL parameter sanitization.
-
- *Aaron Suggs*
-
-* Avoid disabling errors on the PostgreSQL connection when enabling the
- `standard_conforming_strings` setting. Errors were previously disabled because
- the setting wasn't writable in Postgres 8.1 and didn't exist in earlier
- versions. Now Rails only supports Postgres 8.2+ we're fine to assume the
- setting exists. Disabling errors caused problems when using a connection
- pooling tool like PgBouncer because it's not guaranteed to have the same
- connection between calls to `execute` and it could leave the connection
- with errors disabled.
-
- Fixes #22101.
-
- *Harry Marr*
-
-* Set `scope.reordering_value` to `true` if `:reordering`-values are specified.
-
- Fixes #21886.
-
- *Hiroaki Izu*
-
-* Add support for bidirectional destroy dependencies.
+* Allow applications to automatically switch connections.
- Fixes #13609.
+ Adds a middleware and configuration options that can be used in your
+ application to automatically switch between the writing and reading
+ database connections.
- Example:
-
- class Content < ActiveRecord::Base
- has_one :position, dependent: :destroy
- end
-
- class Position < ActiveRecord::Base
- belongs_to :content, dependent: :destroy
- end
-
- *Seb Jacobs*
-
-* Includes HABTM returns correct size now. It's caused by the join dependency
- only instantiates one HABTM object because the join table hasn't a primary key.
-
- Fixes #16032.
-
- Examples:
-
- before:
+ `GET` and `HEAD` requests will read from the replica unless there was
+ a write in the last 2 seconds, otherwise they will read from the primary.
+ Non-get requests will always write to the primary. The middleware accepts
+ an argument for a Resolver class and a Operations class where you are able
+ to change how the auto-switcher works to be most beneficial for your
+ application.
- Project.first.salaried_developers.size # => 3
- Project.includes(:salaried_developers).first.salaried_developers.size # => 1
+ To use the middleware in your application you can use the following
+ configuration options:
- after:
+ ```
+ config.active_record.database_selector = { delay: 2.seconds }
+ config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
+ config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
+ ```
- Project.first.salaried_developers.size # => 3
- Project.includes(:salaried_developers).first.salaried_developers.size # => 3
+ To change the database selection strategy, pass a custom class to the
+ configuration options:
- *Bigxiang*
+ ```
+ config.active_record.database_selector = { delay: 10.seconds }
+ config.active_record.database_resolver = MyResolver
+ config.active_record.database_resolver_context = MyResolver::MyCookies
+ ```
-* Add option to index errors in nested attributes
-
- For models which have nested attributes, errors within those models will
- now be indexed if `:index_errors` is specified when defining a
- has_many relationship, or if its set in the global config.
-
- Example:
-
- class Guitar < ActiveRecord::Base
- has_many :tuning_pegs, index_errors: true
- accepts_nested_attributes_for :tuning_pegs
- end
-
- class TuningPeg < ActiveRecord::Base
- belongs_to :guitar
- validates_numericality_of :pitch
- end
-
- # Old style
- guitar.errors["tuning_pegs.pitch"] = ["is not a number"]
+ *Eileen M. Uchitelle*
- # New style (if defined globally, or set in has_many_relationship)
- guitar.errors["tuning_pegs[1].pitch"] = ["is not a number"]
+* MySQL: Support `:size` option to change text and blob size.
- *Michael Probber*, *Terence Sun*
+ *Ryuta Kamizono*
-* Exit with non-zero status for failed database rake tasks.
+* Make `t.timestamps` with precision by default.
- *Jay Hayes*
+ *Ryuta Kamizono*
-* Queries such as `Computer.joins(:monitor).group(:status).count` will now be
- interpreted as `Computer.joins(:monitor).group('computers.status').count`
- so that when `Computer` and `Monitor` have both `status` columns we don't
- have conflicts in projection.
- *Rafael Sales*
+## Rails 6.0.0.beta1 (January 18, 2019) ##
-* Add ability to default to `uuid` as primary key when generating database migrations.
+* Remove deprecated `#set_state` from the transaction object.
- Example:
+ *Rafael Mendonça França*
- config.generators do |g|
- g.orm :active_record, primary_key_type: :uuid
- end
+* Remove deprecated `#supports_statement_cache?` from the database adapters.
- *Jon McCartie*
+ *Rafael Mendonça França*
-* Don't cache arguments in `#find_by` if they are an `ActiveRecord::Relation`.
+* Remove deprecated `#insert_fixtures` from the database adapters.
- Fixes #20817.
+ *Rafael Mendonça França*
- *Hiroaki Izu*
+* Remove deprecated `ActiveRecord::ConnectionAdapters::SQLite3Adapter#valid_alter_table_type?`.
-* Qualify column name inserted by `group` in calculation.
+ *Rafael Mendonça França*
- Giving `group` an unqualified column name now works, even if the relation
- has `JOIN` with another table which also has a column of the name.
+* Do not allow passing the column name to `sum` when a block is passed.
- *Soutaro Matsumoto*
+ *Rafael Mendonça França*
-* Don't cache prepared statements containing an IN clause or a SQL literal, as
- these queries will change often and are unlikely to have a cache hit.
+* Do not allow passing the column name to `count` when a block is passed.
- *Sean Griffin*
+ *Rafael Mendonça França*
-* Fix `rewhere` in a `has_many` association.
+* Remove delegation of missing methods in a relation to arel.
- Fixes #21955.
+ *Rafael Mendonça França*
- *Josh Branchaud*, *Kal*
+* Remove delegation of missing methods in a relation to private methods of the class.
-* `where` raises ArgumentError on unsupported types.
+ *Rafael Mendonça França*
- Fixes #20473.
+* Deprecate `config.activerecord.sqlite3.represent_boolean_as_integer`.
- *Jake Worth*
+ *Rafael Mendonça França*
-* Add an immutable string type to help reduce memory usage for apps which do
- not need mutation detection on strings.
+* Change `SQLite3Adapter` to always represent boolean values as integers.
- *Sean Griffin*
+ *Rafael Mendonça França*
-* Give `ActiveRecord::Relation#update` its own deprecation warning when
- passed an `ActiveRecord::Base` instance.
+* Remove ability to specify a timestamp name for `#cache_key`.
- Fixes #21945.
+ *Rafael Mendonça França*
- *Ted Johansson*
+* Remove deprecated `ActiveRecord::Migrator.migrations_path=`.
-* Make it possible to pass `:to_table` when adding a foreign key through
- `add_reference`.
+ *Rafael Mendonça França*
- Fixes #21563.
+* Remove deprecated `expand_hash_conditions_for_aggregates`.
- *Yves Senn*
+ *Rafael Mendonça França*
-* No longer pass deprecated option `-i` to `pg_dump`.
+* Set polymorphic type column to NULL on `dependent: :nullify` strategy.
- *Paul Sadauskas*
+ On polymorphic associations both the foreign key and the foreign type columns will be set to NULL.
-* Concurrent `AR::Base#increment!` and `#decrement!` on the same record
- are all reflected in the database rather than overwriting each other.
+ *Laerti Papa*
- *Bogdan Gusiev*
+* Allow permitted instance of `ActionController::Parameters` as argument of `ActiveRecord::Relation#exists?`.
-* Avoid leaking the first relation we call `first` on, per model.
+ *Gannon McGibbon*
- Fixes #21921.
+* Add support for endless ranges introduces in Ruby 2.6.
- *Matthew Draper*, *Jean Boussier*
+ *Greg Navis*
-* Remove unused `pk_and_sequence_for` in `AbstractMysqlAdapter`.
+* Deprecate passing `migrations_paths` to `connection.assume_migrated_upto_version`.
*Ryuta Kamizono*
-* Allow fixtures files to set the model class in the YAML file itself.
-
- To load the fixtures file `accounts.yml` as the `User` model, use:
-
- _fixture:
- model_class: User
- david:
- name: David
-
- Fixes #9516.
-
- *Roque Pinel*
-
-* Don't require a database connection to load a class which uses acceptance
- validations.
-
- *Sean Griffin*
-
-* Correctly apply `unscope` when preloading through associations.
-
- *Jimmy Bourassa*
-
-* Fixed taking precision into count when assigning a value to timestamp attribute.
-
- Timestamp column can have less precision than ruby timestamp
- In result in how big a fraction of a second can be stored in the
- database.
-
-
- m = Model.create!
- m.created_at.usec == m.reload.created_at.usec # => false
- # due to different precision in Time.now and database column
-
- If the precision is low enough, (mysql default is 0, so it is always low
- enough by default) the value changes when model is reloaded from the
- database. This patch fixes that issue ensuring that any timestamp
- assigned as an attribute is converted to column precision under the
- attribute.
-
- *Bogdan Gusiev*
-
-* Introduce `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).
-
- Also deprecate `SchemaCache#tables`, `SchemaCache#table_exists?` and
- `SchemaCache#clear_table_cache!` in favor of their new data source
- counterparts.
-
- *Yves Senn*, *Matthew Draper*
+* MySQL: `ROW_FORMAT=DYNAMIC` create table option by default.
-* Add `ActiveRecord::Base.ignored_columns` to make some columns
- invisible from Active Record.
-
- *Jean Boussier*
-
-* `ActiveRecord::Tasks::MySQLDatabaseTasks` fails if shellout to
- mysql commands (like `mysqldump`) is not successful.
-
- *Steve Mitchell*
-
-* Ensure `select` quotes aliased attributes, even when using `from`.
-
- Fixes #21488.
-
- *Sean Griffin*, *@johanlunds*
-
-* MySQL: support `unsigned` numeric data types.
-
- Example:
-
- create_table :foos do |t|
- t.unsigned_integer :quantity
- t.unsigned_bigint :total
- t.unsigned_float :percentage
- t.unsigned_decimal :price, precision: 10, scale: 2
- end
-
- The `unsigned: true` option may be used for the primary key:
-
- create_table :foos, id: :bigint, unsigned: true do |t|
- …
- end
+ Since MySQL 5.7.9, the `innodb_default_row_format` option defines the default row
+ format for InnoDB tables. The default setting is `DYNAMIC`.
+ The row format is required for indexing on `varchar(255)` with `utf8mb4` columns.
*Ryuta Kamizono*
-* Add `#views` and `#view_exists?` methods on connection adapters.
+* Fix join table column quoting with SQLite.
- *Ryuta Kamizono*
+ *Gannon McGibbon*
-* Correctly dump composite primary key.
+* Allow disabling scopes generated by `ActiveRecord.enum`.
- Example:
+ *Alfred Dominic*
- create_table :barcodes, primary_key: ["region", "code"] do |t|
- t.string :region
- t.integer :code
- end
+* Ensure that `delete_all` on collection proxy returns affected count.
*Ryuta Kamizono*
-* Lookup the attribute name for `restrict_with_error` messages on the
- model class that defines the association.
-
- *kuboon*, *Ronak Jangir*
-
-* Correct query for PostgreSQL 8.2 compatibility.
+* Reset scope after delete on collection association to clear stale offsets of removed records.
- *Ben Murphy*, *Matthew Draper*
+ *Gannon McGibbon*
-* `bin/rails db:migrate` uses
- `ActiveRecord::Tasks::DatabaseTasks.migrations_paths` instead of
- `Migrator.migrations_paths`.
+* Add the ability to prevent writes to a database for the duration of a block.
- *Tobias Bielohlawek*
-
-* Support dropping indexes concurrently in PostgreSQL.
-
- See http://www.postgresql.org/docs/9.4/static/sql-dropindex.html for more
- details.
+ Allows the application to prevent writes to a database. This can be useful when
+ you're building out multiple databases and want to make sure you're not sending
+ writes when you want a read.
- *Grey Baker*
+ If `while_preventing_writes` is called and the query is considered a write
+ query the database will raise an exception regardless of whether the database
+ user is able to write.
-* Deprecate passing conditions to `ActiveRecord::Relation#delete_all`
- and `ActiveRecord::Relation#destroy_all`.
+ This is not meant to be a catch-all for write queries but rather a way to enforce
+ read-only queries without opening a second connection. One purpose of this is to
+ catch accidental writes, not all writes.
- *Wojciech Wnętrzak*
+ *Eileen M. Uchitelle*
-* Instantiating an AR model with `ActionController::Parameters` now raises
- an `ActiveModel::ForbiddenAttributesError` if the parameters include a
- `type` field that has not been explicitly permitted. Previously, the
- `type` field was simply ignored in the same situation.
+* Allow aliased attributes to be used in `#update_columns` and `#update`.
- *Prem Sichanugrist*
+ *Gannon McGibbon*
-* PostgreSQL, `create_schema`, `drop_schema` and `rename_table` now quote
- schema names.
+* Allow spaces in postgres table names.
- Fixes #21418.
+ Fixes issue where "user post" is misinterpreted as "\"user\".\"post\"" when quoting table names with the postgres adapter.
- Example:
+ *Gannon McGibbon*
- create_schema("my.schema")
- # CREATE SCHEMA "my.schema";
+* Cached `columns_hash` fields should be excluded from `ResultSet#column_types`.
- *Yves Senn*
+ PR #34528 addresses the inconsistent behaviour when attribute is defined for an ignored column. The following test
+ was passing for SQLite and MySQL, but failed for PostgreSQL:
-* PostgreSQL, add `:if_exists` option to `#drop_schema`. This makes it
- possible to drop a schema that might exist without raising an exception if
- it doesn't.
+ ```ruby
+ class DeveloperName < ActiveRecord::Type::String
+ def deserialize(value)
+ "Developer: #{value}"
+ end
+ end
- *Yves Senn*
+ class AttributedDeveloper < ActiveRecord::Base
+ self.table_name = "developers"
-* Only try to nullify has_one target association if the record is persisted.
+ attribute :name, DeveloperName.new
- Fixes #21223.
+ self.ignored_columns += ["name"]
+ end
- *Agis Anastasopoulos*
+ developer = AttributedDeveloper.create
+ developer.update_column :name, "name"
-* Uniqueness validator raises descriptive error when running on a persisted
- record without primary key.
+ loaded_developer = AttributedDeveloper.where(id: developer.id).select("*").first
+ puts loaded_developer.name # should be "Developer: name" but it's just "name"
+ ```
- Fixes #21304.
+ *Dmitry Tsepelev*
- *Yves Senn*
+* Make the implicit order column configurable.
-* Add a native JSON data type support in MySQL.
+ When calling ordered finder methods such as `first` or `last` without an
+ explicit order clause, ActiveRecord sorts records by primary key. This can
+ result in unpredictable and surprising behaviour when the primary key is
+ not an auto-incrementing integer, for example when it's a UUID. This change
+ makes it possible to override the column used for implicit ordering such
+ that `first` and `last` will return more predictable results.
Example:
- create_table :json_data_type do |t|
- t.json :settings
+ class Project < ActiveRecord::Base
+ self.implicit_order_column = "created_at"
end
- *Ryuta Kamizono*
-
-* Descriptive error message when fixtures contain a missing column.
-
- Fixes #21201.
+ *Tekin Suleyman*
- *Yves Senn*
+* Bump minimum PostgreSQL version to 9.3.
-* `ActiveRecord::Tasks::PostgreSQLDatabaseTasks` fail if shellout to
- postgresql commands (like `pg_dump`) is not successful.
+ *Yasuo Honda*
- *Bryan Paxton*, *Nate Berkopec*
+* Values of enum are frozen, raising an error when attempting to modify them.
-* Add `ActiveRecord::Relation#in_batches` to work with records and relations
- in batches.
+ *Emmanuel Byrd*
- Available options are `of` (batch size), `load`, `start`, and `finish`.
+* Move `ActiveRecord::StatementInvalid` SQL to error property and include binds as separate error property.
- Examples:
-
- Person.in_batches.each_record(&:party_all_night!)
- Person.in_batches.update_all(awesome: true)
- Person.in_batches.delete_all
- Person.in_batches.each do |relation|
- relation.delete_all
- sleep 10 # Throttles the delete queries
- end
+ `ActiveRecord::ConnectionAdapters::AbstractAdapter#translate_exception_class` now requires `binds` to be passed as the last argument.
- Fixes #20933.
+ `ActiveRecord::ConnectionAdapters::AbstractAdapter#translate_exception` now requires `message`, `sql`, and `binds` to be passed as keyword arguments.
- *Sina Siadat*
-
-* Added methods for PostgreSQL geometric data types to use in migrations.
+ Subclasses of `ActiveRecord::StatementInvalid` must now provide `sql:` and `binds:` arguments to `super`.
Example:
- create_table :foo do |t|
- t.line :foo_line
- t.lseg :foo_lseg
- t.box :foo_box
- t.path :foo_path
- t.polygon :foo_polygon
- t.circle :foo_circle
- end
-
- *Mehmet Emin İNAÇ*
-
-* Add `cache_key` to ActiveRecord::Relation.
-
- Example:
-
- @users = User.where("name like ?", "%Alberto%")
- @users.cache_key
- # => "/users/query-5942b155a43b139f2471b872ac54251f-3-20150714212107656125000"
-
- *Alberto Fernández-Capel*
+ ```
+ class MySubclassedError < ActiveRecord::StatementInvalid
+ def initialize(message, sql:, binds:)
+ super(message, sql: sql, binds: binds)
+ end
+ end
+ ```
-* Properly allow uniqueness validations on primary keys.
+ *Gannon McGibbon*
- Fixes #20966.
-
- *Sean Griffin*, *presskey*
-
-* Don't raise an error if an association failed to destroy when `destroy` was
- called on the parent (as opposed to `destroy!`).
-
- Fixes #20991.
-
- *Sean Griffin*
-
-* `ActiveRecord::RecordNotFound` modified to store model name, primary_key and
- id of the caller model. It allows the catcher of this exception to make
- a better decision to what to do with it.
+* Add an `:if_not_exists` option to `create_table`.
Example:
- class SomeAbstractController < ActionController::Base
- rescue_from ActiveRecord::RecordNotFound, with: :redirect_to_404
-
- private def redirect_to_404(e)
- return redirect_to(posts_url) if e.model == 'Post'
- raise
- end
+ create_table :posts, if_not_exists: true do |t|
+ t.string :title
end
- *Sameer Rahmani*
-
-* Deprecate the keys for association `restrict_dependent_destroy` errors in favor
- of new key names.
-
- Previously `has_one` and `has_many` associations were using the
- `one` and `many` keys respectively. Both of these keys have special
- meaning in I18n (they are considered to be pluralizations) so by
- renaming them to `has_one` and `has_many` we make the messages more explicit
- and most importantly they don't clash with linguistical systems that need to
- validate translation keys (and their pluralizations).
-
- The `:'restrict_dependent_destroy.one'` key should be replaced with
- `:'restrict_dependent_destroy.has_one'`, and `:'restrict_dependent_destroy.many'`
- with `:'restrict_dependent_destroy.has_many'`.
-
- *Roque Pinel*, *Christopher Dell*
-
-* Fix state being carried over from previous transaction.
-
- Considering the following example where `name` is a required attribute.
- Before we had `new_record?` returning `true` for a persisted record:
-
- author = Author.create! name: 'foo'
- author.name = nil
- author.save # => false
- author.new_record? # => true
-
- Fixes #20824.
-
- *Roque Pinel*
-
-* Correctly ignore `mark_for_destruction` when `autosave` isn't set to `true`
- when validating associations.
-
- Fixes #20882.
-
- *Sean Griffin*
-
-* Fix a bug where counter_cache doesn't always work with polymorphic
- relations.
-
- Fixes #16407.
-
- *Stefan Kanev*, *Sean Griffin*
-
-* Ensure that cyclic associations with autosave don't cause duplicate errors
- to be added to the parent record.
-
- Fixes #20874.
-
- *Sean Griffin*
-
-* Ensure that `ActionController::Parameters` can still be passed to nested
- attributes.
-
- Fixes #20922.
-
- *Sean Griffin*
-
-* Deprecate force association reload by passing a truthy argument to
- association method.
-
- For collection association, you can call `#reload` on association proxy to
- force a reload:
-
- @user.posts.reload # Instead of @user.posts(true)
-
- For singular association, you can call `#reload` on the parent object to
- clear its association cache then call the association method:
-
- @user.reload.profile # Instead of @user.profile(true)
-
- Passing a truthy argument to force association to reload will be removed in
- Rails 5.1.
-
- *Prem Sichanugrist*
-
-* Replaced `ActiveSupport::Concurrency::Latch` with `Concurrent::CountDownLatch`
- from the concurrent-ruby gem.
-
- *Jerry D'Antonio*
-
-* 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.
-
- 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*
-
-* Correctly raise `ActiveRecord::AssociationTypeMismatch` when assigning
- a wrong type to a namespaced association.
-
- Fixes #20545.
-
- *Diego Carrion*
-
-* `validates_absence_of` respects `marked_for_destruction?`.
-
- Fixes #20449.
-
- *Yves Senn*
-
-* Include the `Enumerable` module in `ActiveRecord::Relation`
-
- *Sean Griffin*, *bogdan*
-
-* Use `Enumerable#sum` in `ActiveRecord::Relation` if a block is given.
-
- *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*
-
-* Make `remove_foreign_key` reversible. Any foreign key options must be
- specified, similar to `remove_column`.
-
- *Aster Ryan*
-
-* Add `:_prefix` and `:_suffix` options to `enum` definition.
+ That would execute:
- Fixes #17511, #17415.
+ CREATE TABLE IF NOT EXISTS posts (
+ ...
+ )
- *Igor Kapkov*
+ If the table already exists, `if_not_exists: false` (the default) raises an
+ exception whereas `if_not_exists: true` does nothing.
-* Correctly handle decimal arrays with defaults in the schema dumper.
+ *fatkodima*, *Stefan Kanev*
- Fixes #20515.
+* Defining an Enum as a Hash with blank key, or as an Array with a blank value, now raises an `ArgumentError`.
- *Sean Griffin*, *jmondo*
+ *Christophe Maximin*
-* Deprecate the PostgreSQL `:point` type in favor of a new one which will return
- `Point` objects instead of an `Array`
+* Adds support for multiple databases to `rails db:schema:cache:dump` and `rails db:schema:cache:clear`.
- *Sean Griffin*
+ *Gannon McGibbon*
-* Ensure symbols passed to `ActiveRecord::Relation#select` are always treated
- as columns.
-
- Fixes #20360.
+* `update_columns` now correctly raises `ActiveModel::MissingAttributeError`
+ if the attribute does not exist.
*Sean Griffin*
-* Do not set `sql_mode` if `strict: :default` is specified.
-
- # config/database.yml
- production:
- adapter: mysql2
- database: foo_prod
- user: foo
- strict: :default
-
- *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*
-
-* Allow the use of symbols or strings to specify enum values in test
- fixtures:
-
- awdr:
- title: "Agile Web Development with Rails"
- status: :proposed
-
- *George Claghorn*
+* Add support for hash and URL configs in database hash of `ActiveRecord::Base.connected_to`.
-* Clear query cache when `ActiveRecord::Base#reload` is called.
+ ````
+ User.connected_to(database: { writing: "postgres://foo" }) do
+ User.create!(name: "Gannon")
+ end
- *Shane Hender, Pierre Nespo*
+ config = { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }
+ User.connected_to(database: { reading: config }) do
+ User.count
+ end
+ ````
-* Include stored procedures and function on the MySQL structure dump.
+ *Gannon McGibbon*
- *Jonathan Worek*
+* Support default expression for MySQL.
-* Pass `:extend` option for `has_and_belongs_to_many` associations to the
- underlying `has_many :through`.
+ MySQL 8.0.13 and higher supports default value to be a function or expression.
- *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*
-
-* Correctly dump `:options` on `create_table` for MySQL.
+ https://dev.mysql.com/doc/refman/8.0/en/create-table.html
*Ryuta Kamizono*
-* PostgreSQL: `:collation` support for string and text columns.
+* Support expression indexes for MySQL.
- Example:
+ MySQL 8.0.13 and higher supports functional key parts that index
+ expression values rather than column or column prefix values.
- create_table :foos do |t|
- t.string :string_en, collation: 'en_US.UTF-8'
- t.text :text_ja, collation: 'ja_JP.UTF-8'
- end
+ https://dev.mysql.com/doc/refman/8.0/en/create-index.html
*Ryuta Kamizono*
-* Remove `ActiveRecord::Serialization::XmlSerializer` from core.
+* Fix collection cache key with limit and custom select to avoid ambiguous timestamp column error.
- *Zachary Scott*
+ Fixes #33056.
-* Make `unscope` aware of "less than" and "greater than" conditions.
+ *Federico Martinez*
- *TAKAHASHI Kazuaki*
+* Add basic API for connection switching to support multiple databases.
-* `find_by` and `find_by!` raise `ArgumentError` when called without
- arguments.
+ 1) Adds a `connects_to` method for models to connect to multiple databases. Example:
- *Kohei Suzuki*
+ ```
+ class AnimalsModel < ApplicationRecord
+ self.abstract_class = true
-* Revert behavior of `db:schema:load` back to loading the full
- environment. This ensures that initializers are run.
+ connects_to database: { writing: :animals_primary, reading: :animals_replica }
+ end
- Fixes #19545.
+ class Dog < AnimalsModel
+ # connected to both the animals_primary db for writing and the animals_replica for reading
+ end
+ ```
- *Yves Senn*
+ 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.
-* Fix missing index when using `timestamps` with the `index` option.
+ ```
+ 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
+ ```
- The `index` option used with `timestamps` should be passed to both
- `column` definitions for `created_at` and `updated_at` rather than just
- the first.
+ ```
+ 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
+ ```
- *Paul Mucur*
+ *Eileen M. Uchitelle*
-* Rename `:class` to `:anonymous_class` in association options.
+* Enum raises on invalid definition values
- Fixes #19659.
+ 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`.
- *Andrew White*
+ Fixes #33961
-* Autosave existing records on a has many through association when the parent
- is new.
+ *Alberto Almagro*
- Fixes #19782.
+* Reloading associations now clears the Query Cache like `Persistence#reload` does.
- *Sean Griffin*
+ ```
+ class Post < ActiveRecord::Base
+ has_one :category
+ belongs_to :author
+ has_many :comments
+ end
-* 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.
+ # Each of the following will now clear the query cache.
+ post.reload_category
+ post.reload_author
+ post.comments.reload
+ ```
- *Andrey Voronkov*
+ *Christophe Maximin*
-* 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.
-
- *pinglamb*
-
-* Correctly persist a serialized attribute that has been returned to
- its default value by an in-place modification.
-
- Fixes #19467.
-
- *Matthew Draper*
-
-* 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.
-
- Fixes #19420.
-
- *Jake Waller*
-
-* 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`.
+* Fix `transaction` reverting for migrations.
- *Yves Senn*
+ Before: Commands inside a `transaction` in a reverted migration ran uninverted.
+ Now: This change fixes that by reverting commands inside `transaction` block.
-* Restore aborted transaction state when `disable_referential_integrity` fails
- due to missing permissions.
+ *fatkodima*, *David Verhasselt*
- *Toby Ovod-Everett*, *Yves Senn*
+* Raise an error instead of scanning the filesystem root when `fixture_path` is blank.
-* In PostgreSQL, print a warning message if `disable_referential_integrity`
- fails due to missing permissions.
+ *Gannon McGibbon*, *Max Albrecht*
- *Andrey Nering*, *Yves Senn*
+* Allow `ActiveRecord::Base.configurations=` to be set with a symbolized hash.
-* Allow a `:limit` option for MySQL bigint primary key support.
+ *Gannon McGibbon*
- Example:
-
- create_table :foos, id: :primary_key, limit: 8 do |t|
- end
+* Don't update counter cache unless the record is actually saved.
- # or
-
- create_table :foos, id: false do |t|
- t.primary_key :id, limit: 8
- end
+ Fixes #31493, #33113, #33117.
*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.)
+* Deprecate `ActiveRecord::Result#to_hash` in favor of `ActiveRecord::Result#to_a`.
- *Josef Šimánek*
+ *Gannon McGibbon*, *Kevin Cheng*
-* Fixed `ActiveRecord::Relation#becomes!` and `changed_attributes` issues for type
- columns.
+* SQLite3 adapter supports expression indexes.
- Fixes #17139.
+ ```
+ create_table :users do |t|
+ t.string :email
+ end
- *Miklos Fazekas*
+ add_index :users, 'lower(email)', name: 'index_users_on_email', unique: true
+ ```
-* Format the time string according to the precision of the time column.
+ *Gray Kemmey*
- *Ryuta Kamizono*
-
-* Allow a `:precision` option for time type columns.
-
- *Ryuta Kamizono*
-
-* Add `ActiveRecord::Base.suppress` to prevent the receiver from being saved
- during the given block.
-
- 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):
-
- 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
+* Allow subclasses to redefine autosave callbacks for associated records.
- *Michael Ryan*
+ Fixes #33305.
-* `:time` option added for `#touch`.
+ *Andrey Subbota*
- Fixes #18905.
+* Bump minimum MySQL version to 5.5.8.
- *Hyonjee Joo*
+ *Yasuo Honda*
-* Deprecate passing of `start` value to `find_in_batches` and `find_each`
- in favour of `begin_at` value.
+* Use MySQL utf8mb4 character set by default.
- *Vipul A M*
+ `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.
-* Add `foreign_key_exists?` method.
+ *Yasuo Honda*
- *Tõnis Simo*
+* Fix duplicated record creation when using nested attributes with `create_with`.
-* 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`)
-
- # Before:
-
- users.none?
- # SELECT "users".* FROM "users"
-
- users.one?
- # SELECT "users".* FROM "users"
-
- # After:
-
- users.none?
- # SELECT 1 AS one FROM "users" LIMIT 1
-
- 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.
-
- Fixes #18806.
-
- Example:
-
- StiOne.none.scoping do
- StiTwo.all
- end
-
-
- *Sean Griffin*
+ *Darwin Wu*
-* `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.
+* 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.
- Fixes #18664.
+ ```
+ 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] ...>
+ ```
- *Yves Senn*
+ *Zhang Kang*, *Yoshiyuki Kinjo*
-* `find_in_batches` now accepts an `:finish` 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`.
-
- These are not valid values to merge in a relation, so it should warn users
- early.
-
- *Rafael Mendonça França*
-
-* Use `SCHEMA` instead of `DB_STRUCTURE` for specifying a structure file.
-
- This makes the `db:structure` tasks consistent with `test:load_structure`.
-
- *Dieter Komendera*
-
-* Respect custom primary keys for associations when calling `Relation#where`
-
- Fixes #18813.
-
- *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*
-
-* 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.
-
- 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.
-
- *Chris Sinjakli*
-
-* 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:
-
- Example:
-
- SplitTest.group(:key).count
- Property.group(:value).count
-
- *Bogdan Gusiev*
-
-* Added the `#or` method on `ActiveRecord::Relation`, allowing use of the OR
- operator to combine WHERE or HAVING clauses.
-
- Example:
-
- Post.where('id = 1').or(Post.where('id = 2'))
- # => SELECT * FROM posts WHERE (id = 1) OR (id = 2)
+* `ActiveRecord::Base.configurations` now returns an object.
- *Sean Griffin*, *Matthew Draper*, *Gael Muller*, *Olivier El Mekki*
+ `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.
-* Don't define autosave association callbacks twice from
- `accepts_nested_attributes_for`.
+ For example, the following `database.yml`:
- Fixes #18704.
+ ```
+ development:
+ adapter: sqlite3
+ database: db/development.sqlite3
+ ```
- *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*
+ Used to become a hash:
-* 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.
+ ```
+ { "development" => { "adapter" => "sqlite3", "database" => "db/development.sqlite3" } }
+ ```
- Fixes #18580.
+ Is now converted into the following object:
- *Sean Griffin*
+ ```
+ #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[
+ #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development",
+ @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>
+ ]
+ ```
-* Don't remove join dependencies in `Relation#exists?`
+ 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.
- Fixes #18632.
+ ```
+ ActiveRecord::Base.configurations.configs_for(env_name: "development")
+ ActiveRecord::Base.configurations.configs_for(env_name: "development", spec_name: "primary")
+ ```
- *Sean Griffin*
+ *Eileen M. Uchitelle*, *Aaron Patterson*
-* Invalid values assigned to a JSON column are assumed to be `nil`.
+* Add database configuration to disable advisory locks.
- Fixes #18629.
+ ```
+ production:
+ adapter: postgresql
+ advisory_locks: false
+ ```
- *Sean Griffin*
+ *Guo Xiang*
-* 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.
+* SQLite3 adapter `alter_table` method restores foreign keys.
- *Sean Griffin*
+ *Yasuo Honda*
-* Introduce the `:if_exists` option for `drop_table`.
+* Allow `:to_table` option to `invert_remove_foreign_key`.
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=`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated access to connection specification using a string accessor.
-
- 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`.
+ remove_foreign_key :accounts, to_table: :owners
- *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*
+ *Nikolay Epifanov*, *Rich Chen*
-* Change the way in which callback chains can be halted.
+* Add environment & load_config dependency to `bin/rake db:seed` to enable
+ seed load in environments without Rails and custom DB configuration
- 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
- `ActiveSupport.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.
-
- *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`:
+ *Tobias Bielohlawek*
- http://bugs.mysql.com/bug.php?id=68760
+* Fix default value for mysql time types with specified precision.
- *Ryuta Kamizono*
+ *Nikolay Kondratyev*
-* Allow a precision option for MySQL datetimes.
+* Fix `touch` option to behave consistently with `Persistence#touch` method.
*Ryuta Kamizono*
-* Fixed automatic `inverse_of` for models nested in a module.
+* Migrations raise when duplicate column definition.
- *Andrew McCloud*
+ Fixes #33024.
-* Change `ActiveRecord::Relation#update` behavior so that it can
- be called without passing ids of the records to be updated.
+ *Federico Martinez*
- This change allows updating multiple records returned by
- `ActiveRecord::Relation` with callbacks and validations.
+* Bump minimum SQLite version to 3.8
- # Before
- # ArgumentError: wrong number of arguments (1 for 2)
- Comment.where(group: 'expert').update(body: "Group of Rails Experts")
+ *Yasuo Honda*
- # After
- # Comments with group expert updated with body "Group of Rails Experts"
- Comment.where(group: 'expert').update(body: "Group of Rails Experts")
+* Fix parent record should not get saved with duplicate children records.
- *Prathamesh Sonpatki*
+ Fixes #32940.
-* Fix `reaping_frequency` option when the value is a string.
+ *Santosh Wadghule*
- This usually happens when it is configured using `DATABASE_URL`.
+* Fix logic on disabling commit callbacks so they are not called unexpectedly when errors occur.
- *korbin*
+ *Brian Durand*
-* Fix error message when trying to create an associated record and the foreign
- key is missing.
+* Ensure `Associations::CollectionAssociation#size` and `Associations::CollectionAssociation#empty?`
+ use loaded association ids if present.
- Before this fix the following exception was being raised:
-
- NoMethodError: undefined method `val' for #<Arel::Nodes::BindParam:0x007fc64d19c218>
-
- Now the message is:
-
- ActiveRecord::UnknownAttributeError: unknown attribute 'foreign_key' for Model.
-
- *Rafael Mendonça França*
+ *Graham Turner*
-* Fix change detection problem for PostgreSQL bytea type and
- `ArgumentError: string contains null byte` exception with pg-0.18.
+* Add support to preload associations of polymorphic associations when not all the records have the requested associations.
- Fixes #17680.
+ *Dana Sherson*
- *Lars Kanis*
-
-* 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.
-
- Fixes #18070.
-
- *arthurnn*
-
-* `validates_size_of` / `validates_length_of` do not count records
- which are `marked_for_destruction?`.
-
- Fixes #7247.
-
- *Yves Senn*
-
-* Ensure `first!` and friends work on loaded associations.
-
- Fixes #18237.
-
- *Sean Griffin*
-
-* `eager_load` preserves readonly flag for associations.
-
- Fixes #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`.
-
- *Yves Senn*
+ Person.where(name: "David").touch_all(time: Time.new(2020, 5, 16, 0, 0, 0))
-* Fix bug with `ActiveRecord::Type::Numeric` that caused negative values to
- be marked as having changed when set to the same negative value.
+ *fatkodima*, *duggiefresh*
- Fixes #18161.
+* Add `ActiveRecord::Base.base_class?` predicate.
- *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.
-
- Fixes #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.5.0 or newer.
-* Remove deprecated behavior allowing nested arrays to be passed as query
- values.
+ *Jeremy Daer*, *Kasper Timm Hansen*
- *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 1f496cf280..79e52c53af 100644
--- a/activerecord/MIT-LICENSE
+++ b/activerecord/MIT-LICENSE
@@ -1,4 +1,6 @@
-Copyright (c) 2004-2016 David Heinemeier Hansson
+Copyright (c) 2004-2019 David Heinemeier Hansson
+
+Arel originally copyright (c) 2007-2016 Nick Kallen, Bryan Helmkamp, Emilio Tagua, Aaron Patterson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc
index cfbee4d6f7..be573af4ba 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -13,6 +13,8 @@ columns. Although these mappings can be defined explicitly, it's recommended
to follow naming conventions, especially when getting started with the
library.
+You can read more about Active Record in the {Active Record Basics}[https://edgeguides.rubyonrails.org/active_record_basics.html] guide.
+
A short rundown of some of the major features:
* Automated mapping between classes and tables, attributes and columns.
@@ -26,7 +28,7 @@ The Product class is automatically mapped to the table named "products",
which might look like this:
CREATE TABLE products (
- id int NOT NULL auto_increment,
+ id bigint NOT NULL auto_increment,
name varchar(255),
PRIMARY KEY (id)
);
@@ -162,7 +164,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,
@@ -199,16 +201,16 @@ 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
API documentation is at:
-* http://api.rubyonrails.org
+* https://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc
index cd22f76d01..37473c37c6 100644
--- a/activerecord/RUNNING_UNIT_TESTS.rdoc
+++ b/activerecord/RUNNING_UNIT_TESTS.rdoc
@@ -1,7 +1,7 @@
== Setup
If you don't have an environment for running tests, read
-http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#setting-up-a-development-environment
+https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#setting-up-a-development-environment
== Running the Tests
@@ -43,5 +43,9 @@ You can override the +connections:+ parameter in either file using the +ARCONN+
$ 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 46df733cfe..f259ae7e12 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -1,29 +1,28 @@
-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 = []
tasks.each do |task|
- begin
- Rake::Task[task].invoke
- rescue Exception
- errors << task
- end
+ Rake::Task[task].invoke
+ rescue Exception
+ errors << task
end
abort "Errors running #{errors.join(', ')}" if errors.any?
end
-desc 'Run mysql2, sqlite, and postgresql tests by default'
-task :default => :test
+desc "Run mysql2, sqlite, and postgresql tests by default"
+task default: :test
task :package
-task "package:clean"
-desc 'Run 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) :
@@ -40,19 +39,21 @@ namespace :test do
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( 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
@@ -62,23 +63,102 @@ 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\//
- } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
- sh(Gem.ruby, '-w' ,"-Itest", file)
- end or raise "Failures"
+
+ dash_i = [
+ "test",
+ "lib",
+ "../activesupport/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)
+
+ # Every test file loads "cases/helper" first, so doing it
+ # post-fork gains us nothing.
+
+ # We need to dance around minitest autorun, though.
+ require "minitest"
+ Minitest.instance_eval do
+ alias _original_autorun autorun
+ def autorun
+ # no-op
+ end
+ require "cases/helper"
+ alias autorun _original_autorun
+ end
+
+ failing_files = []
+
+ test_options = ENV["TESTOPTS"].to_s.split(/[\s]+/)
+
+ test_files = (Dir["test/cases/**/*_test.rb"].reject {
+ |x| x.include?("/adapters/")
+ } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).sort
+
+ if ENV["BUILDKITE_PARALLEL_JOB_COUNT"]
+ n = ENV["BUILDKITE_PARALLEL_JOB"].to_i
+ m = ENV["BUILDKITE_PARALLEL_JOB_COUNT"].to_i
+
+ test_files = test_files.each_slice(m).map { |slice| slice[n] }.compact
+ end
+
+ test_files.each do |file|
+ puts "--- #{file}"
+ fake_command = Shellwords.join([
+ FileUtils::RUBY,
+ "-w",
+ *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
+
+ Minitest.autorun
+
+ load file
+ }
+
+ unless $?.success?
+ failing_files << file
+ puts "^^^ +++"
+ end
+ puts
+ end
+
+ puts "--- All tests completed"
+ unless failing_files.empty?
+ puts "^^^ +++"
+ puts
+ puts "Failed in:"
+ failing_files.each do |file|
+ puts " #{file}"
+ end
+ puts
+
+ exit 1
+ end
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
@@ -88,59 +168,59 @@ end
namespace :db do
namespace :mysql do
- desc 'Build the MySQL test databases'
+ connection_arguments = lambda do |connection_name|
+ config = ARTest.config["connections"]["mysql2"][connection_name]
+ ["--user=#{config["username"]}", "--password=#{config["password"]}", ("--host=#{config["host"]}" if config["host"])].join(" ")
+ end
+
+ desc "Build the MySQL test databases"
task :build do
- config = ARTest.config['connections']['mysql2']
- %x( mysql --user=#{config['arunit']['username']} --password=#{config['arunit']['password']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
- %x( mysql --user=#{config['arunit2']['username']} --password=#{config['arunit2']['password']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
+ config = ARTest.config["connections"]["mysql2"]
+ %x( mysql #{connection_arguments["arunit"]} -e "create DATABASE #{config["arunit"]["database"]} DEFAULT CHARACTER SET utf8mb4" )
+ %x( mysql #{connection_arguments["arunit2"]} -e "create DATABASE #{config["arunit2"]["database"]} DEFAULT CHARACTER SET utf8mb4" )
end
- desc 'Drop the MySQL test databases'
+ desc "Drop the MySQL test databases"
task :drop do
- config = ARTest.config['connections']['mysql2']
- %x( mysqladmin --user=#{config['arunit']['username']} --password=#{config['arunit']['password']} -f drop #{config['arunit']['database']} )
- %x( mysqladmin --user=#{config['arunit2']['username']} --password=#{config['arunit2']['password']} -f drop #{config['arunit2']['database']} )
+ config = ARTest.config["connections"]["mysql2"]
+ %x( mysqladmin #{connection_arguments["arunit"]} -f drop #{config["arunit"]["database"]} )
+ %x( mysqladmin #{connection_arguments["arunit2"]} -f drop #{config["arunit2"]["database"]} )
end
- desc 'Rebuild the MySQL test databases'
- 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 881dee13e4..f73233c38b 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.5.0"
- 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 = "https://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'
+ s.add_dependency "activesupport", version
+ s.add_dependency "activemodel", version
end
diff --git a/activerecord/bin/test b/activerecord/bin/test
index 7417b068bf..9ecf27ce67 100755
--- a/activerecord/bin/test
+++ b/activerecord/bin/test
@@ -1,6 +1,15 @@
#!/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 ""
@@ -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 c3c46c19c5..024e503ec7 100644
--- a/activerecord/examples/performance.rb
+++ b/activerecord/examples/performance.rb
@@ -1,10 +1,12 @@
+# 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)
@@ -42,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
@@ -72,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..."
@@ -95,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,
@@ -108,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
@@ -140,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 5a041e8417..280b786d73 100644
--- a/activerecord/examples/simple.rb
+++ b/activerecord/examples/simple.rb
@@ -1,13 +1,15 @@
-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 baa497dc98..7d66158f47 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2004-2016 David Heinemeier Hansson
+# Copyright (c) 2004-2019 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
@@ -44,9 +46,8 @@ module ActiveRecord
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
@@ -56,7 +57,7 @@ module ActiveRecord
autoload :Querying
autoload :CollectionCacheKey
autoload :ReadonlyAttributes
- autoload :RecordInvalid, 'active_record/validations'
+ autoload :RecordInvalid, "active_record/validations"
autoload :Reflection
autoload :RuntimeRegistry
autoload :Sanitization
@@ -68,17 +69,17 @@ module ActiveRecord
autoload :StatementCache
autoload :Store
autoload :Suppressor
- autoload :TableMetadata
autoload :Timestamp
autoload :Transactions
autoload :Translation
autoload :Validations
autoload :SecureToken
+ autoload :DatabaseSelector, "active_record/middleware/database_selector"
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
@@ -86,11 +87,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
@@ -101,11 +104,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
@@ -149,17 +154,24 @@ module ActiveRecord
end
end
+ module Middleware
+ extend ActiveSupport::Autoload
+
+ autoload :DatabaseSelector, "active_record/middleware/database_selector"
+ end
+
module Tasks
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
@@ -176,5 +188,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 3ff41ed81b..3250e29b82 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -1,8 +1,8 @@
+# frozen_string_literal: true
+
module ActiveRecord
# See ActiveRecord::Aggregations::ClassMethods for documentation
module Aggregations
- extend ActiveSupport::Concern
-
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(amount currency)
- # 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 +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 c18e88e4cf..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
@@ -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 77d17fc975..64c20adc87 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1,7 +1,9 @@
-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:
@@ -90,13 +92,23 @@ module ActiveRecord
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)}?")
+ 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 HasManyThroughOrderError < ActiveRecordError #:nodoc:
+ def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil)
+ if owner_class_name && reflection && through_reflection
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through '#{owner_class_name}##{through_reflection.name}' before the through association is defined.")
+ else
+ super("Cannot have a has_many :through association before the through association is defined.")
+ end
+ end
+ end
+
class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc:
def initialize(owner = nil, reflection = nil)
if owner && reflection
@@ -107,30 +119,25 @@ module ActiveRecord
end
end
- class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
- end
+ class AmbiguousSourceReflectionForThroughAssociation < ActiveRecordError # :nodoc:
+ def initialize(klass, macro, association_name, options, possible_sources)
+ example_options = options.dup
+ example_options[:source] = possible_sources.first
- class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
+ 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 HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner = nil, reflection = nil)
- if owner && reflection
- super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
- else
- super("Cannot associate new records.")
- end
- end
+ class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
end
- class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner = nil, reflection = nil)
- if owner && reflection
- super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
- else
- super("Cannot dissociate new records.")
- end
- end
+ class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
end
class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
@@ -162,16 +169,6 @@ module ActiveRecord
end
end
- class ReadOnlyAssociation < ActiveRecordError #:nodoc:
- def initialize(reflection = nil)
- if reflection
- super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
- else
- super("Read-only reflection error.")
- end
- end
- end
-
# This error is raised when trying to destroy a parent instance in N:1 or 1:1 associations
# (has_many, has_one) when there is at least 1 child associated instance.
# ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
@@ -197,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)
@@ -239,7 +241,7 @@ module ActiveRecord
association
end
- def association_cached?(name) # :nodoc
+ def association_cached?(name) # :nodoc:
@association_cache.key?(name)
end
@@ -255,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
@@ -274,1556 +276,1590 @@ 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
- # ActiveRecord::Base. 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 #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 int NOT NULL auto_increment,
- # account_id int default NULL,
- # name varchar default NULL,
- # PRIMARY KEY (id)
- # )
- #
- # CREATE TABLE accounts (
- # id int 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 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>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(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 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
- #
- # 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 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(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.
- # 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.
- #
- # === 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 #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]
- # 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 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. 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 ActiveRecord::RecordInvalid
- # 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 #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.
- # [: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]
- # 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 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. 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 ActiveRecord::RecordInvalid
- # 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 #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]
- # 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 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. 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[5.0]
- # 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 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
#
- # 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 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).
- #
- # === 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
+ # 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>.
- # [: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 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)
- 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, or a custom scope, 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+. Polymorphic type will also be nullified
+ # on polymorphic associations. 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
+
+ # 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+. Polymorphic type column is also nullified
+ # on polymorphic associations. 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
- 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 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
- builder = Builder::HasAndBelongsToMany.new name, self, options
+ # 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)
- join_model = builder.through_model
+ builder = Builder::HasAndBelongsToMany.new name, self, options
- const_set join_model.name, join_model
- private_constant join_model.name
+ join_model = builder.through_model
- middle_reflection = builder.middle_reflection join_model
+ const_set join_model.name, join_model
+ private_constant join_model.name
- Builder::HasMany.define_callbacks self, middle_reflection
- Reflection.add_reflection self, middle_reflection.name, middle_reflection
- middle_reflection.parent_reflection = habtm_reflection
+ middle_reflection = builder.middle_reflection join_model
- 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
- }
+ Builder::HasMany.define_callbacks self, middle_reflection
+ Reflection.add_reflection self, middle_reflection.name, middle_reflection
+ middle_reflection.parent_reflection = habtm_reflection
- hm_options = {}
- hm_options[:through] = middle_reflection.name
- hm_options[:source] = join_model.right_reflection.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
+ }
- [: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
+ hm_options = {}
+ hm_options[:through] = middle_reflection.name
+ hm_options[:source] = join_model.right_reflection.name
- has_many name, scope, hm_options, &extension
- self._reflections[name.to_s].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
+
+ 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 021bc32237..272eede824 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -1,51 +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::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
@@ -53,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)
@@ -76,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 f7edfbfb5f..0bb63b97ae 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
@@ -15,11 +17,27 @@ module ActiveRecord
# CollectionAssociation
# HasManyAssociation + ForeignAssociation
# HasManyThroughAssociation + ThroughAssociation
+ #
+ # Associations in Active Record are middlemen between the object that
+ # holds the association, known as the <tt>owner</tt>, and the associated
+ # result set, known as the <tt>target</tt>. Association metadata is available in
+ # <tt>reflection</tt>, which is an instance of <tt>ActiveRecord::Reflection::AssociationReflection</tt>.
+ #
+ # For example, given
+ #
+ # class Blog < ActiveRecord::Base
+ # has_many :posts
+ # end
+ #
+ # blog = Blog.first
+ #
+ # The association of <tt>blog.posts</tt> has the object +blog+ as its
+ # <tt>owner</tt>, the collection of its posts as <tt>target</tt>, and
+ # the <tt>reflection</tt> object represents a <tt>:has_many</tt> macro.
class Association #:nodoc:
attr_reader :owner, :target, :reflection
- attr_accessor :inversed
- delegate :options, :to => :reflection
+ delegate :options, to: :reflection
def initialize(owner, reflection)
reflection.check_validity!
@@ -30,14 +48,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 +57,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 +85,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 +95,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 +104,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 +165,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
@@ -166,14 +180,57 @@ module ActiveRecord
def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc:
except_from_scope_attributes ||= {}
skip_assign = [reflection.foreign_key, reflection.type].compact
- assigned_keys = record.changed
+ assigned_keys = record.changed_attribute_names_to_save
assigned_keys += except_from_scope_attributes.keys.map(&:to_s)
- attributes = create_scope.except(*(assigned_keys - skip_assign))
- record.assign_attributes(attributes)
+ 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
+ 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 |params|
+ as = AssociationScope.create { params.bind }
+ target_scope.merge!(as.scope(self))
+ end
+
+ binds = AssociationScope.get_bind_values(owner, reflection.chain)
+ sc.execute(binds, conn) { |record| set_inverse_instance(record) } || []
+ 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 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
@@ -185,8 +242,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
@@ -217,12 +274,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.
@@ -245,18 +309,19 @@ 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, attributes)
+ yield(record) if block_given?
end
end
# Returns true if statement cache should be skipped on the association reader.
- def skip_statement_cache?
+ def skip_statement_cache?(scope)
reflection.has_scope? ||
scope.eager_loading? ||
klass.scope_attributes? ||
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 882f1225fc..9e38380611 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,17 @@ 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
+ scope = add_constraints(scope, owner, chain)
+ scope.limit!(1) unless reflection.collection?
+ scope
end
def self.get_bind_values(owner, chain)
@@ -38,129 +37,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.to_sym => { 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.to_sym => { 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)
+ def add_constraints(scope, owner, chain)
+ scope = last_chain_scope(scope, chain.last, owner)
- reflection = chain_head
- loop do
- break unless reflection
- table = reflection.alias_name
-
- 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.unscope!(*item.unscope_values)
- scope.where_clause += item.where_clause
- scope.order_values |= item.order_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 41698c5360..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_on_replace(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,26 +34,60 @@ module ActiveRecord
@updated
end
- def decrement_counters # :nodoc:
+ def decrement_counters
update_counters(-1)
end
- def increment_counters # :nodoc:
+ def increment_counters
update_counters(1)
end
+ 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
+
+ foreign_key_was = owner.attribute_before_last_save(reflection.foreign_key)
+
+ if foreign_key_was && model_was < ActiveRecord::Base
+ update_counters_via_scope(model_was, foreign_key_was, -1)
+ end
+ end
+
+ 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 update_counters(by)
if require_counter_update? && foreign_key_present?
if target && !stale_target?
- target.increment!(reflection.counter_cache_column, by)
+ target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch])
else
- klass.update_counters(target_id, reflection.counter_cache_column => by)
+ update_counters_via_scope(klass, owner._read_attribute(reflection.foreign_key), by)
end
end
end
+ 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
@@ -59,24 +96,12 @@ module ActiveRecord
reflection.counter_cache_column && owner.persisted?
end
- def update_counters_on_replace(record)
- if require_counter_update? && different_target?(record)
- record.increment!(reflection.counter_cache_column)
- decrement_counters
- 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)
- 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?
@@ -90,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 d0534056d9..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.
#
@@ -36,11 +38,6 @@ module ActiveRecord::Associations::Builder # :nodoc:
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 # :nodoc:
def self.define_readers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}(*args)
- association(:#{name}).reader(*args)
+ def #{name}
+ association(:#{name}).reader
end
CODE
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 346329c610..fc00f1e900 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord::Associations::Builder # :nodoc:
class BelongsTo < SingularAssociation #:nodoc:
def self.macro
@@ -5,7 +7,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
def self.valid_options(options)
- super + [:polymorphic, :touch, :counter_cache, :optional]
+ super + [:polymorphic, :touch, :counter_cache, :optional, :default]
end
def self.valid_dependent_options
@@ -16,70 +18,40 @@ module ActiveRecord::Associations::Builder # :nodoc:
super
add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache]
add_touch_callbacks(model, reflection) if reflection.options[:touch]
- 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?
- if reflection.polymorphic?
- model = attribute(reflection.foreign_type).try(:constantize)
- model_was = attribute_was(reflection.foreign_type).try(:constantize)
- else
- model = reflection.klass
- model_was = reflection.klass
- end
-
- 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_was.respond_to?(:decrement_counter)
- model_was.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
@@ -105,13 +77,29 @@ module ActiveRecord::Associations::Builder # :nodoc:
n = reflection.name
touch = reflection.options[:touch]
- callback = lambda { |record|
- BelongsTo.touch_record(record, foreign_key, n, touch, belongs_to_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)
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index f25bd7ca9f..5848cd9112 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 # :nodoc:
class CollectionAssociation < Association #:nodoc:
-
CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
def self.valid_options(options)
@@ -21,11 +20,11 @@ module ActiveRecord::Associations::Builder # :nodoc:
}
end
- def self.define_extensions(model, name)
+ def self.define_extensions(model, name, &block)
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)
+ extension = Module.new(&block)
+ model.module_parent.const_set(extension_module_name, extension)
end
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 5fbd79d118..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 @@
+# frozen_string_literal: true
+
module ActiveRecord::Associations::Builder # :nodoc:
class HasAndBelongsToMany # :nodoc:
- class JoinTableResolver # :nodoc:
- KnownTable = Struct.new :join_table
-
- class KnownClass # :nodoc:
- 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
-
attr_reader :lhs_model, :association_name, :options
def initialize(association_name, lhs_model, options)
@@ -42,10 +11,8 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
def through_model
- habtm = JoinTableResolver.build lhs_model, association_name, options
-
join_model = Class.new(ActiveRecord::Base) {
- class << self;
+ class << self
attr_accessor :left_model
attr_accessor :name
attr_accessor :table_name_resolver
@@ -54,7 +21,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
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)
@@ -76,13 +45,15 @@ module ActiveRecord::Associations::Builder # :nodoc:
left_model.retrieve_connection
end
- def self.primary_key
- false
- 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.table_name_resolver = -> { table_name }
join_model.left_model = lhs_model
join_model.add_left_association :left_side, anonymous_class: lhs_model
@@ -92,7 +63,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
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,
@@ -103,29 +74,41 @@ module ActiveRecord::Associations::Builder # :nodoc:
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 7864d4c536..5b9617bc6d 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord::Associations::Builder # :nodoc:
class HasMany < CollectionAssociation #:nodoc:
def self.macro
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index 4de846d12b..bfb37d6eee 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord::Associations::Builder # :nodoc:
class HasOne < SingularAssociation #:nodoc:
def self.macro
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index bb96202a22..0a02ef4cc1 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# This class is inherited by the has_one and belongs_to association classes
module ActiveRecord::Associations::Builder # :nodoc:
@@ -8,7 +10,16 @@ module ActiveRecord::Associations::Builder # :nodoc:
def self.define_accessors(model, reflection)
super
- define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable?
+ mixin = model.generated_association_methods
+ name = reflection.name
+
+ define_constructors(mixin, name) if reflection.constructable?
+
+ mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
+ def reload_#{name}
+ association(:#{name}).force_reload_reader
+ end
+ CODE
end
# Defines the (build|create)_association methods for belongs_to or has_one association
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 2dca6b612e..c3d4eab562 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,28 +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
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing an argument to force an association to reload is now
- deprecated and will be removed in Rails 5.1. Please call `reload`
- on the result collection proxy instead.
- MSG
-
- klass.uncached { reload }
- elsif stale_target?
+ def reader
+ if stale_target?
reload
end
- if null_scope?
- # Cache the proxy separately before the owner has an id
- # or else a post-save proxy will still lack the id
- @null_proxy ||= CollectionProxy.create(klass, self)
- else
- @proxy ||= CollectionProxy.create(klass, self)
- end
+ @proxy ||= CollectionProxy.create(klass, self)
+ @proxy.reset_scope
end
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
@@ -56,105 +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
- @association_ids ||= (
- 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) }
- records = klass.where(reflection.association_primary_key => ids).index_by do |r|
- r.send(reflection.association_primary_key)
- end.values_at(*ids)
- replace(records)
- end
- 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)
- end
-
- def fifth(*args)
- first_nth_or_last(:fifth, *args)
- end
-
- def forty_two(*args)
- first_nth_or_last(:forty_two, *args)
+ def reset
+ super
+ @target = []
+ @association_ids = nil
end
- def third_to_last(*args)
- first_nth_or_last(:third_to_last, *args)
- end
+ def find(*args)
+ if options[:inverse_of] && loaded?
+ args_flatten = args.flatten
+ model = scope.klass
- def second_to_last(*args)
- first_nth_or_last(:second_to_last, *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
@@ -162,23 +105,12 @@ 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.
+ # Add +records+ to this association. Since +<<+ flattens its argument list
+ # and inserts each record, +push+ and +concat+ behave identically.
def concat(*records)
records = records.flatten
if owner.new_record?
@@ -223,12 +155,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
@@ -246,28 +178,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.
#
@@ -276,12 +186,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
@@ -290,8 +195,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
@@ -307,14 +210,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
@@ -322,15 +223,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
@@ -340,42 +232,12 @@ 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 || reflection.has_cached_counter?
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?
- 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)
+ target.empty? && !scope.exists?
end
end
- alias uniq distinct
# Replace this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
@@ -423,29 +285,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
-
- was_loaded = loaded?
- yield(record) if block_given?
-
- unless !was_loaded && loaded?
- if index
- @target[index] = record
- else
- @target << record
- end
- 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
@@ -453,28 +295,13 @@ module ActiveRecord
owner.new_record? && !foreign_key_present?
end
- private
- def get_records
- return scope.to_a if skip_statement_cache?
-
- 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)
- }
- end
-
- binds = AssociationScope.get_bind_values(owner, reflection.chain)
- sc.execute binds, klass, klass.connection
+ def find_from_target?
+ loaded? ||
+ owner.new_record? ||
+ target.any? { |record| record.new_record? || record.changed? }
end
- def find_target
- records = get_records
- records.each { |record| set_inverse_instance(record) }
- records
- end
-
+ private
# We have some records loaded from the database (persisted) and some that are
# in-memory (memory). The same record may be represented in the persisted array
# and in the memory array.
@@ -492,7 +319,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
@@ -513,25 +340,32 @@ 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?
+ }
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?)
@@ -547,21 +381,23 @@ 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
+ @association_ids = nil
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
def replace_records(new_target, original_target)
- delete(target - new_target)
+ delete(difference(target, new_target))
- unless concat(new_target - target)
+ unless concat(difference(new_target, target))
@target = original_target
raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
"new records could not be saved."
@@ -571,24 +407,53 @@ module ActiveRecord
end
def replace_common_records_in_memory(new_target, original_target)
- common_records = new_target & original_target
+ common_records = intersection(new_target, original_target)
common_records.each do |record|
skip_callbacks = true
replace_on_target(record, @target.index(record), skip_callbacks)
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?
+ }
+ 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?
+ @association_ids = nil
+ target << record
+ end
+
+ callback(:after_add, record) unless skip_callbacks
+
+ record
+ ensure
+ @_was_loaded = nil
end
def callback(method, record)
@@ -602,25 +467,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)
@@ -647,16 +493,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 b9aed05135..edcb44f0fc 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -1,10 +1,9 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Associations
- # Association proxies in Active Record are middlemen between the object that
- # holds the association, known as the <tt>@owner</tt>, and the actual associated
- # object, known as the <tt>@target</tt>. The kind of association any proxy is
- # about is available in <tt>@reflection</tt>. That's an instance of the class
- # ActiveRecord::Reflection::AssociationReflection.
+ # Collection proxies in Active Record are middlemen between an
+ # <tt>association</tt>, and its <tt>target</tt> result set.
#
# For example, given
#
@@ -14,27 +13,26 @@ module ActiveRecord
#
# blog = Blog.first
#
- # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
- # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
- # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
+ # The collection proxy returned by <tt>blog.posts</tt> is built from a
+ # <tt>:has_many</tt> <tt>association</tt>, and delegates to a collection
+ # of posts as the <tt>target</tt>.
#
- # This class delegates unknown methods to <tt>@target</tt> via
- # <tt>method_missing</tt>.
+ # This class delegates unknown methods to the <tt>association</tt>'s
+ # relation class via a delegate cache.
#
- # The <tt>@target</tt> object is not \loaded until needed. For example,
+ # The <tt>target</tt> result set is not loaded until needed. For example,
#
# blog.posts.count
#
# 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 +52,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 +80,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,15 +105,6 @@ 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 ActiveRecord::Base.find. Returns ActiveRecord::RecordNotFound
@@ -137,10 +132,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,45 +169,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
+ ##
+ # :method: second
+ #
+ # :call-seq:
+ # second()
+ #
# Same as #first except returns only the second record.
- def second(*args)
- @association.second(*args)
- end
+ ##
+ # :method: third
+ #
+ # :call-seq:
+ # third()
+ #
# Same as #first except returns only the third record.
- def third(*args)
- @association.third(*args)
- end
+ ##
+ # :method: fourth
+ #
+ # :call-seq:
+ # fourth()
+ #
# Same as #first except returns only the fourth record.
- def fourth(*args)
- @association.fourth(*args)
- end
+ ##
+ # :method: fifth
+ #
+ # :call-seq:
+ # fifth()
+ #
# Same as #first except returns only the fifth record.
- def fifth(*args)
- @association.fifth(*args)
- end
+ ##
+ # :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.
- def third_to_last(*args)
- @association.third_to_last(*args)
- end
+ ##
+ # :method: second_to_last
+ #
+ # :call-seq:
+ # second_to_last()
+ #
# Same as #first except returns only the second-to-last record.
- def second_to_last(*args)
- @association.second_to_last(*args)
- end
# 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
@@ -233,8 +253,9 @@ 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
# Gives a record (or N records if a parameter is supplied) from the collection
@@ -262,8 +283,9 @@ module ActiveRecord
# another_person_without.pets # => []
# another_person_without.pets.take # => nil
# another_person_without.pets.take(2) # => []
- def take(n = nil)
- @association.take(n)
+ def take(limit = nil)
+ load_target if find_from_target?
+ super
end
# Returns a new object of the collection type that has been instantiated
@@ -341,34 +363,6 @@ module ActiveRecord
@association.create!(attributes, &block)
end
- # Add one or more records to the collection by setting their foreign keys
- # to the association's primary key. Since #<< flattens its argument list and
- # inserts each record, +push+ and #concat behave identically. Returns +self+
- # so method calls may be chained.
- #
- # class Person < ActiveRecord::Base
- # has_many :pets
- # end
- #
- # person.pets.size # => 0
- # person.pets.concat(Pet.new(name: 'Fancy-Fancy'))
- # person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo'))
- # person.pets.size # => 3
- #
- # person.id # => 1
- # person.pets
- # # => [
- # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
- # # #<Pet id: 2, name: "Spook", person_id: 1>,
- # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
- # # ]
- #
- # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')])
- # person.pets.size # => 5
- def concat(*records)
- @association.concat(*records)
- end
-
# Replaces this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
#
@@ -475,7 +469,7 @@ module ActiveRecord
# Pet.find(1, 2, 3)
# # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
def delete_all(dependent = nil)
- @association.delete_all(dependent)
+ @association.delete_all(dependent).tap { reset_scope }
end
# Deletes the records of the collection directly from the database
@@ -502,7 +496,7 @@ module ActiveRecord
#
# Pet.find(1) # => Couldn't find Pet with id=1
def destroy_all
- @association.destroy_all
+ @association.destroy_all.tap { reset_scope }
end
# Deletes the +records+ supplied from the collection according to the strategy
@@ -597,7 +591,7 @@ module ActiveRecord
# Pet.find(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
@@ -621,7 +615,7 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
def delete(*records)
- @association.delete(*records)
+ @association.delete(*records).tap { reset_scope }
end
# Destroys the +records+ supplied and removes them from the collection.
@@ -661,7 +655,7 @@ module ActiveRecord
#
# 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
@@ -693,9 +687,15 @@ module ActiveRecord
#
# Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6)
def destroy(*records)
- @association.destroy(*records)
+ @association.destroy(*records).tap { reset_scope }
end
+ ##
+ # :method: distinct
+ #
+ # :call-seq:
+ # distinct(value = true)
+ #
# Specifies whether the records should be unique or not.
#
# class Person < ActiveRecord::Base
@@ -710,17 +710,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
# # => [
@@ -728,9 +746,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>.
@@ -760,6 +780,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
@@ -780,14 +806,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>.
#
@@ -806,6 +829,12 @@ module ActiveRecord
@association.empty?
end
+ ##
+ # :method: any?
+ #
+ # :call-seq:
+ # any?()
+ #
# Returns +true+ if the collection is not empty.
#
# class Person < ActiveRecord::Base
@@ -835,10 +864,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>.
#
@@ -873,9 +905,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.
#
@@ -891,27 +920,14 @@ module ActiveRecord
!!@association.include?(record)
end
- def arel #:nodoc:
- 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
@@ -941,6 +957,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.
#
@@ -974,18 +996,15 @@ module ActiveRecord
# # #<Pet id: 5, name: "Brain", person_id: 1>,
# # #<Pet id: 6, name: "Boss", person_id: 1>
# # ]
- def to_ary
- load_target.dup
- end
- alias_method :to_a, :to_ary
def records # :nodoc:
load_target
end
# Adds one or more +records+ to the collection by setting their foreign keys
- # to the association's primary key. Returns +self+, so several appends may be
- # chained together.
+ # to the association's primary key. Since +<<+ flattens its argument list and
+ # inserts each record, +push+ and +concat+ behave identically. Returns +self+
+ # so several appends may be chained together.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -1008,6 +1027,7 @@ module ActiveRecord
end
alias_method :push, :<<
alias_method :append, :<<
+ alias_method :concat, :<<
def prepend(*args)
raise NoMethodError, "prepend on association is not defined. Please use <<, push or append"
@@ -1025,7 +1045,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
@@ -1039,12 +1058,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+.
@@ -1066,8 +1082,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 3ceec0ee46..59af6f54c3 100644
--- a/activerecord/lib/active_record/associations/foreign_association.rb
+++ b/activerecord/lib/active_record/associations/foreign_association.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord::Associations
module ForeignAssociation # :nodoc:
def foreign_key_present?
@@ -7,5 +9,12 @@ module ActiveRecord::Associations
false
end
end
+
+ def nullified_owner_attributes
+ Hash.new.tap do |attrs|
+ attrs[reflection.foreign_key] = nil
+ attrs[reflection.type] = nil if reflection.type.present?
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index a9f6aaafef..5972846940 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
@@ -16,45 +18,22 @@ module ActiveRecord
when :restrict_with_error
unless empty?
record = owner.class.human_attribute_name(reflection.name).downcase
- message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.many', record: record, raise: true) rescue nil
- if message
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- The error key `:'restrict_dependent_destroy.many'` has been deprecated and will be removed in Rails 5.1.
- Please use `:'restrict_dependent_destroy.has_many'` instead.
- MESSAGE
- end
- owner.errors.add(:base, message || :'restrict_dependent_destroy.has_many', record: record)
+ owner.errors.add(:base, :'restrict_dependent_destroy.has_many', record: record)
throw(:abort)
end
+ 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
- end
-
- def empty?
- if reflection.has_cached_counter?
- size.zero?
- else
- super
- end
+ super
end
private
@@ -74,15 +53,15 @@ module ActiveRecord
# the loaded flag is set to true as well.
def count_records
count = if reflection.has_cached_counter?
- owner._read_attribute reflection.counter_cache_column
+ 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
+ loaded! if count == 0
[association_scope.limit_value, count].compact.min
end
@@ -105,13 +84,14 @@ module ActiveRecord
if method == :delete_all
scope.delete_all
else
- scope.update_all(reflection.foreign_key => nil)
+ scope.update_all(nullified_owner_attributes)
end
end
def delete_or_nullify_all_records(method)
- count = delete_count(method, self.scope)
+ count = delete_count(method, scope)
update_counter(-count)
+ count
end
# Deletes the records according to the <tt>:dependent</tt> option.
@@ -143,6 +123,14 @@ module ActiveRecord
end
saved_successfully
end
+
+ def difference(a, b)
+ a - b
+ end
+
+ def intersection(a, b)
+ a & b
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index 36fc381343..0d384950fe 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)
+
+ if owner.new_record? && records
+ records.flatten.each do |record|
+ build_through_record(record)
+ end
+ end
- def through_association
- @through_association ||= owner.association(through_reflection.name)
+ records
end
# The through record (built with build_record) is temporarily cached
@@ -64,21 +57,14 @@ module ActiveRecord
@through_records[record.object_id] ||= begin
ensure_mutable
- 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
+ attributes = through_scope_attributes
+ attributes[source_reflection.name] = record
+ attributes[source_reflection.foreign_type] = options[:source_type] if options[:source_type]
- through_record
+ through_association.build(attributes)
end
end
- def options_for_through_record
- [through_scope_attributes]
- end
-
def through_scope_attributes
scope.where_values_hash(through_association.reflection.name.to_s).
except!(through_association.reflection.foreign_key,
@@ -86,7 +72,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
@@ -94,7 +83,7 @@ module ActiveRecord
def build_record(attributes)
ensure_not_nested
- record = super(attributes)
+ record = super
inverse = source_reflection.inverse_of
if inverse
@@ -108,6 +97,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
@@ -132,21 +126,15 @@ module ActiveRecord
scope = through_association.scope
scope.where! construct_join_attributes(*records)
+ scope = scope.where(through_scope_attributes)
case method
when :destroy
if scope.klass.primary_key
- count = scope.destroy_all.length
+ count = scope.destroy_all.count(&:destroyed?)
else
scope.each(&:_run_destroy_callbacks)
-
- arel = scope.arel
-
- stmt = Arel::DeleteManager.new
- stmt.from scope.klass.arel_table
- stmt.wheres = arel.constraints
-
- count = scope.klass.connection.delete(stmt, 'SQL', scope.bound_attributes)
+ count = scope.delete_all
end
when :nullify
count = scope.update_all(source_reflection.foreign_key => nil)
@@ -166,6 +154,30 @@ module ActiveRecord
else
update_counter(-count)
end
+
+ count
+ end
+
+ def difference(a, b)
+ distribution = distribution(b)
+
+ a.reject { |record| mark_occurrence(distribution, record) }
+ end
+
+ def intersection(a, b)
+ distribution = distribution(b)
+
+ a.select { |record| mark_occurrence(distribution, record) }
+ end
+
+ def mark_occurrence(distribution, record)
+ distribution[record] > 0 && distribution[record] -= 1
+ end
+
+ def distribution(array)
+ array.each_with_object(Hash.new(0)) do |record, distribution|
+ distribution[record] += 1
+ end
end
def through_records_for(record)
@@ -196,7 +208,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 0fe9b2e81b..99971286a3 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 Has One Association
module Associations
+ # = Active Record Has One Association
class HasOneAssociation < SingularAssociation #:nodoc:
include ForeignAssociation
@@ -12,14 +14,7 @@ module ActiveRecord
when :restrict_with_error
if load_target
record = owner.class.human_attribute_name(reflection.name).downcase
- message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.one', record: record, raise: true) rescue nil
- if message
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- The error key `:'restrict_dependent_destroy.one'` has been deprecated and will be removed in Rails 5.1.
- Please use `:'restrict_dependent_destroy.has_one'` instead.
- MESSAGE
- end
- owner.errors.add(:base, message || :'restrict_dependent_destroy.has_one', record: record)
+ owner.errors.add(:base, :'restrict_dependent_destroy.has_one', record: record)
throw(:abort)
end
@@ -28,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(nullified_owner_attributes) 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) if target.persisted?
- 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
@@ -82,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
@@ -108,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 b94feeff12..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) # :nodoc:
- 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,69 +64,32 @@ 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, join_type)
- joins = join_root.children.flat_map { |child|
+ def join_constraints(joins_to_add, join_type, alias_tracker)
+ @alias_tracker = alias_tracker
- if join_type == Arel::Nodes::OuterJoin
- make_left_outer_joins join_root, child
- else
- make_inner_joins join_root, child
- end
- }
+ 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 { |i, object_id|
@@ -142,7 +98,7 @@ module ActiveRecord
}
}
- 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
@@ -153,146 +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_key = primary_key ? row_hash[primary_key] : row_hash
- parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases)
- construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
+ 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)
+ def apply_column_aliases(relation)
+ relation._select!(-> { aliases.columns })
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
+ protected
+ attr_reader :join_root
- [info] + child.children.flat_map { |c| make_outer_joins(child, c) }
- end
+ private
+ attr_reader :alias_tracker
- def make_left_outer_joins(parent, child)
- tables = child.tables
- join_type = Arel::Nodes::OuterJoin
- info = make_constraints parent, child, tables, join_type
+ 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
- [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) }
- end
+ def construct_tables!(join_root)
+ join_root.each_children do |parent, child|
+ child.tables = table_aliases_for(parent, child)
+ end
+ end
- def make_inner_joins(parent, child)
- tables = child.tables
- join_type = Arel::Nodes::InnerJoin
- info = make_constraints parent, child, tables, join_type
+ def make_join_constraints(join_root, join_type)
+ join_root.children.flat_map do |child|
+ make_constraints(join_root, child, join_type)
+ end
+ end
- [info] + child.children.flat_map { |c| make_inner_joins(child, c) }
- end
+ 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
- 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 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 construct_tables!(parent, node)
- node.tables = table_aliases_for(parent, node)
- node.children.each { |child| construct_tables! node, child }
- end
+ def table_alias_for(reflection, parent, join)
+ name = reflection.alias_candidate(parent.table_name)
+ join ? "#{name}_join" : name
+ end
- def table_alias_for(reflection, parent, join)
- name = "#{reflection.plural_name}_#{parent.table_name}"
- name << "_join" if join
- name
- end
+ def walk(left, right)
+ intersection, missing = right.children.map { |node1|
+ [left.children.find { |node2| node1.match? node2 }, node1]
+ }.partition(&:first)
- def walk(left, right)
- intersection, missing = right.children.map { |node1|
- [left.children.find { |node2| node1.match? node2 }, node1]
- }.partition(&:first)
+ joins = intersection.flat_map { |l, r| r.table = l.table; walk(l, r) }
+ joins.concat missing.flat_map { |_, n| make_constraints(left, n) }
+ end
- ojs = missing.flat_map { |_,n| make_outer_joins left, n }
- intersection.flat_map { |l,r| walk l, r }.concat ojs
- end
+ 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
- def find_reflection(klass, name)
- klass._reflect_on_association(name) or
- raise ConfigurationError, "Can't join '#{ klass.name }' to association named '#{ name }'; perhaps you misspelled it?"
- 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 build(associations, base_klass)
- associations.map do |name, right|
- reflection = find_reflection base_klass, name
- reflection.check_validity!
- reflection.check_eager_loadable!
+ if reflection.polymorphic?
+ raise EagerLoadPolymorphicError.new(reflection)
+ end
- if reflection.polymorphic?
- raise EagerLoadPolymorphicError.new(reflection)
+ JoinAssociation.new(reflection, build(right, reflection.klass))
end
-
- JoinAssociation.new reflection, build(right, reflection.klass)
end
- end
-
- def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
- 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, rs, seen, model_cache, aliases)
- 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
- 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
+ def construct_model(record, node, row, model_cache, id)
+ other = record.association(node.reflection.name)
- model = seen[ar_parent.object_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[ar_parent.object_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 c5fbe0d1d1..ca0305abbb 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,20 @@
-require 'active_record/associations/join_dependency/join_part'
+# frozen_string_literal: true
+
+require "active_record/associations/join_dependency/join_part"
+require "active_support/core_ext/array/extract"
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,112 +22,48 @@ 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
+ join_scope = reflection.join_scope(table, foreign_table, foreign_klass)
- constraint = build_constraint(klass, table, key, foreign_table, foreign_key)
+ arel = join_scope.arel(alias_tracker.aliases)
+ nodes = arel.constraints.first
- predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table))
- scope_chain_items = scope_chain[scope_chain_index].map do |item|
- if item.is_a?(Relation)
- item
- else
- ActiveRecord::Relation.create(klass, table, predicate_builder)
- .instance_exec(node, &item)
- end
- end
- scope_chain_index += 1
-
- klass_scope =
- if klass.current_scope
- klass.current_scope.clone
- else
- relation = ActiveRecord::Relation.create(
- klass,
- table,
- predicate_builder,
- )
- klass.send(:build_default_scope, relation)
- end
- scope_chain_items.concat [klass_scope].compact
-
- rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right|
- left.merge right
+ others = nodes.children.extract! do |node|
+ Arel.fetch_attribute(node) { |attr| attr.relation.name != table.name }
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(nodes), join_type)
- if reflection.type
- value = foreign_klass.base_class.name
- column = klass.columns_hash[reflection.type.to_s]
-
- binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name))
- constraint = constraint.and klass.arel_attribute(reflection.type, table).eq(Arel::Nodes::BindParam.new)
+ unless others.empty?
+ joins.concat arel.join_sources
+ right = joins.last.right
+ right.expr.children.concat(others)
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 ecf6fb8643..c7cd87f9d4 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,20 +44,10 @@ 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
- NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, [])
-
# Eager loads the named associations for the given Active Record record(s).
#
# In this description, 'association name' shall refer to the name passed
@@ -91,14 +83,12 @@ module ActiveRecord
# { author: :avatar }
# [ :books, { author: :avatar } ]
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
@@ -106,107 +96,107 @@ module ActiveRecord
private
- # Loads all the given data into +records+ for the +association+.
- 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 recognized 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
-
- # Loads all the given data into +records+ for a singular +association+.
- #
- # Functions by instantiating a preloader class such as Preloader::HasManyThrough 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)
- 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)
- 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
+ # 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
- h
- end
- class AlreadyLoaded # :nodoc:
- attr_reader :owners, :reflection
+ def preloaders_for_reflection(reflection, records, scope)
+ records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs|
+ preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope).run
+ end
+ end
- def initialize(klass, owners, reflection, preload_scope)
- @owners = owners
- @reflection = reflection
+ 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
- def run(preloader); end
+ class AlreadyLoaded # :nodoc:
+ def initialize(klass, owners, reflection, preload_scope)
+ @owners = owners
+ @reflection = reflection
+ end
- def preloaded_records
- owners.flat_map { |owner| owner.association(reflection.name).target }
- end
- end
+ def run
+ self
+ 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 preloaded_records
+ @preloaded_records ||= records_by_owner.flat_map(&:last)
+ end
- # Returns a class containing the logic needed to load preload the data
- # and attach it to a relation. For example +Preloader::Association+ or
- # +Preloader::HasManyThrough+. The class returned implements a `run` method
- # that accepts a preloader.
- def preloader_for(reflection, owners, rhs_klass)
- return NullPreloader unless rhs_klass
+ def records_by_owner
+ @records_by_owner ||= owners.each_with_object({}) do |owner, result|
+ result[owner] = Array(owner.association(reflection.name).target)
+ end
+ end
- if owners.first.association(reflection.name).loaded?
- return AlreadyLoaded
+ private
+ attr_reader :owners, :reflection
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
+
+ # 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 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 3032bc786e..46532f651e 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -1,159 +1,136 @@
+# 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)
@klass = klass
@owners = owners
@reflection = reflection
@preload_scope = preload_scope
@model = owners.first && owners.first.class
- @scope = nil
- @preloaded_records = []
- end
-
- def run(preloader)
- preload(preloader)
- end
-
- def preload(preloader)
- raise NotImplementedError
- end
-
- def scope
- @scope ||= build_scope
- end
-
- def records_for(ids)
- query_scope(ids)
- end
-
- def query_scope(ids)
- scope.where(association_key_name => ids)
- end
-
- def table
- klass.arel_table
- end
-
- # The name of the key on the associated records
- def association_key_name
- raise NotImplementedError
- end
-
- # This is overridden by HABTM as the condition should be on the foreign_key column in
- # the join table
- def association_key
- klass.arel_attribute(association_key_name, table)
- end
-
- # The name of the key on the model which declares the association
- def owner_key_name
- raise NotImplementedError
- end
-
- def options
- reflection.options
end
- private
-
- def associated_records_by_owner(preloader)
- records = load_records
- owners.each_with_object({}) do |owner, result|
- result[owner] = records[convert_key(owner[owner_key_name])] || []
+ def run
+ if !preload_scope || preload_scope.empty_scope?
+ owners.each do |owner|
+ associate_records_to_owner(owner, records_by_owner[owner] || [])
+ end
+ else
+ # Custom preload scope is used and
+ # the association can not be marked as loaded
+ # Loading into a Hash instead
+ records_by_owner
end
+ self
end
- def owner_keys
- unless defined?(@owner_keys)
- @owner_keys = owners.map do |owner|
- owner[owner_key_name]
+ def records_by_owner
+ @records_by_owner ||= preloaded_records.each_with_object({}) do |record, result|
+ owners_by_key[convert_key(record[association_key_name])].each do |owner|
+ (result[owner] ||= []) << record
end
- @owner_keys.uniq!
- @owner_keys.compact!
end
- @owner_keys
end
- def key_conversion_required?
- @key_conversion_required ||= association_key_type != owner_key_type
- end
-
- def convert_key(key)
- if key_conversion_required?
- key.to_s
- else
- key
+ def preloaded_records
+ return @preloaded_records if defined?(@preloaded_records)
+ 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
+ slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
+ @preloaded_records = slices.flat_map do |slice|
+ records_for(slice)
end
end
- def association_key_type
- @klass.type_for_attribute(association_key_name.to_s).type
- end
+ private
+ attr_reader :owners, :reflection, :preload_scope, :model, :klass
- def owner_key_type
- @model.type_for_attribute(owner_key_name.to_s).type
- end
+ # The name of the key on the associated records
+ def association_key_name
+ reflection.join_primary_key(klass)
+ end
- def load_records
- 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
- slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
- @preloaded_records = slices.flat_map do |slice|
- records_for(slice)
+ # The name of the key on the model which declares the association
+ def owner_key_name
+ reflection.join_foreign_key
end
- @preloaded_records.group_by do |record|
- convert_key(record[association_key_name])
+
+ def associate_records_to_owner(owner, records)
+ association = owner.association(reflection.name)
+ if reflection.collection?
+ association.target = records
+ else
+ association.target = records.first
+ end
end
- end
- def reflection_scope
- @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
- end
+ def owner_keys
+ @owner_keys ||= owners_by_key.keys
+ end
- def build_scope
- scope = klass.unscoped
+ def owners_by_key
+ @owners_by_key ||= owners.each_with_object({}) do |owner, result|
+ key = convert_key(owner[owner_key_name])
+ (result[key] ||= []) << owner if key
+ end
+ end
- values = reflection_scope.values
- preload_values = preload_scope.values
+ def key_conversion_required?
+ unless defined?(@key_conversion_required)
+ @key_conversion_required = (association_key_type != owner_key_type)
+ end
- scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause
- scope.references_values = Array(values[:references]) + Array(preload_values[:references])
+ @key_conversion_required
+ end
- if preload_values[:select] || values[:select]
- scope._select!(preload_values[:select] || values[:select])
+ def convert_key(key)
+ if key_conversion_required?
+ key.to_s
+ else
+ key
+ end
end
- 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 association_key_type
+ @klass.type_for_attribute(association_key_name).type
end
- if order_values = preload_values[:order] || values[:order]
- scope.order!(order_values)
+ def owner_key_type
+ @model.type_for_attribute(owner_key_name).type
end
- if preload_values[:reordering] || values[:reordering]
- scope.reordering_value = true
+ def records_for(ids)
+ scope.where(association_key_name => ids).load do |record|
+ # Processing only the first owner
+ # because the record is modified but not an owner
+ owner = owners_by_key[convert_key(record[association_key_name])].first
+ association = owner.association(reflection.name)
+ association.set_inverse_instance(record)
+ end
end
- 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]) + Array(preload_values[:unscope])
- klass.default_scoped.merge(scope)
- end
+ def build_scope
+ scope = klass.scope_for_association
+
+ if reflection.type && !reflection.through_reflection?
+ 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 9939280fa4..0000000000
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module ActiveRecord
- module Associations
- class Preloader
- class CollectionAssociation < Association #:nodoc:
- private
-
- 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 c4add621ca..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_one.rb
+++ /dev/null
@@ -1,15 +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
- 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 b0203909ce..bec1c4c94a 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -1,108 +1,115 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Associations
class Preloader
- module ThroughAssociation #:nodoc:
- def through_reflection
- reflection.through_reflection
+ class ThroughAssociation < Association # :nodoc:
+ PRELOADER = ActiveRecord::Associations::Preloader.new
+
+ def initialize(*)
+ super
+ @already_loaded = owners.first.association(through_reflection.name).loaded?
end
- def source_reflection
- reflection.source_reflection
+ def preloaded_records
+ @preloaded_records ||= source_preloaders.flat_map(&:preloaded_records)
end
- def associated_records_by_owner(preloader)
- preloader.preload(owners,
- through_reflection.name,
- through_scope)
+ def records_by_owner
+ return @records_by_owner if defined?(@records_by_owner)
+ source_records_by_owner = source_preloaders.map(&:records_by_owner).reduce(:merge)
+ through_records_by_owner = through_preloaders.map(&:records_by_owner).reduce(:merge)
- through_records = owners.map do |owner|
- association = owner.association through_reflection.name
+ @records_by_owner = owners.each_with_object({}) do |owner, result|
+ through_records = through_records_by_owner[owner] || []
- center = target_records_from_association(association)
- [owner, Array(center)]
- end
-
- reset_association owners, through_reflection.name
+ 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
+ end
- middle_records = through_records.flat_map { |(_,rec)| rec }
+ records = through_records.flat_map do |record|
+ source_records_by_owner[record]
+ end
- preloaders = preloader.preload(middle_records,
- source_reflection.name,
- reflection_scope)
+ records.compact!
+ records.sort_by! { |rhs| preload_index[rhs] } if scope.order_values.any?
+ records.uniq! if scope.distinct_value
+ result[owner] = records
+ end
+ end
- @preloaded_records = preloaders.flat_map(&:preloaded_records)
+ private
+ def source_preloaders
+ @source_preloaders ||= PRELOADER.preload(middle_records, source_reflection.name, scope)
+ end
- middle_to_pl = preloaders.each_with_object({}) do |pl,h|
- pl.owners.each { |middle|
- h[middle] = pl
- }
+ def middle_records
+ through_preloaders.flat_map(&:preloaded_records)
end
- through_records.each_with_object({}) do |(lhs,center), records_by_owner|
- pl_to_middle = center.group_by { |record| middle_to_pl[record] }
+ def through_preloaders
+ @through_preloaders ||= PRELOADER.preload(owners, through_reflection.name, through_scope)
+ end
- records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
- rhs_records = middles.flat_map { |r|
- association = r.association source_reflection.name
+ def through_reflection
+ reflection.through_reflection
+ end
- target_records_from_association(association)
- }.compact
+ def source_reflection
+ reflection.source_reflection
+ end
- # Respect the order on `reflection_scope` if it exists, else use the natural order.
- if reflection_scope.values[:order].present?
- @id_map ||= id_to_index_map @preloaded_records
- rhs_records.sort_by { |rhs| @id_map[rhs] }
- else
- rhs_records
- end
+ def preload_index
+ @preload_index ||= preloaded_records.each_with_object({}).with_index do |(record, result), index|
+ result[record] = index
end
end
- end
- private
+ def through_scope
+ scope = through_reflection.klass.unscoped
+ options = reflection.options
- def id_to_index_map(ids)
- id_map = {}
- ids.each_with_index { |id, index| id_map[id] = index }
- id_map
- end
+ values = reflection_scope.values
+ if annotations = values[:annotate]
+ scope.annotate!(*annotations)
+ end
- def reset_association(owners, association_name)
- should_reset = (through_scope != through_reflection.klass.unscoped) ||
- (reflection.options[:source_type] && through_reflection.collection?)
+ 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
- # Don't cache the association - we would only be caching a subset
- if should_reset
- owners.each { |owner|
- owner.association(association_name).reset
- }
- end
- end
+ 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
- def through_scope
- scope = through_reflection.klass.unscoped
+ if left_outer_joins = values[:left_outer_joins]
+ scope.left_outer_joins!(source_reflection.name => left_outer_joins)
+ end
- 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])
- scope.where_clause = reflection_scope.where_clause
+ if scope.eager_loading? && order_values = values[:order]
+ scope = scope.order(order_values)
+ end
end
- scope.references! reflection_scope.values[:references]
- if scope.eager_loading? && order_values = reflection_scope.values[:order]
- scope = scope.order(order_values)
- end
+ scope
end
-
- scope
- end
-
- def target_records_from_association(association)
- association.loaded? ? association.target : association.reader
- 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 f913f0852a..a92932fa4b 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -1,17 +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
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing an argument to force an association to reload is now
- deprecated and will be removed in Rails 5.1. Please call `reload`
- on the parent object instead.
- MSG
-
- klass.uncached { reload }
- elsif !loaded? || stale_target?
+ def reader
+ if !loaded? || stale_target?
reload
end
@@ -23,46 +17,26 @@ 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
-
- def create_scope
- scope.scope_for_create.stringify_keys.except(klass.primary_key)
- end
-
- def get_records
- return scope.limit(1).records if skip_statement_cache?
-
- 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)
- }
- end
+ # Implements the reload reader method, e.g. foo.reload_bar for
+ # Foo.has_one :bar
+ def force_reload_reader
+ reload(true)
+ target
+ end
- binds = AssociationScope.get_bind_values(owner, reflection.chain)
- sc.execute binds, klass, klass.connection
+ private
+ def scope_for_create
+ super.except!(klass.primary_key)
end
def find_target
- if record = get_records.first
- set_inverse_instance record
- end
+ super.first
end
def replace(record)
@@ -73,9 +47,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 d0ec3e8015..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
- protected
+ refl
+ end
+ end
+
+ 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
@@ -102,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 3c4c8f10ec..0000000000
--- a/activerecord/lib/active_record/attribute.rb
+++ /dev/null
@@ -1,207 +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, 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, 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)
- self.class.new(name, value_before_type_cast, type, original_attribute)
- 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
-
- attr_reader :original_attribute
- alias_method :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
- if assigned?
- original_attribute.original_value_for_database
- else
- _original_value_for_database
- end
- end
-
- def _original_value_for_database
- value_for_database
- 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?
- 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 4580813364..0000000000
--- a/activerecord/lib/active_record/attribute/user_provided_default.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-require 'active_record/attribute'
-
-module ActiveRecord
- class Attribute # :nodoc:
- class UserProvidedDefault < FromUser # :nodoc:
- def initialize(name, value, type, database_default)
- @user_provided_value = value
- super(name, value, type, database_default)
- end
-
- def 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
-
- protected
-
- attr_reader :user_provided_value
- end
- end
-end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 4c22be8235..929045f29b 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -1,55 +1,50 @@
-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 ActiveModel::AttributeAssignment#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
+ 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
+ def execute_callstack_for_multiparameter_attributes(callstack)
+ errors = []
+ callstack.each do |name, values_with_empty_parameters|
if values_with_empty_parameters.each_value.all?(&:nil?)
values = nil
else
@@ -59,33 +54,32 @@ module ActiveRecord
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 e902eb7531..af7e46e649 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 'concurrent/map'
+# frozen_string_literal: true
+
+require "mutex_m"
module ActiveRecord
# = Active Record Attribute Methods
@@ -23,18 +22,11 @@ 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 GeneratedAttributeMethods < Module; end # :nodoc:
+ class GeneratedAttributeMethodsBuilder < Module #:nodoc:
+ include Mutex_m
+ end
module ClassMethods
def inherited(child_class) #:nodoc:
@@ -43,7 +35,8 @@ module ActiveRecord
end
def initialize_generated_modules # :nodoc:
- @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m }
+ @generated_attribute_methods = const_set(:GeneratedAttributeMethods, GeneratedAttributeMethodsBuilder.new)
+ private_constant :GeneratedAttributeMethods
@attribute_methods_generated = false
include @generated_attribute_methods
@@ -58,11 +51,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:
@@ -97,7 +89,7 @@ module ActiveRecord
# If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
# defines its own attribute method, then we don't want to overwrite that.
defined = method_defined_within?(method_name, superclass, Base) &&
- ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
+ ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethodsBuilder)
defined || super
end
end
@@ -123,7 +115,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:
@@ -148,7 +140,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
@@ -161,10 +153,61 @@ 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.
@@ -209,34 +252,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
- case name
- when :to_partial_path
- name = "to_partial_path".freeze
- when :to_model
- name = "to_model".freeze
- else
- name = name.to_s
- end
-
# 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+.
@@ -279,9 +315,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)
#
@@ -292,20 +327,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
@@ -334,8 +359,6 @@ module ActiveRecord
#
# Note: +:id+ is always present.
#
- # Alias for the #read_attribute method.
- #
# class Person < ActiveRecord::Base
# belongs_to :organization
# end
@@ -360,7 +383,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
@@ -398,67 +421,51 @@ module ActiveRecord
@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 1db6776688..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,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module AttributeMethods
# = Active Record Attribute Methods Before Type Cast
@@ -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 0bcfa5f00d..45e4b8adfa 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,9 +1,10 @@
-require 'active_support/core_ext/module/attribute_accessors'
-require 'active_record/attribute_mutation_tracker'
+# 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
@@ -13,139 +14,175 @@ 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
- @mutation_tracker = nil
- @previous_mutation_tracker = nil
- @changed_attributes = HashWithIndifferentAccess.new
- end
- end
-
- def initialize_dup(other) # :nodoc:
- super
- @attributes = self.class._default_attributes.map do |attr|
- attr.with_value_from_user(@attributes.fetch_value(attr.name))
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
+ @mutations_before_last_save = nil
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
+ @mutations_from_database = nil
end
- @mutation_tracker = nil
end
- def changes_applied
- @previous_mutation_tracker = mutation_tracker
- @changed_attributes = HashWithIndifferentAccess.new
- store_original_attributes
+ # 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
- def clear_changes_information
- @previous_mutation_tracker = nil
- @changed_attributes = HashWithIndifferentAccess.new
- store_original_attributes
- end
-
- def raw_write_attribute(attr_name, *)
- result = super
- clear_attribute_change(attr_name)
- result
- end
-
- def clear_attribute_changes(attr_names)
- super
- attr_names.each do |attr_name|
- clear_attribute_change(attr_name)
+ private
+ def write_attribute_without_type_cast(attr_name, value)
+ name = attr_name.to_s
+ if self.class.attribute_alias?(name)
+ name = self.class.attribute_alias(name)
+ end
+ result = super(name, value)
+ clear_attribute_change(name)
+ result
end
- 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(mutation_tracker.changed_values).freeze
+ def _update_record(attribute_names = attribute_names_for_partial_writes)
+ affected_rows = super
+ changes_applied
+ affected_rows
end
- end
- def changes
- cache_changed_attributes do
- super
+ def _create_record(attribute_names = attribute_names_for_partial_writes)
+ id = super
+ changes_applied
+ id
end
- end
-
- def previous_changes
- previous_mutation_tracker.changes
- end
- def attribute_changed_in_place?(attr_name)
- mutation_tracker.changed_in_place?(attr_name)
- end
-
- private
-
- def mutation_tracker
- unless defined?(@mutation_tracker)
- @mutation_tracker = nil
+ def attribute_names_for_partial_writes
+ partial_writes? ? changed_attribute_names_to_save : attribute_names
end
- @mutation_tracker ||= AttributeMutationTracker.new(@attributes)
- end
-
- def changes_include?(attr_name)
- super || mutation_tracker.changed?(attr_name)
- end
-
- def clear_attribute_change(attr_name)
- mutation_tracker.forget_change(attr_name)
- 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 store_original_attributes
- @attributes = @attributes.map(&:forgetting_assignment)
- @mutation_tracker = nil
- end
-
- def previous_mutation_tracker
- @previous_mutation_tracker ||= NullMutationTracker.instance
- end
-
- def cache_changed_attributes
- @cached_changed_attributes = changed_attributes
- yield
- ensure
- clear_changed_attributes_cache
- end
-
- def clear_changed_attributes_cache
- remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
- end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 0d5cb8b37c..6af5346fa7 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -1,4 +1,6 @@
-require 'set'
+# frozen_string_literal: true
+
+require "set"
module ActiveRecord
module AttributeMethods
@@ -8,121 +10,135 @@ module ActiveRecord
# 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.
+ # Returns the primary key column's 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.
+ # Sets the primary key column's 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.
+ # Queries the primary key column's value.
def id?
sync_with_transaction_state
query_attribute(self.class.primary_key)
end
- # Returns the primary key value before type cast.
+ # Returns the primary key column's value before type cast.
def id_before_type_cast
sync_with_transaction_state
read_attribute_before_type_cast(self.class.primary_key)
end
- # Returns the primary key previous value.
+ # Returns the primary key column's previous value.
def id_was
sync_with_transaction_state
attribute_was(self.class.primary_key)
end
- protected
-
- def attribute_method?(attr_name)
- attr_name == 'id' || super
+ # Returns the primary key column's value from the database.
+ 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 10498f4322..6811f54b10 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
@@ -14,8 +16,7 @@ module ActiveRecord
when true then true
when false, nil then false
else
- column = self.class.columns_hash[attr_name]
- if column.nil?
+ if !type_for_attribute(attr_name) { false }
if Numeric === value || value !~ /[^0-9]/
!value.to_i.zero?
else
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index ab2ecaa7c5..ffac5313ad 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -1,47 +1,28 @@
+# frozen_string_literal: true
+
module ActiveRecord
module AttributeMethods
module Read
extend ActiveSupport::Concern
- module ClassMethods
- protected
-
- # 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 define_method_attribute(name)
- safe_name = name.unpack('h*'.freeze).first
- temp_method = "__temp__#{safe_name}"
-
- ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
-
- 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) }
+ module ClassMethods # :nodoc:
+ private
+
+ 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
# Returns the value of the attribute identified by <tt>attr_name</tt> after
@@ -49,27 +30,24 @@ module ActiveRecord
# to a date object, like Date.new(2004, 12, 12)).
def read_attribute(attr_name, &block)
name = attr_name.to_s
- name = self.class.primary_key if name == 'id'.freeze
+ 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
# This method exists to avoid the expensive primary_key check internally, without
# breaking compatibility with the read_attribute API
- 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
- @attributes.fetch_value(attr_name.to_s, &block)
- end
- else
- def _read_attribute(attr_name) # :nodoc:
- @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? }
- end
+ def _read_attribute(attr_name, &block) # :nodoc
+ @attributes.fetch_value(attr_name.to_s, &block)
end
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 65978aea2a..6e0e90f39c 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -1,8 +1,20 @@
+# 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
@@ -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 e160460286..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,4 +1,4 @@
-require 'active_support/core_ext/string/strip'
+# frozen_string_literal: true
module ActiveRecord
module AttributeMethods
@@ -26,90 +26,65 @@ module ActiveRecord
private
- def convert_time_to_time_zone(value)
- return if value.nil?
+ 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) }
+ 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)
+ def map_avoiding_infinite_recursion(value)
+ map(value) do |v|
+ if value.equal?(v)
+ nil
+ else
+ yield(v)
+ end
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.strip_heredoc)
- Time columns will become time zone aware in Rails 5.1. This
- still causes `String`s to be parsed as if they were in `Time.zone`,
- and `Time`s to be converted to `Time.zone`.
- To keep the old behavior, you must add the following to your initializer:
+ 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)
- config.active_record.time_zone_aware_types = [:datetime]
-
- To silence this deprecation warning, add the following:
-
- config.active_record.time_zone_aware_types = [:datetime, :time]
- MESSAGE
+ 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 5599b590ca..455e67e19b 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module AttributeMethods
module Write
@@ -7,53 +9,59 @@ module ActiveRecord
attribute_method_suffix "="
end
- module ClassMethods
- protected
+ module ClassMethods # :nodoc:
+ private
- def define_method_attribute=(name)
- safe_name = name.unpack('h*'.freeze).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) # :nodoc:
- 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_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb
deleted file mode 100644
index 0133b4d0be..0000000000
--- a/activerecord/lib/active_record/attribute_mutation_tracker.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-module ActiveRecord
- class AttributeMutationTracker # :nodoc:
- def initialize(attributes)
- @attributes = attributes
- 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|
- if changed?(attr_name)
- result[attr_name] = [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
- end
- end
- end
-
- def changed?(attr_name)
- attr_name = attr_name.to_s
- attributes[attr_name].changed?
- end
-
- def changed_in_place?(attr_name)
- attributes[attr_name].changed_in_place?
- end
-
- def forget_change(attr_name)
- attr_name = attr_name.to_s
- attributes[attr_name] = attributes[attr_name].forgetting_assignment
- end
-
- protected
-
- attr_reader :attributes
-
- private
-
- def attr_names
- attributes.keys
- end
- end
-
- class NullMutationTracker # :nodoc:
- include Singleton
-
- def changed_values
- {}
- end
-
- def changes
- {}
- end
-
- def changed?(*)
- false
- end
-
- def changed_in_place?(*)
- false
- end
-
- def forget_change(*)
- 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 be581ac2a9..0000000000
--- a/activerecord/lib/active_record/attribute_set.rb
+++ /dev/null
@@ -1,108 +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 deep_dup
- dup.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/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
deleted file mode 100644
index 3bd7c7997b..0000000000
--- a/activerecord/lib/active_record/attribute_set/builder.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-require 'active_record/attribute'
-
-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 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
-
- protected
-
- attr_reader :types, :values, :additional_types, :delegate_hash
-
- 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
-
- 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
- end
-end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index e0ceafc617..7cf421c184 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -1,4 +1,6 @@
-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
@@ -6,8 +8,7 @@ module ActiveRecord
extend ActiveSupport::Concern
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
@@ -34,12 +35,15 @@ 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).
#
+ # When using a symbol for +cast_type+, extra options are forwarded to the
+ # constructor of the type object.
+ #
# ==== Examples
#
# The type detected by Active Record can be overridden.
@@ -56,7 +60,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
@@ -67,12 +71,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
@@ -89,6 +95,7 @@ module ActiveRecord
#
# \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
@@ -108,12 +115,22 @@ module ActiveRecord
# my_float_range: 1.0..3.5
# }
#
+ # Passing options to the type constructor
+ #
+ # # app/models/my_model.rb
+ # class MyModel < ActiveRecord::Base
+ # attribute :small_int, :integer, limit: 2
+ # end
+ #
+ # MyModel.create(small_int: 65537)
+ # # => Error: 65537 is out of range for the limit of two bytes
+ #
# ==== Creating Custom Types
#
# 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
#
@@ -131,7 +148,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
@@ -140,7 +157,7 @@ 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.
#
@@ -167,8 +184,10 @@ 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: currency_converter
@@ -185,8 +204,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
@@ -237,24 +256,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 06c7482bf9..fe94662543 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
# = Active Record Autosave Association
#
@@ -140,24 +142,23 @@ module ActiveRecord
included do
Associations::Builder::Association.extensions << AssociationBuilderExtension
- mattr_accessor :index_nested_attribute_errors, instance_writer: false
- self.index_nested_attribute_errors = false
+ mattr_accessor :index_nested_attribute_errors, instance_writer: false, default: false
end
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
@@ -181,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
@@ -215,13 +217,7 @@ 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
@@ -267,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
@@ -325,30 +321,24 @@ 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, index=nil)
+ def association_valid?(reflection, record, index = nil)
return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?)
- validation_context = self.validation_context unless [:create, :update].include?(self.validation_context)
- unless valid = record.valid?(validation_context)
+ context = validation_context unless [:create, :update].include?(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|
- if indexed_attribute
- attribute = "#{reflection.name}[#{index}].#{attribute}"
- else
- attribute = "#{reflection.name}.#{attribute}"
- end
+ attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
errors[attribute] << message
errors[attribute].uniq!
end
record.errors.details.each_key do |attribute|
- if indexed_attribute
- reflection_attribute = "#{reflection.name}[#{index}].#{attribute}"
- else
- reflection_attribute = "#{reflection.name}.#{attribute}"
- end
+ reflection_attribute =
+ normalize_reflection_attribute(indexed_attribute, reflection, index, attribute).to_sym
record.errors.details[attribute].each do |error|
errors.details[reflection_attribute] << error
@@ -362,11 +352,22 @@ 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
@@ -381,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?)
@@ -396,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
@@ -435,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
@@ -448,8 +457,14 @@ module ActiveRecord
# If the record is new or it has changed, returns true.
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)
+ association_foreign_key_changed?(reflection, record, key) ||
+ record.will_save_change_to_attribute?(reflection.foreign_key)
+ end
+
+ def association_foreign_key_changed?(reflection, record, key)
+ return false if reflection.through_reflection?
+
+ record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key
end
# Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
@@ -457,7 +472,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]
@@ -465,7 +482,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)
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 6a1a27ce41..db097cb930 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1,25 +1,28 @@
-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/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 '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
@@ -286,6 +289,7 @@ module ActiveRecord #:nodoc:
extend Enum
extend Delegation::DelegateCache
extend CollectionCacheKey
+ extend Aggregations::ClassMethods
include Core
include Persistence
@@ -303,6 +307,7 @@ module ActiveRecord #:nodoc:
include AttributeDecorators
include Locking::Optimistic
include Locking::Pessimistic
+ include DefineCallbacks
include AttributeMethods
include Callbacks
include Timestamp
@@ -310,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 1f1b11eb68..ef5444dfc3 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
# = Active Record \Callbacks
#
@@ -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.delete_by(parent_id: id)
# 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:
#
@@ -225,6 +213,55 @@ module ActiveRecord
#
# This way, the +before_destroy+ gets executed before the <tt>dependent: :destroy</tt> is called, and the data is still available.
#
+ # Also, there are cases when you want several callbacks of the same type to
+ # be executed in order.
+ #
+ # For example:
+ #
+ # class Topic < 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.
+ #
+ # In case there are multiple transactional callbacks as seen below, the order
+ # is reversed.
+ #
+ # For example:
+ #
+ # class Topic < ActiveRecord::Base
+ # has_many :children
+ #
+ # 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!],
@@ -265,17 +302,6 @@ module ActiveRecord
:before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback
]
- 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
-
def destroy #:nodoc:
@_destroy_callback_already_called ||= false
return if @_destroy_callback_already_called
@@ -292,17 +318,21 @@ module ActiveRecord
_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:
+ def create_or_update(**)
_run_save_callbacks { super }
end
- def _create_record #:nodoc:
+ def _create_record
_run_create_callbacks { super }
end
- def _update_record(*) #:nodoc:
+ def _update_record
_run_update_callbacks { super }
end
end
diff --git a/activerecord/lib/active_record/coders/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 2456b8ad8c..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,37 +16,35 @@ module ActiveRecord
def dump(obj)
return if obj.nil?
- assert_valid_value(obj)
+ 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)
- assert_valid_value(obj)
+ assert_valid_value(obj, action: "load")
obj ||= object_class.new if object_class != Object
obj
end
- def assert_valid_value(obj)
+ def assert_valid_value(obj, action:)
unless obj.nil? || obj.is_a?(object_class)
raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
+ "can't #{action} `#{@attr_name}`: was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
end
end
private
- def check_arity_of_constructor
- begin
+ 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
index 5dcc98424a..4b6db8a96c 100644
--- a/activerecord/lib/active_record/collection_cache_key.rb
+++ b/activerecord/lib/active_record/collection_cache_key.rb
@@ -1,24 +1,37 @@
+# frozen_string_literal: true
+
module ActiveRecord
module CollectionCacheKey
-
def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
- query_signature = Digest::MD5.hexdigest(collection.to_sql)
+ query_signature = ActiveSupport::Digest.hexdigest(collection.to_sql)
key = "#{collection.model_name.cache_key}/query-#{query_signature}"
- if collection.loaded?
- size = collection.size
+ if collection.loaded? || collection.distinct_value
+ size = collection.records.size
if size > 0
- timestamp = collection.max_by(&timestamp_column).public_send(timestamp_column)
+ timestamp = collection.max_by(&timestamp_column)._read_attribute(timestamp_column)
end
else
- column_type = type_for_attribute(timestamp_column.to_s)
- column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(timestamp_column)}"
+ 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
- query = collection
- .unscope(:select)
- .select("COUNT(*) AS size", "MAX(#{column}) AS timestamp")
- .unscope(:order)
- result = connection.select_one(query)
+ result = connection.select_one(arel, nil)
if result.blank?
size = 0
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 e389d818fd..0ded1a5318 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 'concurrent/map'
-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
@@ -61,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
@@ -116,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)
@@ -135,7 +132,7 @@ 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
@@ -150,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 = Concurrent.monotonic_time
+ 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 connection from the pool within %0.3f seconds (waited %0.3f seconds); all pooled connections were in use' %
- [timeout, elapsed]
- raise ConnectionTimeoutError, msg
+ elapsed = Concurrent.monotonic_time - t0
+ if elapsed >= timeout
+ msg = "could not obtain a connection from the pool within %0.3f seconds (waited %0.3f seconds); all pooled connections were in use" %
+ [timeout, elapsed]
+ 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
@@ -268,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
@@ -296,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
@@ -323,23 +324,25 @@ 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.
+ # 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
+ # 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 = Concurrent::Map.new(:initial_capacity => @size)
+ @thread_cached_conns = Concurrent::Map.new(initial_capacity: @size)
@connections = []
@automatic_reconnect = true
@@ -349,10 +352,25 @@ module ActiveRecord
# 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
@@ -361,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 obtained through
- # #connection or #with_connection methods, connections 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)]
@@ -415,7 +433,10 @@ module ActiveRecord
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 = []
@@ -426,14 +447,29 @@ 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
- # <tt>spec.config[:checkout_timeout] * 2</tt> seconds), the pool is forcefully
+ # <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.
#
@@ -442,41 +478,27 @@ module ActiveRecord
# connections in the pool within a timeout interval (default duration is
# <tt>spec.config[:checkout_timeout] * 2</tt> seconds).
def clear_reloadable_connections(raise_on_acquisition_timeout = true)
- num_new_conns_required = 0
-
with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do
synchronize do
@connections.each do |conn|
- 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
- # <tt>spec.config[:checkout_timeout] * 2</tt> 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!
@@ -507,14 +529,16 @@ module ActiveRecord
# +conn+: an AbstractAdapter object, which was obtained by earlier by
# 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_checkin_callbacks do
- conn.expire
- end
+ conn._run_checkin_callbacks do
+ conn.expire
+ end
- @available.add conn
+ @available.add conn
+ end
end
end
@@ -530,20 +554,20 @@ 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
@@ -556,230 +580,295 @@ module ActiveRecord
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
- # <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
+ 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 = Concurrent.monotonic_time + (@checkout_timeout * 2)
+
+ @available.with_a_bias_for(Thread.current) do
+ loop do
+ synchronize do
+ return if collected_conns.size == @connections.size && @now_connecting == 0
+ remaining_timeout = timeout_time - Concurrent.monotonic_time
+ 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:
- # - 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 +@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
- # <tt>synchronize { conn.lease }</tt> 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_checkout_callbacks 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:
#
@@ -790,7 +879,7 @@ module ActiveRecord
# end
#
# class Book < ActiveRecord::Base
- # establish_connection "library_db"
+ # establish_connection :library_db
# end
#
# class ScaryBook < Book
@@ -821,28 +910,67 @@ 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 = Concurrent::Map.new(:initial_capacity => 2) do |h,k|
- h[k] = Concurrent::Map.new(:initial_capacity => 2)
+ def self.create_owner_to_pool # :nodoc:
+ Concurrent::Map.new(initial_capacity: 2) do |h, k|
+ # Discard the parent's connection pools immediately; we have no need
+ # of them
+ discard_unowned_pools(h)
+
+ h[k] = Concurrent::Map.new(initial_capacity: 2)
+ end
+ end
+
+ def self.unowned_pool_finalizer(pid_map) # :nodoc:
+ lambda do |_|
+ discard_unowned_pools(pid_map)
end
- @class_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k|
- h[k] = Concurrent::Map.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 = ConnectionHandler.create_owner_to_pool
+
+ # 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
@@ -869,87 +997,81 @@ 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)
+
+ unless pool
+ # multiple database application
+ if ActiveRecord::Base.connection_handler != ActiveRecord::Base.default_connection_handler
+ raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found for the '#{ActiveRecord::Base.current_role}' role."
+ else
+ raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found."
+ end
+ end
+
+ pool.connection
end
# 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
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
+ private
+
+ def owner_to_pool
+ @owner_to_pool[Process.pid]
+ 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 6711049588..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,11 +14,13 @@ 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
@@ -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 824040775d..6aacbe5f88 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,34 +9,61 @@ 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
+
+ if prepared_statements
+ sql, binds = visitor.compile(arel_or_sql_string.ast, collector)
+
+ if binds.length > bind_params_length
+ unprepared_statement do
+ sql, binds = to_sql_and_binds(arel_or_sql_string)
+ visitor.preparable = false
+ end
+ end
+ else
+ sql = visitor.compile(arel_or_sql_string.ast, collector)
+ end
+ [sql.freeze, binds]
else
- arel
+ visitor.preparable = false if prepared_statements
+ [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 = [], preparable: nil)
- arel, binds = binds_from_relation arel, binds
- sql = to_sql(arel, binds)
- if !prepared_statements || (arel.is_a?(String) && preparable.nil?)
- preparable = false
- else
- preparable = visitor.preparable
+ arel = arel_from_relation(arel)
+ sql, binds = to_sql_and_binds(arel, binds)
+
+ if preparable.nil?
+ preparable = prepared_statements ? visitor.preparable : false
end
+
if prepared_statements && preparable
select_prepared(sql, name, binds)
else
@@ -50,24 +79,37 @@ 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, binds = [])
- arel, binds = binds_from_relation arel, binds
- select_rows(to_sql(arel, binds), name, binds).map(&:first)
+ select_rows(arel, name, binds).map(&:first)
end
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
- def select_rows(sql, name = nil, binds = [])
+ 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
+
+ def query(sql, name = nil) # :nodoc:
+ exec_query(sql, name).rows
+ end
+
+ # Determines whether the SQL statement is a write query.
+ def write_query?(sql)
+ raise NotImplementedError
end
- undef_method :select_rows
# Executes the SQL statement in the context of this connection and returns
# the raw result from the connection adapter.
@@ -75,76 +117,80 @@ module ActiveRecord
# 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 = [], prepare: false)
+ 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, 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
- # Executes the truncate statement.
- def truncate(table_name, name = nil)
- raise NotImplementedError
- end
-
# 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
# 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, pk, sequence_name = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds)
+ 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
- alias insert_sql insert
- deprecate insert_sql: :insert
# Executes the update statement and returns the number of rows affected.
def update(arel, name = nil, binds = [])
- exec_update(to_sql(arel, binds), name, binds)
+ sql, binds = to_sql_and_binds(arel, binds)
+ exec_update(sql, name, binds)
end
- alias update_sql update
- deprecate update_sql: :update
# Executes the delete statement and returns the number of rows affected.
def delete(arel, name = nil, binds = [])
- exec_delete(to_sql(arel, binds), name, binds)
+ sql, binds = to_sql_and_binds(arel, binds)
+ exec_delete(sql, name, binds)
+ end
+
+ # Executes the truncate statement.
+ def truncate(table_name, name = nil)
+ execute(build_truncate_statements(table_name), name)
end
- alias delete_sql delete
- deprecate delete_sql: :delete
- # Returns +true+ when the connection adapter supports prepared statement
- # caching, otherwise returns +false+
- def supports_statement_cache?
- false
+ def truncate_tables(*table_names) # :nodoc:
+ return if table_names.empty?
+
+ with_multi_statements do
+ disable_referential_integrity do
+ Array(build_truncate_statements(*table_names)).each do |sql|
+ execute_batch(sql, "Truncate Tables")
+ end
+ end
+ end
end
# Runs the given block in a database transaction, and returns the result
@@ -158,7 +204,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.7/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.
#
@@ -210,7 +256,7 @@ 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://www.postgresql.org/docs/current/static/transaction-iso.html
# * https://dev.mysql.com/doc/refman/5.7/en/set-transaction.html
#
# An ActiveRecord::TransactionIsolationError will be raised if:
@@ -220,9 +266,7 @@ module ActiveRecord
# * You are creating a nested (savepoint) transaction
#
# The 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.
+ # isolation level.
def transaction(requires_new: nil, isolation: nil, joinable: true)
if !requires_new && current_transaction.joinable?
if isolation
@@ -238,14 +282,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
@@ -292,9 +338,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
@@ -306,70 +349,134 @@ 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_set` 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)
- fixture = fixture.stringify_keys
+ execute(build_fixture_sql(Array.wrap(fixture), table_name), "Fixture Insert")
+ end
- columns = schema_cache.columns_hash(table_name)
- binds = fixture.map do |name, 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
- 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))
+ def insert_fixtures_set(fixture_set, tables_to_delete = [])
+ fixture_inserts = build_fixture_statements(fixture_set)
+ table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name(table)}" }
+ total_sql = Array(combine_multi_statements(table_deletes + fixture_inserts))
+
+ with_multi_statements do
+ disable_referential_integrity do
+ transaction(requires_new: true) do
+ total_sql.each do |sql|
+ execute_batch(sql, "Fixtures Load")
+ end
+ end
end
end
-
- execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", 'Fixture Insert'
end
- def empty_insert_statement_value
+ 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, key) # :nodoc:
- subselect = subquery_for(key, select)
-
- update.where key.in(subselect)
+ # 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) # :nodoc:
+ if value.is_a?(Hash) || value.is_a?(Array)
+ YAML.dump(value)
+ else
+ value
+ end
end
- alias join_to_delete join_to_update
- protected
+ private
+ def execute_batch(sql, name = nil)
+ execute(sql, name)
+ end
+
+ DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze
+ private_constant :DEFAULT_INSERT_VALUE
+
+ def default_insert_value(column)
+ DEFAULT_INSERT_VALUE
+ end
+
+ def build_fixture_sql(fixtures, table_name)
+ columns = schema_cache.columns_hash(table_name)
+
+ values_list = 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
- # Returns a subquery for the given key using the join information.
- def subquery_for(key, select)
- subselect = select.clone
- subselect.projections = [key]
- subselect
+ table = Arel::Table.new(table_name)
+ manager = Arel::InsertManager.new
+ manager.into(table)
+
+ if values_list.size == 1
+ values = values_list.shift
+ new_values = []
+ columns.each_key.with_index { |column, i|
+ unless values[i].equal?(DEFAULT_INSERT_VALUE)
+ new_values << values[i]
+ manager.columns << table[column]
+ end
+ }
+ values_list << new_values
+ else
+ columns.each_key { |column| manager.columns << table[column] }
+ end
+
+ manager.values = manager.create_values_list(values_list)
+ manager.to_sql
+ end
+
+ def build_fixture_statements(fixture_set)
+ fixture_set.map do |table_name, fixtures|
+ next if fixtures.empty?
+ build_fixture_sql(fixtures, table_name)
+ end.compact
+ end
+
+ def build_truncate_statements(*table_names)
+ truncate_tables = table_names.map do |table_name|
+ "TRUNCATE TABLE #{quote_table_name(table_name)}"
+ end
+ combine_multi_statements(truncate_tables)
+ end
+
+ def with_multi_statements
+ yield
+ end
+
+ def combine_multi_statements(total_sql)
+ total_sql.join(";\n")
end
# Returns an ActiveRecord::Result instance.
@@ -381,20 +488,25 @@ module ActiveRecord
exec_query(sql, name, binds, prepare: true)
end
- def sql_for_insert(sql, pk, id_value, sequence_name, binds)
- [sql, binds, pk, sequence_name]
+ def sql_for_insert(sql, pk, sequence_name, binds)
+ [sql, binds]
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
- [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 0bdfd4f900..93b1c4e632 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -1,16 +1,23 @@
+# 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)
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
def #{method_name}(*)
- clear_query_cache if @query_cache_enabled
+ ActiveRecord::Base.clear_query_caches_for_current_thread if @query_cache_enabled
super
end
end_code
@@ -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,21 @@ module ActiveRecord
# the same SQL query and repeatedly return the same result each time, silently
# undermining the randomness you were expecting.
def clear_query_cache
- @query_cache.clear
+ @lock.synchronize do
+ @query_cache.clear
+ end
end
def select_all(arel, name = nil, binds = [], preparable: nil)
if @query_cache_enabled && !locked?(arel)
- arel, binds = binds_from_relation arel, binds
- sql = to_sql(arel, binds)
- cache_sql(sql, binds) { super(sql, name, binds, preparable: preparable) }
+ arel = arel_from_relation(arel)
+ sql, binds = to_sql_and_binds(arel, binds)
+
+ if preparable.nil?
+ preparable = prepared_statements ? visitor.preparable : false
+ end
+
+ cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable) }
else
super
end
@@ -73,23 +109,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 2eeefb13d7..2877530917 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)
@@ -63,21 +57,10 @@ module ActiveRecord
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.
@@ -112,19 +95,19 @@ module ActiveRecord
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
@@ -146,47 +129,70 @@ 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
+
+ def quoted_binary(value) # :nodoc:
+ "'#{quote_string(value.to_s)}'"
+ end
+
+ def sanitize_as_sql_comment(value) # :nodoc:
+ value.to_s.gsub(%r{ (/ (?: | \g<1>) \*) \+? \s* | \s* (\* (?: | \g<2>) /) }x, "")
end
private
+ def type_casted_binds(binds)
+ 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 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 lookup_cast_type(sql_type)
+ type_map.lookup(sql_type)
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
+ 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 _quote(value)
+ case value
+ when String, Symbol, 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 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 nil, Numeric, String then value
+ when Type::Time::Value then quoted_time(value)
+ when Date, Time then quoted_date(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 0ba4d94e3c..7d20825a75 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
@@ -15,32 +15,33 @@ module ActiveRecord
end
delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
- :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options, to: :@conn
- private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
- :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options
+ :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :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 #{quote_table_name(o.name)} "
+ 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)} "
statements = o.columns.map { |c| accept c }
statements << accept(o.primary_keys) if o.primary_keys
@@ -53,18 +54,18 @@ module ActiveRecord
statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
end
- create_sql << "(#{statements.join(', ')}) " if statements.present?
- create_sql << "#{o.options}"
- create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
+ 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_PrimaryKeyDefinition(o)
- "PRIMARY KEY (#{o.name.join(', ')})"
+ "PRIMARY KEY (#{o.name.map { |name| quote_column_name(name) }.join(', ')})"
end
def visit_ForeignKeyDefinition(o)
- sql = <<-SQL.strip_heredoc
+ sql = +<<~SQL
CONSTRAINT #{quote_column_name(o.name)}
FOREIGN KEY (#{quote_column_name(o.column)})
REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)})
@@ -82,17 +83,22 @@ module ActiveRecord
"DROP CONSTRAINT #{quote_column_name(name)}"
end
+ def table_options(o)
+ table_options = {}
+ table_options[:comment] = o.comment
+ table_options[:options] = o.options
+ table_options
+ end
+
+ def add_table_options!(create_sql, options)
+ if options_sql = options[:options]
+ create_sql << " #{options_sql}"
+ end
+ 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)
@@ -110,7 +116,20 @@ module ActiveRecord
sql
end
+ 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)
+ prefix = ActiveRecord::Base.table_name_prefix
+ suffix = ActiveRecord::Base.table_name_suffix
+ to_table = "#{prefix}#{to_table}#{suffix}"
options = foreign_key_options(from_table, to_table, options)
accept ForeignKeyDefinition.new(from_table, to_table, options)
end
@@ -121,7 +140,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
@@ -129,5 +148,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 4f97c7c065..4861872129 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -1,32 +1,77 @@
+# frozen_string_literal: true
+
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 PrimaryKeyDefinition < Struct.new(:name) # :nodoc:
- end
+ AddColumnDefinition = Struct.new(:column) # :nodoc:
- class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #: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
@@ -51,18 +96,24 @@ 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 }
- else
- to_table == options_or_to_table.to_s
- end
+ def validate?
+ options.fetch(:validate, true)
end
+ alias validated? validate?
- private
- def default_primary_key
- "id"
+ def export_name_on_schema_dump?
+ !ActiveRecord::SchemaDumper.fk_ignore_pattern.match?(name) if name
+ end
+
+ def defined_for?(to_table: nil, **options)
+ (to_table.nil? || to_table.to_s == self.to_table) &&
+ options.all? { |k, v| self.options[k].to_s == v.to_s }
end
+
+ private
+ def default_primary_key
+ "id"
+ end
end
class ReferenceDefinition # :nodoc:
@@ -71,7 +122,7 @@ module ActiveRecord
polymorphic: false,
index: true,
foreign_key: false,
- type: :integer,
+ type: :bigint,
**options
)
@name = name
@@ -100,89 +151,87 @@ 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).merge(column: column_name)
- end
+ def foreign_key_options
+ as_options(foreign_key).merge(column: column_name)
+ end
- def columns
- result = [[column_name, 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_name
- "#{name}_id"
- end
+ def column_name
+ "#{name}_id"
+ end
- def column_names
- columns.map(&:first)
- 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
+ def foreign_table_name
+ foreign_key_options.fetch(:to_table) do
+ Base.pluralize_table_names ? name.to_s.pluralize : name
+ end
end
- end
end
module ColumnMethods
+ extend ActiveSupport::Concern
+
# Appends a primary key definition to the table definition.
# Can be called multiple times, but this is probably not a good idea.
def primary_key(name, type = :primary_key, **options)
column(name, type, options.merge(primary_key: true))
end
+ ##
+ # :method: column
+ # :call-seq: column(name, type, **options)
+ #
# Appends a column or columns of a specified type.
#
# t.string(:goat)
# t.string(:goat, :sheep)
#
# See TableDefinition#column
- [
- :bigint,
- :binary,
- :boolean,
- :date,
- :datetime,
- :decimal,
- :float,
- :integer,
- :string,
- :text,
- :time,
- :timestamp,
- ].each do |column_type|
- module_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{column_type}(*args, **options)
- args.each { |name| column(name, :#{column_type}, options) }
+
+ included do
+ define_column_methods :bigint, :binary, :boolean, :date, :datetime, :decimal,
+ :float, :integer, :json, :string, :text, :time, :timestamp, :virtual
+
+ alias :numeric :decimal
+ end
+
+ class_methods do
+ private def define_column_methods(*column_types) # :nodoc:
+ column_types.each do |column_type|
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{column_type}(*names, **options)
+ raise ArgumentError, "Missing column name(s) for #{column_type}" if names.empty?
+ names.each { |name| column(name, :#{column_type}, options) }
+ end
+ RUBY
end
- CODE
+ end
end
- alias_method :numeric, :decimal
end
# Represents the schema of an SQL table in an abstract way. This class
@@ -206,18 +255,29 @@ module ActiveRecord
class TableDefinition
include ColumnMethods
- attr_accessor :indexes
- attr_reader :name, :temporary, :options, :as, :foreign_keys
+ attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys
- def initialize(name, temporary, options, as = nil)
+ def initialize(
+ conn,
+ name,
+ temporary: false,
+ if_not_exists: false,
+ options: nil,
+ as: nil,
+ comment: nil,
+ **
+ )
+ @conn = conn
@columns_hash = {}
- @indexes = {}
+ @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:
@@ -297,16 +357,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 = {})
+ 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)
@@ -326,22 +390,24 @@ 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.push([table_name, options])
+ foreign_keys << [table_name, options]
end
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
# <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?
+ if !options.key?(:precision) && @conn.supports_datetime_with_precision?
+ options[:precision] = 6
+ end
+
column(:created_at, :datetime, options)
column(:updated_at, :datetime, options)
end
@@ -353,37 +419,38 @@ module ActiveRecord
#
# See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use.
def references(*args, **options)
- args.each do |col|
- ReferenceDefinition.new(col, **options).add_to(self)
+ args.each do |ref_name|
+ ReferenceDefinition.new(ref_name, options).add_to(self)
end
end
alias :belongs_to :references
- def new_column_definition(name, type, options) # :nodoc:
+ def new_column_definition(name, type, **options) # :nodoc:
+ if integer_like_primary_key?(type, options)
+ type = integer_like_primary_key_type(type, options)
+ end
type = aliased_types(type.to_s, type)
- column = create_column_definition name, type
-
- column.limit = options[:limit]
- column.precision = options[:precision]
- column.scale = options[:scale]
- column.default = options[:default]
- column.null = options[:null]
- column.first = options[:first]
- column.after = options[:after]
- column.auto_increment = options[:auto_increment]
- column.primary_key = type == :primary_key || options[:primary_key]
- column.collation = options[:collation]
- column
+ 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 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:
@@ -444,7 +511,11 @@ module ActiveRecord
# t.date
# t.binary
# t.boolean
+ # t.foreign_key
+ # t.json
+ # t.virtual
# t.remove
+ # t.remove_foreign_key
# t.remove_references
# t.remove_belongs_to
# t.remove_index
@@ -466,13 +537,15 @@ module ActiveRecord
# t.column(:name, :string)
#
# See TableDefinition#column for details of the options you can use.
- def column(column_name, type, options = {})
+ 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 {connection.column_exists?}[rdoc-ref:SchemaStatements#column_exists?]
def column_exists?(column_name, type = nil, options = {})
@@ -493,9 +566,9 @@ module ActiveRecord
# 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 {connection.index_exists?}[rdoc-ref:SchemaStatements#index_exists?]
def index_exists?(column_name, options = {})
@@ -586,8 +659,7 @@ module ActiveRecord
# t.belongs_to(:supplier, foreign_key: true)
#
# See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use.
- def references(*args)
- options = args.extract_options!
+ def references(*args, **options)
args.each do |ref_name|
@base.add_reference(name, ref_name, options)
end
@@ -600,29 +672,39 @@ module ActiveRecord
# t.remove_belongs_to(:supplier, polymorphic: true)
#
# See {connection.remove_reference}[rdoc-ref:SchemaStatements#remove_reference]
- def remove_references(*args)
- options = args.extract_options!
+ def remove_references(*args, **options)
args.each do |ref_name|
@base.remove_reference(name, ref_name, options)
end
end
alias :remove_belongs_to :remove_references
- # Adds a foreign key.
+ # Adds a foreign key to the table using a supplied table name.
#
- # t.foreign_key(:authors)
+ # t.foreign_key(:authors)
+ # t.foreign_key(:authors, column: :author_id, primary_key: "id")
#
# See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key]
- def foreign_key(*args) # :nodoc:
+ def foreign_key(*args)
@base.add_foreign_key(name, *args)
end
+ # Removes the given foreign key from the table.
+ #
+ # t.remove_foreign_key(:authors)
+ # t.remove_foreign_key(column: :author_id)
+ #
+ # See {connection.remove_foreign_key}[rdoc-ref:SchemaStatements#remove_foreign_key]
+ def remove_foreign_key(*args)
+ @base.remove_foreign_key(name, *args)
+ end
+
# Checks to see if a foreign key exists.
#
- # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors)
+ # t.foreign_key(:authors) unless t.foreign_key_exists?(:authors)
#
# See {connection.foreign_key_exists?}[rdoc-ref:SchemaStatements#foreign_key_exists?]
- def foreign_key_exists?(*args) # :nodoc:
+ def foreign_key_exists?(*args)
@base.foreign_key_exists?(name, *args)
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 4880d216d6..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,103 +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 = Hash[prepare_column_options(column).map { |k, v| [k, "#{k}: #{v}"] }]
- spec[:name] = column.name.inspect
- spec[:type] = schema_type(column).to_s
- spec
- end
-
- def column_spec_for_primary_key(column)
- return {} if default_primary_key?(column)
- spec = { id: schema_type(column).inspect }
- spec.merge!(prepare_column_options(column))
+ class SchemaDumper < SchemaDumper # :nodoc:
+ def self.create(connection, options)
+ new(connection, options)
end
- # This can be overridden on an Adapter level basis to support other
- # extended datatypes (Example: Adding an array option in the
- # PostgreSQL::ColumnDumper)
- def prepare_column_options(column)
- spec = {}
-
- 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?
-
- spec[:null] = 'false' unless column.null
-
- if collation = schema_collation(column)
- spec[:collation] = collation
+ def default_primary_key?(column)
+ schema_type(column) == :bigint
end
- spec
- end
-
- # Lists the valid migration options
- def migration_keys
- [:name, :limit, :precision, :scale, :default, :null, :collation]
- end
-
- private
+ def explicit_primary_key_default?(column)
+ false
+ end
- def default_primary_key?(column)
- schema_type(column) == :integer
- end
+ def schema_type_with_virtual(column)
+ if @connection.supports_virtual_columns? && column.virtual?
+ :virtual
+ else
+ schema_type(column)
+ end
+ end
- def schema_type(column)
- if column.bigint?
- :bigint
- else
- column.type
+ def schema_type(column)
+ if column.bigint?
+ :bigint
+ else
+ column.type
+ end
end
- end
- def schema_limit(column)
- limit = column.limit unless column.bigint?
- limit.inspect if limit && limit != native_database_types[column.type][:limit]
- 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_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)
- type = lookup_cast_type_from_column(column)
- default = type.deserialize(column.default)
- if default.nil?
- schema_expression(column)
- else
- type.type_cast_for_schema(default)
+ 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
- end
- def schema_expression(column)
- "-> { #{column.default_function.inspect} }" if column.default_function
- end
+ def schema_expression(column)
+ "-> { #{column.default_function.inspect} }" if column.default_function
+ 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 020d9bbdca..6981ea6ecd 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,9 @@
-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 "active_support/deprecation"
+require "digest/sha2"
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -18,14 +21,21 @@ 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
@@ -34,12 +44,14 @@ module ActiveRecord
# 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(name = nil)
- raise NotImplementedError, "#tables is not implemented"
+ def tables
+ query_values(data_source_sql(type: "BASE TABLE"), "SCHEMA")
end
# Checks to see if the table +table_name+ exists on the database.
@@ -47,12 +59,14 @@ 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
- raise NotImplementedError, "#views is not implemented"
+ query_values(data_source_sql(type: "VIEW"), "SCHEMA")
end
# Checks to see if the view +view_name+ exists on the database.
@@ -60,11 +74,15 @@ module ActiveRecord
# 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.
#
@@ -90,10 +108,12 @@ module ActiveRecord
indexes(table_name).any? { |i| checks.all? { |check| check[i] } }
end
- # Returns an array of Column objects for the table specified by +table_name+.
- # See the concrete implementation for details on the expected parameter values.
+ # Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name)
- raise NotImplementedError, "#columns is not implemented"
+ table_name = table_name.to_s
+ column_definitions(table_name).map do |field|
+ new_column_from_field(table_name, field)
+ end
end
# Checks to see if a column exists in a given table.
@@ -110,12 +130,12 @@ module ActiveRecord
# column_exists?(:suppliers, :name, :string, null: false)
# column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2)
#
- def column_exists?(table_name, column_name, type = nil, options = {})
+ def column_exists?(table_name, column_name, type = nil, **options)
column_name = column_name.to_s
checks = []
checks << lambda { |c| c.name == column_name }
- checks << lambda { |c| c.type == type } if type
- (migration_keys - [:name]).each do |attr|
+ checks << lambda { |c| c.type == type.to_sym rescue nil } if type
+ column_options_keys.each do |attr|
checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr)
end
@@ -124,14 +144,9 @@ module ActiveRecord
# Returns just a table's primary key
def primary_key(table_name)
- pks = primary_keys(table_name)
- warn <<-WARNING.strip_heredoc if pks.count > 1
- WARNING: Rails does not support composite primary key.
-
- #{table_name} has composite primary key. Composite primary key is ignored.
- WARNING
-
- pks.first if pks.one?
+ 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
@@ -174,7 +189,9 @@ module ActiveRecord
# 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
@@ -189,19 +206,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 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
#
@@ -212,7 +232,7 @@ module ActiveRecord
# generates:
#
# CREATE TABLE objects (
- # guid int auto_increment PRIMARY KEY,
+ # guid bigint auto_increment PRIMARY KEY,
# name varchar(80)
# )
#
@@ -229,18 +249,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
@@ -254,8 +291,8 @@ module ActiveRecord
# SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id
#
# See also TableDefinition#column for details on how to create columns.
- def create_table(table_name, options = {})
- td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
+ def create_table(table_name, **options)
+ td = create_table_definition(table_name, options)
if options[:id] != false && !options[:as]
pk = options.fetch(:primary_key) do
@@ -271,18 +308,28 @@ module ActiveRecord
yield td if block_given?
- if options[:force] && data_source_exists?(table_name)
- drop_table(table_name, options)
+ if options[:force]
+ drop_table(table_name, options.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
+ 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
end
@@ -292,9 +339,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>]
@@ -320,21 +367,20 @@ 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
@@ -366,6 +412,8 @@ module ActiveRecord
#
# Defaults to false.
#
+ # Only supported on the MySQL and PostgreSQL adapter, ignored elsewhere.
+ #
# ====== Add a column
#
# change_table(:suppliers) do |t|
@@ -390,7 +438,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
#
@@ -398,7 +446,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
#
@@ -419,7 +467,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)
@@ -469,19 +517,24 @@ module ActiveRecord
#
# Available options are (none of these exists by default):
# * <tt>:limit</tt> -
- # Requests a maximum column length. This is number of characters for a <tt>:string</tt> column
+ # 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.
+ # 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>.
+ # 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
+ # 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
@@ -502,7 +555,7 @@ module ActiveRecord
# 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].
+ # * SqlServer: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
# Default (38,0).
#
# == Examples
@@ -524,10 +577,14 @@ module ActiveRecord
# 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 = {})
+ def add_column(table_name, column_name, type, **options)
at = create_alter_table table_name
at.add_column(column_name, type, options)
execute schema_creation.accept at
@@ -550,9 +607,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.
@@ -667,7 +725,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
#
@@ -690,6 +748,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)
@@ -736,7 +807,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)
@@ -753,30 +824,27 @@ 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.
# #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 constraint. Defaults to false.
# [<tt>:polymorphic</tt>]
@@ -784,17 +852,25 @@ module ActiveRecord
# [<tt>:null</tt>]
# Whether the column allows nulls. Defaults to true.
#
- # ====== Create a user_id integer column
+ # ====== Create a user_id bigint column without an 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
#
@@ -804,8 +880,8 @@ module ActiveRecord
#
# add_reference(:products, :supplier, foreign_key: {to_table: :firms})
#
- def add_reference(table_name, *args)
- ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self))
+ def add_reference(table_name, ref_name, **options)
+ ReferenceDefinition.new(ref_name, options).add_to(update_table_definition(table_name, self))
end
alias :add_belongs_to :add_reference
@@ -814,7 +890,7 @@ module ActiveRecord
#
# ====== Remove the reference
#
- # remove_reference(:products, :user, index: true)
+ # remove_reference(:products, :user, index: false)
#
# ====== Remove polymorphic reference
#
@@ -822,16 +898,22 @@ 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
@@ -883,6 +965,8 @@ module ActiveRecord
# Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
# [<tt>:on_update</tt>]
# Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
+ # [<tt>:validate</tt>]
+ # (Postgres only) Specify whether or not the constraint should be validated. Defaults to +true+.
def add_foreign_key(from_table, to_table, options = {})
return unless supports_foreign_keys?
@@ -906,15 +990,22 @@ 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.
- def remove_foreign_key(from_table, options_or_to_table = {})
+ # 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, to_table = nil, **options)
return unless supports_foreign_keys?
- fk_name_to_delete = foreign_key_for!(from_table, options_or_to_table).name
+ fk_name_to_delete = foreign_key_for!(from_table, to_table: to_table, **options).name
at = create_alter_table from_table
at.drop_foreign_key fk_name_to_delete
@@ -924,33 +1015,21 @@ 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}"
+ def foreign_key_exists?(from_table, to_table = nil, **options)
+ foreign_key_for(from_table, to_table: to_table, **options).present?
end
def foreign_key_column_for(table_name) # :nodoc:
- prefix = Base.table_name_prefix
- suffix = Base.table_name_suffix
- name = table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s
+ name = strip_table_name_prefix_and_suffix(table_name)
"#{name.singularize}_id"
end
@@ -962,53 +1041,43 @@ module ActiveRecord
end
def dump_schema_information #:nodoc:
- sm_table = ActiveRecord::Migrator.schema_migrations_table_name
-
- sql = "INSERT INTO #{sm_table} (version) VALUES "
- sql << ActiveRecord::SchemaMigration.order('version').pluck(:version).map {|v| "('#{v}')" }.join(', ')
- sql << ";\n\n"
- 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
- end
-
- def initialize_internal_metadata_table
- ActiveRecord::InternalMetadata.create_table
+ versions = ActiveRecord::SchemaMigration.all_versions
+ insert_versions_sql(versions) if versions.any?
end
def internal_string_options_for_primary_key # :nodoc:
{ primary_key: true }
end
- def assume_migrated_upto_version(version, migrations_paths)
- migrations_paths = Array(migrations_paths)
+ def assume_migrated_upto_version(version, migrations_paths = nil)
+ unless migrations_paths.nil?
+ ActiveSupport::Deprecation.warn(<<~MSG.squish)
+ Passing migrations_paths to #assume_migrated_upto_version is deprecated and will be removed in Rails 6.1.
+ MSG
+ end
+
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
- end
+ migrated = migration_context.get_all_versions
+ versions = migration_context.migrations.map(&:version)
unless migrated.include?(version)
- execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')"
+ execute "INSERT INTO #{sm_table} (version) VALUES (#{quote(version)})"
end
- inserting = (versions - migrated).select {|v| v < version}
+ inserting = (versions - migrated).select { |v| v < version }
if inserting.any?
- if (duplicate = inserting.detect {|v| inserting.count(v) > 1})
+ if (duplicate = inserting.detect { |v| inserting.count(v) > 1 })
raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict."
end
- execute "INSERT INTO #{sm_table} (version) VALUES #{inserting.map {|v| "('#{v}')"}.join(', ') }"
+ 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
@@ -1024,7 +1093,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
@@ -1041,7 +1110,7 @@ module ActiveRecord
end
# Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT.
- # PostgreSQL, MySQL, 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"])
@@ -1058,6 +1127,10 @@ module ActiveRecord
def add_timestamps(table_name, options = {})
options[:null] = false if options[:null].nil?
+ if !options.key?(:precision) && supports_datetime_with_precision?
+ options[:precision] = 6
+ end
+
add_column table_name, :created_at, :datetime, options
add_column table_name, :updated_at, :datetime, options
end
@@ -1075,16 +1148,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]) {
@@ -1098,66 +1170,88 @@ 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 data_source_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
def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
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
+ # 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
+
+ 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
- # 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 options_for_index_columns(options)
+ if options.is_a?(Hash)
+ options.symbolize_keys
+ else
+ Hash.new { |hash, column| hash[column] = options }
+ end
+ end
- # add index sort order if supported
+ # 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 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 = {})
- # if the adapter doesn't support the indexes call the best we can do
- # is return the default index name for the options provided
- return index_name(table_name, options) unless respond_to?(:indexes)
+ return options[:name] if can_remove_index_by_name?(options)
checks = []
if options.is_a?(Hash)
- checks << lambda { |i| i.name == options[:name].to_s } if options.has_key?(:name)
- column_names = Array(options[:column]).map(&:to_s)
+ checks << lambda { |i| i.name == options[:name].to_s } if options.key?(:name)
+ column_names = index_column_names(options[:column])
else
- column_names = Array(options).map(&:to_s)
+ column_names = index_column_names(options)
end
- if column_names.any?
- checks << lambda { |i| i.columns.join('_and_') == column_names.join('_and_') }
+ if column_names.present?
+ checks << lambda { |i| index_name(table_name, i.columns) == index_name(table_name, column_names) }
end
- raise ArgumentError "No name or columns specified" if checks.none?
+ raise ArgumentError, "No name or columns specified" if checks.none?
matching_indexes = indexes(table_name).select { |i| checks.all? { |check| check[i] } }
@@ -1193,36 +1287,132 @@ module ActiveRecord
end
end
- private
- def create_table_definition(name, temporary = false, options = nil, as = nil)
- TableDefinition.new(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(self, *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 strip_table_name_prefix_and_suffix(table_name)
+ prefix = Base.table_name_prefix
+ suffix = Base.table_name_suffix
+ table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s
+ end
+
+ def foreign_key_name(table_name, options)
+ options.fetch(:name) do
+ identifier = "#{table_name}_#{options.fetch(:column)}_fk"
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
+
+ "fk_rails_#{hashed_identifier}"
+ end
+ end
+
+ def foreign_key_for(from_table, **options)
+ return unless supports_foreign_keys?
+ foreign_keys(from_table).detect { |fk| fk.defined_for?(options) }
+ end
+
+ def foreign_key_for!(from_table, to_table: nil, **options)
+ foreign_key_for(from_table, to_table: to_table, **options) ||
+ raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table || options}")
+ 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 ca795cb1ad..c9e84e48cc 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,22 +17,49 @@ 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?
committed? || rolledback?
end
- def set_state(state)
- unless VALID_STATES.include?(state)
- raise ArgumentError, "Invalid transaction state: #{state}"
- end
- @state = state
+ 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
@@ -41,14 +73,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
@@ -57,8 +89,12 @@ module ActiveRecord
records << record
end
- def rollback
- @state.set_state(:rolledback)
+ def materialize!
+ @materialized = true
+ end
+
+ def materialized?
+ @materialized
end
def rollback_records
@@ -72,10 +108,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
@@ -87,7 +119,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
@@ -101,47 +133,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
@@ -149,60 +189,104 @@ 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)
+ @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
+
+ if @connection.supports_lazy_transactions? && lazy_transactions_enabled? && options[:_lazy] != false
+ @has_unmaterialized_transactions = true
else
- SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options,
- run_commit_callbacks: run_commit_callbacks)
+ transaction.materialize!
end
+ @stack.push(transaction)
+ transaction
+ end
+ end
- @stack.push(transaction)
- transaction
+ def disable_lazy_transactions!
+ materialize_transactions
+ @lazy_transactions_enabled = false
end
- def commit_transaction
- transaction = @stack.last
+ def enable_lazy_transactions!
+ @lazy_transactions_enabled = true
+ end
- begin
- transaction.before_commit_records
- ensure
- @stack.pop
+ 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
+ @connection.lock.synchronize do
+ transaction = @stack.last
+
+ begin
+ transaction.before_commit_records
+ ensure
+ @stack.pop
+ end
- transaction.commit
- transaction.commit_records
+ 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
- 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
- rescue Exception
- rollback_transaction(transaction) unless transaction.state.completed?
- raise
+ @connection.lock.synchronize do
+ transaction = begin_transaction options
+ yield
+ rescue Exception => error
+ if transaction
+ rollback_transaction
+ after_failure_actions(transaction, error)
+ end
+ raise
+ ensure
+ if !error && transaction
+ if Thread.current.status == "aborting"
+ rollback_transaction
+ else
+ begin
+ commit_transaction
+ rescue Exception
+ rollback_transaction(transaction) unless transaction.state.completed?
+ raise
+ end
end
end
end
@@ -226,7 +310,6 @@ module ActiveRecord
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 069346253a..7aad306d50 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,11 +1,16 @@
-require 'active_record/type'
-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 '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 "active_support/deprecation"
+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 +19,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
@@ -25,11 +30,11 @@ module ActiveRecord
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
end
- autoload_under 'abstract' do
+ autoload_under "abstract" do
autoload :SchemaStatements
autoload :DatabaseStatements
autoload :DatabaseLimits
@@ -39,7 +44,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
@@ -61,23 +66,27 @@ module ActiveRecord
# Most of the methods in the adapter are useful during migrations. Most
# 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_accessor :pool
+ attr_reader :schema_cache, :visitor, :owner, :logger, :lock, :prepared_statements, :prevent_writes
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,7 +101,10 @@ module ActiveRecord
end
end
- attr_reader :prepared_statements
+ def self.build_read_query_regexp(*parts) # :nodoc:
+ parts = parts.map { |part| /\A[\(\s]*#{part}/i }
+ Regexp.union(*parts)
+ end
def initialize(connection, logger = nil, config = {}) # :nodoc:
super()
@@ -103,61 +115,88 @@ module ActiveRecord
@logger = logger
@config = config
@pool = nil
+ @idle_since = Concurrent.monotonic_time
@schema_cache = SchemaCache.new self
- @visitor = nil
- @prepared_statements = false
@quoted_column_names, @quoted_table_names = {}, {}
+ @prevent_writes = false
+ @visitor = arel_visitor
+ @statements = build_statement_pool
+ @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
+
+ 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 Version
- include Comparable
+ def replica?
+ @config[:replica] || false
+ end
- def initialize(version_string)
- @version = version_string.split('.').map(&:to_i)
- end
+ # Determines whether writes are currently being prevents.
+ #
+ # Returns true if the connection is a replica, or if +prevent_writes+
+ # is set to true.
+ def preventing_writes?
+ replica? || prevent_writes
+ end
- def <=>(version_string)
- @version <=> version_string.split('.').map(&:to_i)
- end
+ # Prevent writing to the database regardless of role.
+ #
+ # In some cases you may want to prevent writes to the database
+ # even if you are on a database that can write. `while_preventing_writes`
+ # will prevent writes to the database for the duration of the block.
+ def while_preventing_writes
+ original, @prevent_writes = @prevent_writes, true
+ yield
+ ensure
+ @prevent_writes = original
end
- 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) })
- end
+ def migrations_paths # :nodoc:
+ @config[:migrations_paths] || Migrator.migrations_paths
end
- class SQLString < Arel::Collectors::SQLString
- def compile(bvs, conn)
- super(bvs)
- end
+ def migration_context # :nodoc:
+ MigrationContext.new(migrations_paths)
end
- def collector
- if prepared_statements
- SQLString.new
- else
- BindCollector.new
+ class Version
+ include Comparable
+
+ def initialize(version_string)
+ @version = version_string.split(".").map(&:to_i)
end
- end
- def valid_type?(type)
- true
+ def <=>(version_string)
+ @version <=> version_string.split(".").map(&:to_i)
+ end
+
+ def to_s
+ @version.join(".")
+ end
end
- def schema_creation
- SchemaCreation.new self
+ 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
@@ -173,7 +212,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
@@ -189,17 +258,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?
@@ -237,6 +295,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
@@ -263,11 +326,28 @@ 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
+ deprecate :supports_foreign_keys_in_create?
+
# Does this adapter support views?
def supports_views?
false
end
+ # Does this adapter support materialized views?
+ def supports_materialized_views?
+ false
+ end
+
# Does this adapter support datetime with precision?
def supports_datetime_with_precision?
false
@@ -278,6 +358,57 @@ module ActiveRecord
false
end
+ # Does this adapter support metadata comments on database objects (tables, columns, indexes)?
+ def supports_comments?
+ false
+ end
+
+ # Can comments for tables, columns, and indexes be specified in create/alter table statements?
+ def supports_comments_in_create?
+ false
+ end
+
+ # 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
+
+ # Does this adapter support optimizer hints?
+ def supports_optimizer_hints?
+ false
+ end
+
+ def supports_lazy_transactions?
+ false
+ end
+
+ def supports_insert_returning?
+ false
+ end
+
+ def supports_insert_on_duplicate_skip?
+ false
+ end
+
+ def supports_insert_on_duplicate_update?
+ false
+ end
+
+ def supports_insert_conflict_target?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -286,6 +417,10 @@ 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
#
@@ -340,6 +475,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.
@@ -350,11 +498,9 @@ module ActiveRecord
# this should be overridden by concrete adapters
end
- ###
- # Clear any caching the database adapter may be doing, for example
- # clearing the prepared statement cache. This is database specific.
+ # Clear any caching the database adapter may be doing.
def clear_cache!
- # this should be overridden by concrete adapters
+ @lock.synchronize { @statements.clear } if @statements
end
# Returns true if its required to reload the connection between requests for development mode.
@@ -365,39 +511,36 @@ module ActiveRecord
# Checks whether the connection to the database is still active (i.e. not stale).
# This is done under the hood by calling #active?. If the connection
# is no longer active, then this method will reconnect to the database.
- def verify!(*ignored)
+ def verify!
reconnect! unless active?
end
# Provides access to the underlying database driver for this adapter. For
# example, this method returns a Mysql2::Client object in case of Mysql2Adapter,
- # and a PGconn object in case of PostgreSQLAdapter.
+ # and a PG::Connection object in case of PostgreSQLAdapter.
#
# This is useful for when you need to call a proprietary method such as
# PostgreSQL's lo_* methods.
def raw_connection
+ disable_lazy_transactions!
@connection
end
- def create_savepoint(name = nil)
+ def default_uniqueness_comparison(attribute, value, klass) # :nodoc:
+ attribute.eq(value)
end
- def release_savepoint(name = nil)
+ def case_sensitive_comparison(attribute, value) # :nodoc:
+ attribute.eq(value)
end
- def case_sensitive_comparison(table, attribute, column, value)
- if value.nil?
- table[attribute].eq(value)
- else
- table[attribute].eq(Arel::Nodes::BindParam.new)
- end
- end
+ def case_insensitive_comparison(attribute, value) # :nodoc:
+ column = column_for_attribute(attribute)
- def case_insensitive_comparison(table, attribute, column, value)
if can_perform_case_insensitive_comparison_for?(column)
- table[attribute].lower.eq(table.lower(Arel::Nodes::BindParam.new))
+ attribute.lower.eq(attribute.relation.lower(value))
else
- table[attribute].eq(Arel::Nodes::BindParam.new)
+ attribute.eq(value)
end
end
@@ -406,143 +549,182 @@ 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, null, table_name, default_function = nil, collation = nil) # :nodoc:
- Column.new(name, default, sql_type_metadata, null, table_name, 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)
+ # Called by ActiveRecord::InsertAll,
+ # Passed an instance of ActiveRecord::InsertAll::Builder,
+ # This method implements standard bulk inserts for all databases, but
+ # should be overridden by adapters to implement common features with
+ # non-standard syntax like handling duplicates or returning values.
+ def build_insert_sql(insert) # :nodoc:
+ if insert.skip_duplicates? || insert.update_duplicates?
+ raise NotImplementedError, "#{self.class} should define `build_insert_sql` to implement adapter-specific logic for handling duplicates during INSERT"
+ end
+
+ "INSERT #{insert.into} #{insert.values_list}"
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)
+ private
+ def check_version
+ end
+
+ 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
+
+ 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
- end
- def extract_scale(sql_type) # :nodoc:
- case sql_type
+ 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, binds)
+ message = "#{e.class.name}: #{e.message}"
+
+ exception = translate_exception(
+ e, message: message, sql: sql, binds: binds
+ )
+ 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,
+ connection: self) do
+ @lock.synchronize do
+ yield
+ end
+ rescue => e
+ raise translate_exception_class(e, sql, binds)
+ 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:, sql:, binds:)
+ # override in derived class
+ case exception
+ when RuntimeError
+ exception
+ else
+ ActiveRecord::StatementInvalid.new(message, sql: sql, binds: binds)
+ end
+ end
- def translate_exception(exception, message)
- # override in derived class
- ActiveRecord::StatementInvalid.new(message)
- 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 column_for_attribute(attribute)
+ table_name = attribute.relation.name
+ schema_cache.columns_hash(table_name)[attribute.name.to_s]
+ 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
+
+ def build_statement_pool
+ 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 8015d1ed9e..8ca2cfa9ed 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,28 +1,21 @@
-require 'active_record/connection_adapters/abstract_adapter'
-require 'active_record/connection_adapters/mysql/column'
-require 'active_record/connection_adapters/mysql/explain_pretty_printer'
-require 'active_record/connection_adapters/mysql/quoting'
-require 'active_record/connection_adapters/mysql/schema_creation'
-require 'active_record/connection_adapters/mysql/schema_definitions'
-require 'active_record/connection_adapters/mysql/schema_dumper'
-require 'active_record/connection_adapters/mysql/type_metadata'
-
-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 MySQL::Quoting
- include MySQL::ColumnDumper
- include Savepoints
-
- def update_table_definition(table_name, base) # :nodoc:
- MySQL::Table.new(table_name, base)
- end
-
- def schema_creation
- MySQL::SchemaCreation.new(self)
- end
+ include MySQL::SchemaStatements
##
# :singleton-method:
@@ -31,75 +24,55 @@ module ActiveRecord
# to your application.rb file:
#
# ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false
- class_attribute :emulate_booleans
- self.emulate_booleans = true
-
- QUOTED_TRUE, QUOTED_FALSE = '1', '0'
+ class_attribute :emulate_booleans, default: true
NATIVE_DATABASE_TYPES = {
- primary_key: "int auto_increment PRIMARY KEY",
+ primary_key: "bigint auto_increment PRIMARY KEY",
string: { name: "varchar", limit: 255 },
text: { name: "text" },
integer: { name: "int", limit: 4 },
- float: { name: "float" },
+ float: { name: "float", limit: 24 },
decimal: { name: "decimal" },
datetime: { name: "datetime" },
+ timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
binary: { name: "blob" },
+ blob: { name: "blob" },
boolean: { name: "tinyint", limit: 1 },
json: { name: "json" },
}
- INDEX_TYPES = [:fulltext, :spatial]
- INDEX_USINGS = [:btree, :hash]
-
- def initialize(connection, logger, connection_options, config)
- super(connection, logger, config)
-
- @visitor = Arel::Visitors::MySQL.new self
-
- if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
- @prepared_statements = true
- @visitor.extend(DetermineIfPreparableVisitor)
- else
- @prepared_statements = false
- end
-
- if version < '5.0.0'
- raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.0."
- end
- end
-
- CHARSETS_OF_4BYTES_MAXLEN = ['utf8mb4', 'utf16', 'utf16le', 'utf32']
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
+ private
- def internal_string_options_for_primary_key # :nodoc:
- super.tap { |options|
- options[:collation] = collation.sub(/\A[^_]+/, 'utf8') if CHARSETS_OF_4BYTES_MAXLEN.include?(charset)
- }
+ def dealloc(stmt)
+ stmt.close
+ end
end
- def version
- @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0])
+ def initialize(connection, logger, connection_options, config)
+ super(connection, logger, config)
end
- # Returns true, since this connection adapter supports migrations.
- def supports_migrations?
- true
+ def version #:nodoc:
+ @version ||= Version.new(version_string)
end
- def supports_primary_key?
- true
+ def mariadb? # :nodoc:
+ /mariadb/i.match?(full_version)
end
def supports_bulk_alter? #:nodoc:
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
+ !mariadb? && version >= "8.0.1"
+ end
+
+ def supports_expression_index?
+ !mariadb? && version >= "8.0.13"
end
def supports_transaction_isolation?
@@ -123,19 +96,36 @@ module ActiveRecord
end
def supports_datetime_with_precision?
- version >= '5.6.4'
+ mariadb? || version >= "5.6.4"
+ end
+
+ def supports_virtual_columns?
+ mariadb? || version >= "5.7.5"
+ end
+
+ # See https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html for more details.
+ def supports_optimizer_hints?
+ !mariadb? && version >= "5.7.7"
end
def supports_advisory_locks?
true
end
+ def supports_insert_on_duplicate_skip?
+ true
+ end
+
+ def supports_insert_on_duplicate_update?
+ true
+ end
+
def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
- select_value("SELECT GET_LOCK('#{lock_name}', #{timeout});").to_s == '1'
+ query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
end
def release_advisory_lock(lock_name) # :nodoc:
- select_value("SELECT RELEASE_LOCK('#{lock_name}')").to_s == '1'
+ query_value("SELECT RELEASE_LOCK(#{quote(lock_name.to_s)})") == 1
end
def native_database_types
@@ -143,7 +133,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 ===========================================
@@ -154,48 +144,16 @@ module ActiveRecord
raise NotImplementedError
end
- def new_column(field, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil) # :nodoc:
- MySQL::Column.new(field, default, sql_type_metadata, null, table_name, 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 quoted_true
- QUOTED_TRUE
- end
-
- def unquoted_true
- 1
- end
-
- def quoted_false
- QUOTED_FALSE
- end
-
- def unquoted_false
- 0
- end
-
- def quoted_date(value)
- if supports_datetime_with_precision?
- super
- else
- super.sub(/\.\d{6}\z/, '')
- end
- end
-
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity #:nodoc:
- old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
+ old = query_value("SELECT @@FOREIGN_KEY_CHECKS")
begin
update("SET FOREIGN_KEY_CHECKS = 0")
@@ -205,27 +163,35 @@ module ActiveRecord
end
end
+ # CONNECTION MANAGEMENT ====================================
+
+ def clear_cache! # :nodoc:
+ reload_type_map
+ super
+ end
+
#--
# DATABASE STATEMENTS ======================================
#++
def explain(arel, binds = [])
sql = "EXPLAIN #{to_sql(arel, binds)}"
- start = Time.now
- result = exec_query(sql, 'EXPLAIN', binds)
- elapsed = Time.now - start
+ start = Concurrent.monotonic_time
+ result = exec_query(sql, "EXPLAIN", binds)
+ elapsed = Concurrent.monotonic_time - start
MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
end
- def clear_cache!
- super
- reload_type_map
- end
-
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
- log(sql, name) { @connection.query(sql) }
+ materialize_transactions
+
+ log(sql, name) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @connection.query(sql)
+ end
+ end
end
# Mysql2Adapter doesn't have to free a result after using it, but we use this method
@@ -252,19 +218,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, key) # :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
@@ -280,7 +234,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'
@@ -288,9 +242,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 row_format_dynamic_by_default?
+ 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
@@ -299,135 +257,38 @@ 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) # :nodoc:
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- #tables currently returns both tables and views.
- This behavior is deprecated and will be changed with Rails 5.1 to only return tables.
- Use #data_sources instead.
- MSG
-
- if name
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing arguments to #tables is deprecated without replacement.
- MSG
- end
-
- data_sources
- end
-
- def data_sources
- sql = "SELECT table_name FROM information_schema.tables "
- sql << "WHERE table_schema = #{quote(@config[:database])}"
-
- select_values(sql, 'SCHEMA')
- end
-
- def truncate(table_name, name = nil)
- execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
- end
-
- def table_exists?(table_name)
- # Update lib/active_record/internal_metadata.rb when this gets removed
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- #table_exists? currently checks both tables and views.
- This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
- Use #data_source_exists? instead.
- MSG
-
- data_source_exists?(table_name)
+ show_variable "collation_database"
end
- def data_source_exists?(table_name)
- return false unless table_name.present?
+ def table_comment(table_name) # :nodoc:
+ scope = quoted_scope(table_name)
- schema, name = table_name.to_s.split('.', 2)
- schema, name = @config[:database], schema unless name # A table was provided without a schema
-
- sql = "SELECT table_name FROM information_schema.tables "
- sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}"
-
- select_values(sql, 'SCHEMA').any?
- end
-
- def views # :nodoc:
- select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA')
- end
-
- def view_exists?(view_name) # :nodoc:
- return false unless view_name.present?
-
- schema, name = view_name.to_s.split('.', 2)
- schema, name = @config[:database], schema unless name # A view was provided without a schema
-
- sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'"
- sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}"
-
- select_values(sql, 'SCHEMA').any?
- end
-
- # Returns an array of indexes for the given table.
- def indexes(table_name, name = nil) #:nodoc:
- 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
-
- 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:
- table_name = table_name.to_s
- column_definitions(table_name).map do |field|
- type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
- if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP"
- default, default_function = nil, field[:Default]
- else
- default, default_function = field[:Default], nil
- end
- new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation])
- 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)
@@ -439,6 +300,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:
@@ -464,7 +330,6 @@ module ActiveRecord
# 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.
def drop_table(table_name, options = {})
- create_table_info_cache.delete(table_name) if create_table_info_cache.key?(table_name)
execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
end
@@ -480,99 +345,101 @@ 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}'
- SQL
+ raise ArgumentError unless table_name.present?
- create_table_info = create_table_info(table_name)
+ 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
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)
+ 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
-
- # Maps logical Rails types to MySQL-specific data types.
- def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil)
- 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
- super(type, limit, precision, scale)
+ raw_table_options.sub!(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
+
+ table_options[:options] = raw_table_options
+
+ # strip COMMENT
+ if raw_table_options.sub!(/ COMMENT='.+'/, "")
+ table_options[:comment] = table_comment(table_name)
end
- sql << ' unsigned' if unsigned && type != :primary_key
- sql
+ table_options
end
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
- variables = select_all("select @@#{name} as 'Value'", 'SCHEMA')
- variables.first['Value'] unless variables.empty?
+ query_value("SELECT @@#{name}", "SCHEMA")
rescue ActiveRecord::StatementInvalid
nil
end
@@ -580,22 +447,38 @@ module ActiveRecord
def primary_keys(table_name) # :nodoc:
raise ArgumentError unless table_name.present?
- schema, name = table_name.to_s.split('.', 2)
- schema, name = @config[:database], schema unless name # A table was provided without a schema
+ scope = quoted_scope(table_name)
- select_values(<<-SQL.strip_heredoc, 'SCHEMA')
+ query_values(<<~SQL, "SCHEMA")
SELECT column_name
FROM information_schema.key_column_usage
WHERE constraint_name = 'PRIMARY'
- AND table_schema = #{quote(schema)}
- AND table_name = #{quote(name)}
+ AND table_schema = #{scope[:schema]}
+ AND table_name = #{scope[:name]}
ORDER BY ordinal_position
SQL
end
- def case_sensitive_comparison(table, attribute, column, value)
- if !value.nil? && column.collation && !column.case_sensitive?
- table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new))
+ def default_uniqueness_comparison(attribute, value, klass) # :nodoc:
+ column = column_for_attribute(attribute)
+
+ if column.collation && !column.case_sensitive? && !value.nil?
+ ActiveSupport::Deprecation.warn(<<~MSG.squish)
+ Uniqueness validator will no longer enforce case sensitive comparison in Rails 6.1.
+ To continue case sensitive comparison on the :#{attribute.name} attribute in #{klass} model,
+ pass `case_sensitive: true` option explicitly to the uniqueness validator.
+ MSG
+ attribute.eq(Arel::Nodes::Bin.new(value))
+ else
+ super
+ end
+ end
+
+ def case_sensitive_comparison(attribute, value) # :nodoc:
+ column = column_for_attribute(attribute)
+
+ if column.collation && !column.case_sensitive?
+ attribute.eq(Arel::Nodes::Bin.new(value))
else
super
end
@@ -615,350 +498,334 @@ module ActiveRecord
# 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, '')
+ s.gsub(/\s+(?:ASC|DESC)\b/i, "")
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
- [super, *order_columns].join(', ')
+ (order_columns << super).join(", ")
end
def strict_mode?
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
+ def build_insert_sql(insert) # :nodoc:
+ sql = +"INSERT #{insert.into} #{insert.values_list}"
- 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)
- m.register_type %r(^json)i, MysqlJson.new
-
- 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)
+ if insert.skip_duplicates?
+ any_column = quote_column_name(insert.model.columns.first.name)
+ sql << " ON DUPLICATE KEY UPDATE #{any_column}=#{any_column}"
+ elsif insert.update_duplicates?
+ sql << " ON DUPLICATE KEY UPDATE "
+ sql << insert.updatable_columns.map { |column| "#{column}=VALUES(#{column})" }.join(",")
end
- 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
+ sql
end
- def register_integer_type(mapping, key, options) # :nodoc:
- mapping.register_type(key) do |sql_type|
- if /\bunsigned\z/ === 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
+ def initialize_type_map(m = type_map)
super
- end
- end
- def fetch_type_metadata(sql_type, extra = "")
- MySQL::TypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?)
- 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\s*\((.+)\)/i, 1]
+ .split(",").map { |enum| enum.strip.length - 2 }.max
+ MysqlString.new(limit: limit)
+ 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})"}
+ m.register_type(%r(^set)i) do |sql_type|
+ limit = sql_type[/^set\s*\((.+)\)/i, 1]
+ .split(",").map { |set| set.strip.length - 1 }.sum - 1
+ MysqlString.new(limit: limit)
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)
-
- # add index sort order
- option_strings = add_index_sort_order(option_strings, column_names, options)
-
- column_names.map {|name| quote_column_name(name) + option_strings[name]}
- end
-
- def translate_exception(exception, message)
- case error_number(exception)
- when 1062
- RecordNotUnique.new(message)
- when 1452
- InvalidForeignKey.new(message)
- else
- super
+ 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
- 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)
-
- 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
+ ER_FK_INCOMPATIBLE_COLUMNS = 3780
+
+ def translate_exception(exception, message:, sql:, binds:)
+ case error_number(exception)
+ when ER_DUP_ENTRY
+ RecordNotUnique.new(message, sql: sql, binds: binds)
+ when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2
+ InvalidForeignKey.new(message, sql: sql, binds: binds)
+ when ER_CANNOT_ADD_FOREIGN, ER_FK_INCOMPATIBLE_COLUMNS
+ mismatched_foreign_key(message, sql: sql, binds: binds)
+ when ER_CANNOT_CREATE_TABLE
+ if message.include?("errno: 150")
+ mismatched_foreign_key(message, sql: sql, binds: binds)
+ else
+ super
+ end
+ when ER_DATA_TOO_LONG
+ ValueTooLong.new(message, sql: sql, binds: binds)
+ when ER_OUT_OF_RANGE
+ RangeError.new(message, sql: sql, binds: binds)
+ when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT
+ NotNullViolation.new(message, sql: sql, binds: binds)
+ when ER_LOCK_DEADLOCK
+ Deadlocked.new(message, sql: sql, binds: binds)
+ when ER_LOCK_WAIT_TIMEOUT
+ LockWaitTimeout.new(message, sql: sql, binds: binds)
+ when ER_QUERY_TIMEOUT
+ StatementTimeout.new(message, sql: sql, binds: binds)
+ when ER_QUERY_INTERRUPTED
+ QueryCanceled.new(message, sql: sql, binds: binds)
+ 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]
+ def add_timestamps_for_alter(table_name, options = {})
+ options[:null] = false if options[:null].nil?
- # Materialize subquery by adding distinct
- # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
- subsubselect.distinct unless select.limit || select.offset || select.orders.any?
+ if !options.key?(:precision) && supports_datetime_with_precision?
+ options[:precision] = 6
+ end
- subselect = Arel::SelectManager.new(select.engine)
- subselect.project Arel.sql(key.name)
- subselect.from subsubselect.as('__active_record_temp')
- end
+ [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
+ end
- def mariadb?
- full_version =~ /mariadb/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 supports_rename_index?
- mariadb? ? false : version >= '5.7.6'
- end
+ def supports_rename_index?
+ mariadb? ? false : version >= "5.7.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 = @config[:wait_timeout]
- wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
- variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout)
+ # 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
- defaults = [':default', :default].to_set
+ defaults = [":default", :default].to_set
- # Make MySQL reject illegal values rather than truncating or blanking them, see
- # http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables
- # If the user has provided another value for sql_mode, don't replace it.
- 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', '')"
+ # 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 = "CONCAT(#{sql_mode}, ',NO_AUTO_VALUE_ON_ZERO')"
- end
- sql_mode_assignment = "@@SESSION.sql_mode = #{sql_mode}, " if sql_mode
-
- # NAMES does not have an equals sign, see
- # http://dev.mysql.com/doc/refman/5.7/en/set-statement.html#id944430
- # (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
-
- # 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)}"
+ 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
- # or else nil; compact to clear nils out
- end.compact.join(', ')
- # ...and send them all in one query
- @connection.query "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)}"
+ end
+ # or else nil; compact to clear nils out
+ end.compact.join(", ")
- def column_definitions(table_name) # :nodoc:
- execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result|
- each_hash(result)
+ # ...and send them all in one query
+ execute "SET #{encoding} #{sql_mode_assignment} #{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
+ def column_definitions(table_name) # :nodoc:
+ execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
+ each_hash(result)
end
end
- end
-
- def create_table_info_cache # :nodoc:
- @create_table_info_cache ||= {}
- end
-
- def create_table_info(table_name) # :nodoc:
- create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
- end
- def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
- MySQL::TableDefinition.new(name, temporary, options, as)
- end
-
- 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}")
+ def create_table_info(table_name) # :nodoc:
+ exec_query("SHOW CREATE TABLE #{quote_table_name(table_name)}", "SCHEMA").first["Create Table"]
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 byte length #{limit}")
+ def arel_visitor
+ Arel::Visitors::MySQL.new(self)
end
- end
- 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}")
+ def build_statement_pool
+ StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
end
- end
- class MysqlJson < Type::Internal::AbstractJson # :nodoc:
- def changed_in_place?(raw_old_value, new_value)
- # Normalization is required because MySQL JSON data format includes
- # the space between the elements.
- super(serialize(deserialize(raw_old_value)), new_value)
- end
- end
+ def mismatched_foreign_key(message, sql:, binds:)
+ match = %r/
+ (?:CREATE|ALTER)\s+TABLE\s*(?:`?\w+`?\.)?`?(?<table>\w+)`?.+?
+ FOREIGN\s+KEY\s*\(`?(?<foreign_key>\w+)`?\)\s*
+ REFERENCES\s*(`?(?<target_table>\w+)`?)\s*\(`?(?<primary_key>\w+)`?\)
+ /xmi.match(sql)
- class MysqlString < Type::String # :nodoc:
- def serialize(value)
- case value
- when true then "1"
- when false then "0"
- else super
+ options = {
+ message: message,
+ sql: sql,
+ binds: binds,
+ }
+
+ if match
+ options[:table] = match[:table]
+ options[:foreign_key] = match[:foreign_key]
+ options[:target_table] = match[:target_table]
+ options[:primary_key] = match[:primary_key]
+ options[:primary_key_column] = column_for(match[:target_table], match[:primary_key])
end
+
+ MismatchedForeignKey.new(options)
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(:json, MysqlJson, adapter: :mysql2)
- ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2)
- ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, 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 2e718b29fa..5d81de9fe1 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -1,21 +1,21 @@
-require 'set'
+# frozen_string_literal: true
module ActiveRecord
# :stopdoc:
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
- attr_reader :name, :default, :sql_type_metadata, :null, :table_name, :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</tt>.
+ # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id bigint</tt>.
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
# +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil)
+ 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
@@ -23,6 +23,7 @@ module ActiveRecord
@default = default
@default_function = default_function
@collation = collation
+ @comment = comment
end
def has_default?
@@ -30,7 +31,7 @@ module ActiveRecord
end
def bigint?
- /\Abigint\b/ === sql_type
+ /\Abigint\b/.match?(sql_type)
end
# Returns the human name of the column name.
@@ -41,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
@@ -53,9 +76,9 @@ module ActiveRecord
protected
- def attributes_for_hash
- [self.class, name, default, sql_type_metadata, null, table_name, 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 4bc6447368..9eaf9d9a89 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 && @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,141 @@ 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 '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 LoadError, "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 LoadError, "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(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
- # 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
+ # 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, <<~MSG
+ The `#{env_name}` database is not configured for the `#{ActiveRecord::ConnectionHandling::DEFAULT_ENV.call}` environment.
+
+ Available databases configurations are:
+
+ #{build_configuration_sentence}
+ MSG
+ 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}")
+ def build_configuration_sentence # :nodoc:
+ configs = configurations.configs_for(include_replicas: true)
+
+ configs.group_by(&:env_name).map do |env, config|
+ namespaces = config.map(&:spec_name)
+ if namespaces.size > 1
+ "#{env}: #{namespaces.join(", ")}"
+ else
+ env
+ end
+ end.join("\n")
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
index 0fdc185c45..1df4dea2d8 100644
--- a/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
+++ b/activerecord/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb
@@ -1,14 +1,21 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module DetermineIfPreparableVisitor
- attr_reader :preparable
+ attr_accessor :preparable
def accept(*)
@preparable = true
super
end
- def visit_Arel_Nodes_In(*)
+ def visit_Arel_Nodes_In(o, collector)
+ @preparable = false
+ super
+ end
+
+ def visit_Arel_Nodes_NotIn(o, collector)
@preparable = false
super
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb
index 9c45fdd44a..fa1541019d 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb
@@ -1,48 +1,25 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module MySQL
class Column < ConnectionAdapters::Column # :nodoc:
- delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true
-
- def initialize(*)
- super
- assert_valid_default
- extract_default
- 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?
- /\A(?:tiny|medium|long)?blob\b/ === sql_type || type == :text
- end
+ delegate :extra, to: :sql_type_metadata, allow_nil: true
def unsigned?
- /\bunsigned\z/ === sql_type
+ /\bunsigned(?: zerofill)?\z/.match?(sql_type)
end
def case_sensitive?
- collation && collation !~ /_ci\z/
+ collation && !/_ci\z/.match?(collation)
end
def auto_increment?
- extra == 'auto_increment'
- end
-
- private
-
- def extract_default
- if blob_or_text_column?
- @default = null || strict ? nil : ''
- end
+ extra == "auto_increment"
end
- def assert_valid_default
- if blob_or_text_column? && default.present?
- raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
- end
+ def virtual?
+ /\b(?:VIRTUAL|STORED|PERSISTENT)\b/.match?(extra)
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..1199c0ad1b
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -0,0 +1,201 @@
+# 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
+ @connection.abandon_results!
+ result
+ end
+
+ def query(sql, name = nil) # :nodoc:
+ execute(sql, name).to_a
+ end
+
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :explain, :select, :set, :show, :release, :savepoint, :rollback) # :nodoc:
+ private_constant :READ_QUERY
+
+ def write_query?(sql) # :nodoc:
+ !READ_QUERY.match?(sql)
+ end
+
+ # Executes the SQL statement in the context of this connection.
+ def execute(sql, name = nil)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
+ # 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)
+ if without_prepared_statement?(binds)
+ execute_and_free(sql, name) do |result|
+ if result
+ ActiveRecord::Result.new(result.fields, result.to_a)
+ else
+ ActiveRecord::Result.new([], [])
+ end
+ end
+ else
+ exec_stmt_and_free(sql, name, binds, cache_stmt: prepare) do |_, result|
+ if result
+ ActiveRecord::Result.new(result.fields, result.to_a)
+ else
+ ActiveRecord::Result.new([], [])
+ end
+ end
+ end
+ end
+
+ def exec_delete(sql, name = nil, binds = [])
+ 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 execute_batch(sql, name = nil)
+ super
+ @connection.abandon_results!
+ end
+
+ def default_insert_value(column)
+ super unless column.auto_increment?
+ end
+
+ def last_inserted_id(result)
+ @connection.last_id
+ end
+
+ def supports_set_server_option?
+ @connection.respond_to?(:set_server_option)
+ end
+
+ def build_truncate_statements(*table_names)
+ if table_names.size == 1
+ super.first
+ else
+ super
+ end
+ 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 combine_multi_statements(total_sql)
+ total_sql.each_with_object([]) do |sql, total_sql_chunks|
+ previous_packet = total_sql_chunks.last
+ if max_allowed_packet_reached?(sql, previous_packet)
+ total_sql_chunks << +sql
+ else
+ previous_packet << ";\n"
+ previous_packet << sql
+ end
+ end
+ end
+
+ def max_allowed_packet_reached?(current_packet, previous_packet)
+ if current_packet.bytesize > max_allowed_packet
+ raise ActiveRecordError,
+ "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable."
+ elsif previous_packet.nil?
+ true
+ else
+ (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet
+ end
+ end
+
+ def max_allowed_packet
+ @max_allowed_packet ||= begin
+ bytes_margin = 2
+ show_variable("max_allowed_packet") - bytes_margin
+ end
+ end
+
+ def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
+ materialize_transactions
+
+ # 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
index 1820853196..20c3c83664 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module MySQL
@@ -36,34 +38,34 @@ module ActiveRecord
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
+ 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
- end
- def build_separator(widths)
- padding = 1
- '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
- 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])
+ 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
- '| ' + cells.join(' | ') + ' |'
- end
- def build_footer(nrows, elapsed)
- rows_label = nrows == 1 ? 'row' : 'rows'
- "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
- end
+ def build_footer(nrows, elapsed)
+ rows_label = nrows == 1 ? "row" : "rows"
+ "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
+ 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
index 7c5980da2a..75564a61d6 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module MySQL
@@ -7,16 +9,33 @@ module ActiveRecord
end
def quote_table_name(name)
- @quoted_table_names[name] ||= super.gsub('.', '`.`')
+ @quoted_table_names[name] ||= super.gsub(".", "`.`").freeze
end
- private
+ def unquoted_true
+ 1
+ end
- def _quote(value)
- if value.is_a?(Type::Binary::Data)
- "x'#{value.hex}'"
- else
+ 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
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
index 1e2c859af9..82ed320617 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
@@ -1,56 +1,71 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module MySQL
- class SchemaCreation < AbstractAdapter::SchemaCreation
+ 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_ColumnDefinition(o)
- o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned)
- super
- end
-
- def visit_AddColumnDefinition(o)
- add_column_position!(super, column_options(o.column))
- end
-
- 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]}"
+ 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
- if options[:collation]
- sql << " COLLATE #{options[:collation]}"
+
+ 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
- super
- end
-
- def add_column_position!(sql, options)
- if options[:first]
- sql << " FIRST"
- elsif options[:after]
- sql << " AFTER #{quote_column_name(options[:after])}"
+
+ def add_column_position!(sql, options)
+ if options[:first]
+ sql << " FIRST"
+ elsif options[:after]
+ sql << " AFTER #{quote_column_name(options[:after])}"
+ end
+
+ sql
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
+ 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
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
index 157e75dbf7..d21535a709 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
@@ -1,88 +1,90 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module MySQL
module ColumnMethods
- def primary_key(name, type = :primary_key, **options)
- options[:auto_increment] = true if type == :bigint && !options.key?(:default)
- super
- end
+ extend ActiveSupport::Concern
- def blob(*args, **options)
- args.each { |name| column(name, :blob, options) }
- end
+ ##
+ # :method: blob
+ # :call-seq: blob(*names, **options)
- def tinyblob(*args, **options)
- args.each { |name| column(name, :tinyblob, options) }
- end
+ ##
+ # :method: tinyblob
+ # :call-seq: tinyblob(*names, **options)
- def mediumblob(*args, **options)
- args.each { |name| column(name, :mediumblob, options) }
- end
+ ##
+ # :method: mediumblob
+ # :call-seq: mediumblob(*names, **options)
- def longblob(*args, **options)
- args.each { |name| column(name, :longblob, options) }
- end
+ ##
+ # :method: longblob
+ # :call-seq: longblob(*names, **options)
- def tinytext(*args, **options)
- args.each { |name| column(name, :tinytext, options) }
- end
+ ##
+ # :method: tinytext
+ # :call-seq: tinytext(*names, **options)
- def mediumtext(*args, **options)
- args.each { |name| column(name, :mediumtext, options) }
- end
+ ##
+ # :method: mediumtext
+ # :call-seq: mediumtext(*names, **options)
- def longtext(*args, **options)
- args.each { |name| column(name, :longtext, options) }
- end
+ ##
+ # :method: longtext
+ # :call-seq: longtext(*names, **options)
- def json(*args, **options)
- args.each { |name| column(name, :json, options) }
- end
+ ##
+ # :method: unsigned_integer
+ # :call-seq: unsigned_integer(*names, **options)
- def unsigned_integer(*args, **options)
- args.each { |name| column(name, :unsigned_integer, options) }
- end
+ ##
+ # :method: unsigned_bigint
+ # :call-seq: unsigned_bigint(*names, **options)
- def unsigned_bigint(*args, **options)
- args.each { |name| column(name, :unsigned_bigint, options) }
- end
+ ##
+ # :method: unsigned_float
+ # :call-seq: unsigned_float(*names, **options)
- def unsigned_float(*args, **options)
- args.each { |name| column(name, :unsigned_float, options) }
- end
+ ##
+ # :method: unsigned_decimal
+ # :call-seq: unsigned_decimal(*names, **options)
- def unsigned_decimal(*args, **options)
- args.each { |name| column(name, :unsigned_decimal, options) }
+ included do
+ define_column_methods :blob, :tinyblob, :mediumblob, :longblob,
+ :tinytext, :mediumtext, :longtext, :unsigned_integer, :unsigned_bigint,
+ :unsigned_float, :unsigned_decimal
end
end
- class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
- attr_accessor :charset, :unsigned
- end
-
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
include ColumnMethods
- def new_column_definition(name, type, options) # :nodoc:
- column = super
- case column.type
+ def new_column_definition(name, type, **options) # :nodoc:
+ case type
+ when :virtual
+ type = options[:type]
when :primary_key
- column.type = :integer
- column.auto_increment = true
+ type = :integer
+ options[:limit] ||= 8
+ options[:primary_key] = true
when /\Aunsigned_(?<type>.+)\z/
- column.type = $~[:type].to_sym
- column.unsigned = true
+ type = $~[:type].to_sym
+ options[:unsigned] = true
end
- column.unsigned ||= options[:unsigned]
- column.charset = options[:charset]
- column
+
+ super
end
private
+ def aliased_types(name, fallback)
+ fallback
+ end
- def create_column_definition(name, type)
- MySQL::ColumnDefinition.new(name, type)
- end
+ def integer_like_primary_key_type(type, options)
+ options[:auto_increment] = true
+ type
+ end
end
class Table < ActiveRecord::ConnectionAdapters::Table
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
index be40df4101..57518b02fa 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -1,53 +1,85 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module MySQL
- module ColumnDumper
- def column_spec_for_primary_key(column)
- if column.bigint?
- spec = { id: :bigint.inspect }
- spec[:default] = schema_default(column) || 'nil' unless column.auto_increment?
- else
- spec = super.except!(:null)
- end
- spec[:unsigned] = 'true' if column.unsigned?
- spec
- end
-
- def prepare_column_options(column)
- spec = super
- spec[:unsigned] = 'true' if column.unsigned?
- spec
- end
-
- def migration_keys
- super + [:unsigned]
- end
-
+ 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 /\A(?<size>tiny|medium|long)(?:text|blob)/ =~ column.sql_type
+ spec = { size: size.to_sym.inspect }.merge!(spec)
+ end
+
+ if @connection.supports_virtual_columns? && column.virtual?
+ spec[:as] = extract_expression_for_virtual_column(column)
+ spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra)
+ 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?
- 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
+ else
+ super
+ end
+ end
+
+ def schema_limit(column)
+ super unless /\A(?:tiny|medium|long)?(?:text|blob)/.match?(column.sql_type)
+ end
- def schema_type(column)
- if column.sql_type == 'tinyblob'
- :blob
- else
- super
+ def schema_precision(column)
+ super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0
end
- 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.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 schema_collation(column)
- if column.collation && table_name = column.table_name
- @table_collation_cache ||= {}
- @table_collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"]
- column.collation.inspect if column.collation != @table_collation_cache[table_name]
+ 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..4018f0815c
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -0,0 +1,261 @@
+# 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 create_table(table_name, options: default_row_format, **)
+ super
+ end
+
+ def internal_string_options_for_primary_key
+ super.tap do |options|
+ if !row_format_dynamic_by_default? && CHARSETS_OF_4BYTES_MAXLEN.include?(charset)
+ 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
+
+ # Maps logical Rails types to MySQL-specific data types.
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, size: limit_to_size(limit, type), unsigned: nil, **)
+ sql =
+ case type.to_s
+ when "integer"
+ integer_to_sql(limit)
+ when "text"
+ type_with_size_to_sql("text", size)
+ when "blob"
+ type_with_size_to_sql("blob", size)
+ when "binary"
+ if (0..0xfff) === limit
+ "varbinary(#{limit})"
+ else
+ type_with_size_to_sql("blob", size)
+ end
+ else
+ super
+ end
+
+ sql = "#{sql} unsigned" if unsigned && type != :primary_key
+ sql
+ end
+
+ private
+ CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"]
+
+ def row_format_dynamic_by_default?
+ if mariadb?
+ version >= "10.2.2"
+ else
+ version >= "5.7.9"
+ end
+ end
+
+ def default_row_format
+ return if row_format_dynamic_by_default?
+
+ unless defined?(@default_row_format)
+ if query_value("SELECT @@innodb_file_per_table = 1 AND @@innodb_file_format = 'Barracuda'") == 1
+ @default_row_format = "ROW_FORMAT=DYNAMIC"
+ else
+ @default_row_format = nil
+ end
+ end
+
+ @default_row_format
+ end
+
+ def schema_creation
+ MySQL::SchemaCreation.new(self)
+ end
+
+ def create_table_definition(*args)
+ MySQL::TableDefinition.new(self, *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
+
+ def type_with_size_to_sql(type, size)
+ case size&.to_s
+ when nil, "tiny", "medium", "long"
+ "#{size}#{type}"
+ else
+ raise ArgumentError,
+ "#{size.inspect} is invalid :size value. Only :tiny, :medium, and :long are allowed."
+ end
+ end
+
+ def limit_to_size(limit, type)
+ case type.to_s
+ when "text", "blob", "binary"
+ case limit
+ when 0..0xff; "tiny"
+ when nil, 0x100..0xffff; nil
+ when 0x10000..0xffffff; "medium"
+ when 0x1000000..0xffffffff; "long"
+ else raise ActiveRecordError, "No #{type} type has byte size #{limit}"
+ end
+ end
+ end
+
+ def integer_to_sql(limit)
+ 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
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
index e1e3f7b472..7ad0944d51 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb
@@ -1,14 +1,17 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module MySQL
class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc:
- attr_reader :extra, :strict
+ undef to_yaml if method_defined?(:to_yaml)
+
+ attr_reader :extra
- def initialize(type_metadata, extra: "", strict: false)
+ def initialize(type_metadata, extra: "")
super(type_metadata)
@type_metadata = type_metadata
@extra = extra
- @strict = strict
end
def ==(other)
@@ -23,9 +26,9 @@ module ActiveRecord
protected
- def attributes_for_hash
- [self.class, @type_metadata, extra, strict]
- end
+ def attributes_for_hash
+ [self.class, @type_metadata, extra]
+ 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 e7541748de..9bdaa00336 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,23 +1,22 @@
-require 'active_record/connection_adapters/abstract_mysql_adapter'
+# frozen_string_literal: true
-gem 'mysql2', '>= 0.3.18', '< 0.5'
-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[:username] = 'root' if config[:username].nil?
config[:flags] ||= 0
- if Mysql2::Client.const_defined? :FOUND_ROWS
- if config[:flags].kind_of? Array
- config[:flags].push "FOUND_ROWS".freeze
- else
- config[:flags] |= Mysql2::Client::FOUND_ROWS
- end
+ if config[:flags].kind_of? Array
+ config[:flags].push "FOUND_ROWS"
+ else
+ config[:flags] |= Mysql2::Client::FOUND_ROWS
end
client = Mysql2::Client.new(config)
@@ -33,23 +32,41 @@ 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_json?
- !mariadb? && version >= '5.7.8'
+ !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
# HELPER METHODS ===========================================
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
@@ -74,7 +91,6 @@ module ActiveRecord
#++
def active?
- return false unless @connection
@connection.ping
end
@@ -89,76 +105,29 @@ module ActiveRecord
# Otherwise, this method does nothing.
def disconnect!
super
- unless @connection.nil?
- @connection.close
- @connection = nil
- end
- end
-
- #--
- # DATABASE STATEMENTS ======================================
- #++
-
- # Returns a record hash with the column names as keys and column values
- # as values.
- def select_one(arel, name = nil, binds = [])
- arel, binds = binds_from_relation(arel, binds)
- execute(to_sql(arel, binds), name).each(as: :hash) do |row|
- @connection.next_result while @connection.more_results?
- return row
- end
- 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 = [])
- result = execute(sql, name)
- @connection.next_result while @connection.more_results?
- result.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
- end
-
- super
- end
-
- def exec_query(sql, name = 'SQL', binds = [], prepare: false)
- result = execute(sql, name)
- @connection.next_result while @connection.more_results?
- ActiveRecord::Result.new(result.fields, result.to_a) if result
+ @connection.close
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
+ def discard! # :nodoc:
+ @connection.automatic_close = false
+ @connection = nil
end
private
- def connect
- @connection = Mysql2::Client.new(@config)
- configure_connection
- end
+ def connect
+ @connection = Mysql2::Client.new(@config)
+ configure_connection
+ end
- def configure_connection
- @connection.query_options.merge!(:as => :array)
- super
- end
+ def configure_connection
+ @connection.query_options[:as] = :array
+ super
+ end
- def full_version
- @full_version ||= @connection.server_info[:version]
- end
+ def full_version
+ @full_version ||= @connection.server_info[:version]
+ 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 3ad1911a28..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,11 +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
- %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 6f2e03b370..ae7dbd2868 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -1,38 +1,12 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module DatabaseStatements
def explain(arel, binds = [])
sql = "EXPLAIN #{to_sql(arel, binds)}"
- PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
- end
-
- def select_value(arel, name = nil, binds = [])
- arel, binds = binds_from_relation arel, binds
- sql = to_sql(arel, binds)
- execute_and_clear(sql, name, binds) do |result|
- result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0
- end
- end
-
- def select_values(arel, name = nil, binds = [])
- arel, binds = binds_from_relation arel, binds
- sql = to_sql(arel, binds)
- execute_and_clear(sql, name, binds) do |result|
- 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
+ PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", binds))
end
# The internal PostgreSQL identifier of the money data type.
@@ -74,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
@@ -84,22 +58,41 @@ 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.
- # Note: the PGresult object is manually memory managed; if you don't
- # need it specifically, you many want consider the exec_query wrapper.
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :explain, :select, :set, :show, :release, :savepoint, :rollback) # :nodoc:
+ private_constant :READ_QUERY
+
+ def write_query?(sql) # :nodoc:
+ !READ_QUERY.match?(sql)
+ end
+
+ # 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)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
+ 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 = [], prepare: false)
+ def exec_query(sql, name = "SQL", binds = [], prepare: false)
execute_and_clear(sql, name, binds, prepare: prepare) do |result|
types = {}
fields = result.fields
@@ -112,36 +105,41 @@ 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) # :nodoc:
+ def sql_for_insert(sql, pk, 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
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
@@ -164,6 +162,20 @@ module ActiveRecord
def exec_rollback_db_transaction
execute "ROLLBACK"
end
+
+ private
+ def build_truncate_statements(*table_names)
+ "TRUNCATE TABLE #{table_names.map(&method(:quote_table_name)).join(", ")}"
+ end
+
+ # 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
index 789b88912c..086a5dcc15 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -26,12 +28,12 @@ module ActiveRecord
pp = []
pp << header.center(width).rstrip
- pp << '-' * width
+ pp << "-" * width
- pp += lines.map {|line| " #{line}"}
+ pp += lines.map { |line| " #{line}" }
nrows = result.rows.length
- rows_label = nrows == 1 ? 'row' : 'rows'
+ rows_label = nrows == 1 ? "row" : "rows"
pp << "(#{nrows} #{rows_label})"
pp.join("\n") + "\n"
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 87593ef704..b1dfbde86e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -1,14 +1,18 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
class Array < Type::Value # :nodoc:
- include Type::Helpers::Mutable
+ include ActiveModel::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
@@ -54,15 +68,23 @@ module ActiveRecord
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 424769f765..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,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -5,8 +7,8 @@ module ActiveRecord
class DateTime < Type::DateTime # :nodoc:
def cast_value(value)
case value
- when 'infinity' then ::Float::INFINITY
- when '-infinity' then -::Float::INFINITY
+ 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))
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..7b42677101 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
class Hstore < Type::Value # :nodoc:
- include Type::Helpers::Mutable
+ include ActiveModel::Type::Helpers::Mutable
def type
:hstore
@@ -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 dbc879ffd4..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- module PostgreSQL
- module OID # :nodoc:
- class Json < Type::Internal::AbstractJson
- 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..7f6adc351c
--- /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 ActiveModel::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 dcc12ae2a4..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,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -22,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..8c74cecc4d 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -1,9 +1,13 @@
+# frozen_string_literal: true
+
module ActiveRecord
+ Point = Struct.new(:x, :y)
+
module ConnectionAdapters
module PostgreSQL
module OID # :nodoc:
class Point < Type::Value # :nodoc:
- include Type::Helpers::Mutable
+ include ActiveModel::Type::Helpers::Mutable
def type
:point
@@ -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 a8d2310035..aa7701e038 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
@@ -14,11 +14,11 @@ 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 if value == "empty"
return value unless value.is_a?(::String)
extracted = extract_bounds(value)
@@ -35,7 +35,7 @@ module ActiveRecord
if value.is_a?(::Range)
from = type_cast_single_for_database(value.begin)
to = type_cast_single_for_database(value.end)
- "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}"
+ ::Range.new(from, to, value.exclude_end?)
else
super
end
@@ -53,39 +53,43 @@ module ActiveRecord
::Range.new(new_begin, new_end, value.exclude_end?)
end
+ def force_equality?(value)
+ value.is_a?(::Range)
+ end
+
private
- def type_cast_single(value)
- infinity?(value) ? value : @subtype.deserialize(value)
- end
+ def type_cast_single(value)
+ infinity?(value) ? value : @subtype.deserialize(value)
+ end
- def type_cast_single_for_database(value)
- infinity?(value) ? '' : @subtype.serialize(value)
- end
+ def type_cast_single_for_database(value)
+ infinity?(value) ? value : @subtype.serialize(@subtype.cast(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 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
+ def infinity(negative: false)
+ if subtype.respond_to?(:infinity)
+ subtype.infinity(negative: negative)
+ elsif negative
+ -::Float::INFINITY
+ else
+ ::Float::INFINITY
+ end
end
- end
- def infinity?(value)
- value.respond_to?(:infinite?) && value.infinite?
- 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 6155e53632..203087bc36 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,10 +33,10 @@ 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(", ")]
+ <<~SQL % [known_type_names.join(", "), known_type_types.join(", ")]
WHERE
t.typname IN (%s)
OR t.typtype IN (%s)
@@ -42,66 +46,66 @@ module ActiveRecord
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..28abdbd073 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
@@ -11,9 +13,12 @@ module ActiveRecord
:uuid
end
- def cast(value)
- value.to_s[ACCEPTABLE_UUID, 0]
- end
+ private
+
+ def cast_value(value)
+ casted = value.to_s
+ casted if casted.match?(ACCEPTABLE_UUID)
+ end
end
end
end
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 6414459cd1..d40e0ef1f0 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
@@ -28,12 +30,12 @@ module ActiveRecord
# - "schema.name".table_name
# - "schema.name"."table.name"
def quote_table_name(name) # :nodoc:
- @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted
+ @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)
- PGconn.quote_ident(name)
+ PG::Connection.quote_ident(name)
end
def quote_table_name_for_assignment(table, attr)
@@ -42,7 +44,7 @@ module ActiveRecord
# Quotes column names for use in SQL queries.
def quote_column_name(name) # :nodoc:
- @quoted_column_names[name] ||= PGconn.quote_ident(super)
+ @quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze
end
# Quote date/time values for use in SQL input.
@@ -55,10 +57,14 @@ module ActiveRecord
end
end
+ def quoted_binary(value) # :nodoc:
+ "'#{escape_bytea(value.to_s)}'"
+ end
+
def quote_default_expression(value, column) # :nodoc:
if value.is_a?(Proc)
value.call
- elsif column.type == :uuid && value =~ /\(\)/
+ 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)
@@ -73,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.begin)},#{type_cast_range_value(range.end)}#{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..84dd28907b
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
@@ -0,0 +1,76 @@
+# 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 visit_ChangeColumnDefinition(o)
+ column = o.column
+ column.sql_type = type_to_sql(column.type, column.options)
+ quoted_column_name = quote_column_name(o.name)
+
+ change_column_sql = +"ALTER COLUMN #{quoted_column_name} TYPE #{column.sql_type}"
+
+ options = column_options(column)
+
+ if options[:collation]
+ change_column_sql << " COLLATE \"#{options[:collation]}\""
+ end
+
+ if options[:using]
+ change_column_sql << " USING #{options[:using]}"
+ elsif options[:cast_as]
+ cast_as_type = type_to_sql(options[:cast_as], options)
+ change_column_sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})"
+ end
+
+ if options.key?(:default)
+ if options[:default].nil?
+ change_column_sql << ", ALTER COLUMN #{quoted_column_name} DROP DEFAULT"
+ else
+ quoted_default = quote_default_expression(options[:default], column)
+ change_column_sql << ", ALTER COLUMN #{quoted_column_name} SET DEFAULT #{quoted_default}"
+ end
+ end
+
+ if options.key?(:null)
+ change_column_sql << ", ALTER COLUMN #{quoted_column_name} #{options[:null] ? 'DROP' : 'SET'} NOT NULL"
+ end
+
+ change_column_sql
+ 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 6399bddbee..3bb7c52899 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -1,7 +1,11 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module ColumnMethods
+ extend ActiveSupport::Concern
+
# Defines the primary key field.
# Use of the native PostgreSQL UUID type is supported, and can be used
# by defining your tables as such:
@@ -11,11 +15,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,158 +38,185 @@ 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
- def bigserial(*args, **options)
- args.each { |name| column(name, :bigserial, options) }
- end
+ ##
+ # :method: bigserial
+ # :call-seq: bigserial(*names, **options)
- def bit(*args, **options)
- args.each { |name| column(name, :bit, options) }
- end
+ ##
+ # :method: bit
+ # :call-seq: bit(*names, **options)
- def bit_varying(*args, **options)
- args.each { |name| column(name, :bit_varying, options) }
- end
+ ##
+ # :method: bit_varying
+ # :call-seq: bit_varying(*names, **options)
- def cidr(*args, **options)
- args.each { |name| column(name, :cidr, options) }
- end
+ ##
+ # :method: cidr
+ # :call-seq: cidr(*names, **options)
- def citext(*args, **options)
- args.each { |name| column(name, :citext, options) }
- end
+ ##
+ # :method: citext
+ # :call-seq: citext(*names, **options)
- def daterange(*args, **options)
- args.each { |name| column(name, :daterange, options) }
- end
+ ##
+ # :method: daterange
+ # :call-seq: daterange(*names, **options)
- def hstore(*args, **options)
- args.each { |name| column(name, :hstore, options) }
- end
+ ##
+ # :method: hstore
+ # :call-seq: hstore(*names, **options)
- def inet(*args, **options)
- args.each { |name| column(name, :inet, options) }
- end
+ ##
+ # :method: inet
+ # :call-seq: inet(*names, **options)
- def int4range(*args, **options)
- args.each { |name| column(name, :int4range, options) }
- end
+ ##
+ # :method: interval
+ # :call-seq: interval(*names, **options)
- def int8range(*args, **options)
- args.each { |name| column(name, :int8range, options) }
- end
+ ##
+ # :method: int4range
+ # :call-seq: int4range(*names, **options)
- def json(*args, **options)
- args.each { |name| column(name, :json, options) }
- end
+ ##
+ # :method: int8range
+ # :call-seq: int8range(*names, **options)
- def jsonb(*args, **options)
- args.each { |name| column(name, :jsonb, options) }
- end
+ ##
+ # :method: jsonb
+ # :call-seq: jsonb(*names, **options)
- def ltree(*args, **options)
- args.each { |name| column(name, :ltree, options) }
- end
+ ##
+ # :method: ltree
+ # :call-seq: ltree(*names, **options)
- def macaddr(*args, **options)
- args.each { |name| column(name, :macaddr, options) }
- end
+ ##
+ # :method: macaddr
+ # :call-seq: macaddr(*names, **options)
- def money(*args, **options)
- args.each { |name| column(name, :money, options) }
- end
+ ##
+ # :method: money
+ # :call-seq: money(*names, **options)
- def numrange(*args, **options)
- args.each { |name| column(name, :numrange, options) }
- end
+ ##
+ # :method: numrange
+ # :call-seq: numrange(*names, **options)
- def point(*args, **options)
- args.each { |name| column(name, :point, options) }
- end
+ ##
+ # :method: oid
+ # :call-seq: oid(*names, **options)
- def line(*args, **options)
- args.each { |name| column(name, :line, options) }
- end
+ ##
+ # :method: point
+ # :call-seq: point(*names, **options)
- def lseg(*args, **options)
- args.each { |name| column(name, :lseg, options) }
- end
+ ##
+ # :method: line
+ # :call-seq: line(*names, **options)
- def box(*args, **options)
- args.each { |name| column(name, :box, options) }
- end
+ ##
+ # :method: lseg
+ # :call-seq: lseg(*names, **options)
- def path(*args, **options)
- args.each { |name| column(name, :path, options) }
- end
+ ##
+ # :method: box
+ # :call-seq: box(*names, **options)
- def polygon(*args, **options)
- args.each { |name| column(name, :polygon, options) }
- end
+ ##
+ # :method: path
+ # :call-seq: path(*names, **options)
- def circle(*args, **options)
- args.each { |name| column(name, :circle, options) }
- end
+ ##
+ # :method: polygon
+ # :call-seq: polygon(*names, **options)
- def serial(*args, **options)
- args.each { |name| column(name, :serial, options) }
- end
+ ##
+ # :method: circle
+ # :call-seq: circle(*names, **options)
- def tsrange(*args, **options)
- args.each { |name| column(name, :tsrange, options) }
- end
+ ##
+ # :method: serial
+ # :call-seq: serial(*names, **options)
- def tstzrange(*args, **options)
- args.each { |name| column(name, :tstzrange, options) }
- end
+ ##
+ # :method: tsrange
+ # :call-seq: tsrange(*names, **options)
- def tsvector(*args, **options)
- args.each { |name| column(name, :tsvector, options) }
- end
+ ##
+ # :method: tstzrange
+ # :call-seq: tstzrange(*names, **options)
- def uuid(*args, **options)
- args.each { |name| column(name, :uuid, options) }
- end
+ ##
+ # :method: tsvector
+ # :call-seq: tsvector(*names, **options)
- def xml(*args, **options)
- args.each { |name| column(name, :xml, options) }
- end
- end
+ ##
+ # :method: uuid
+ # :call-seq: uuid(*names, **options)
+
+ ##
+ # :method: xml
+ # :call-seq: xml(*names, **options)
- class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
- attr_accessor :array
+ included do
+ define_column_methods :bigserial, :bit, :bit_varying, :cidr, :citext, :daterange,
+ :hstore, :inet, :interval, :int4range, :int8range, :jsonb, :ltree, :macaddr,
+ :money, :numrange, :oid, :point, :line, :lseg, :box, :path, :polygon, :circle,
+ :serial, :tsrange, :tstzrange, :tsvector, :uuid, :xml
+ end
end
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
index 1047ba8cac..84643d20da 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
@@ -1,46 +1,49 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
- module ColumnDumper
- def column_spec_for_primary_key(column)
- spec = super.except!(:null)
- if schema_type(column) == :uuid
- spec[:default] ||= 'nil'
- end
- spec
- end
+ class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
+ private
- # Adds +:array+ option to the default set
- def prepare_column_options(column)
- spec = super
- spec[:array] = 'true' if column.array?
- spec
- end
+ 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
- # Adds +:array+ as a valid migration key
- def migration_keys
- super + [:array]
- end
+ def prepare_column_options(column)
+ spec = super
+ spec[:array] = "true" if column.array?
+ spec
+ end
- private
+ def default_primary_key?(column)
+ schema_type(column) == :bigserial
+ end
- def default_primary_key?(column)
- schema_type(column) == :serial
- 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?
+ def schema_type(column)
+ return super unless column.serial?
- if column.bigint?
- :bigserial
- else
- :serial
+ if column.bigint?
+ :bigserial
+ else
+ :serial
+ end
end
- end
- def schema_expression(column)
- super unless column.serial?
- end
+ def schema_expression(column)
+ super unless column.serial?
+ 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 ca2a41b136..a38c1325c0 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)
-
- 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
- ""
+ options = { encoding: "utf8" }.merge!(options.symbolize_keys)
+
+ option_string = options.each_with_object(+"") do |(key, value), memo|
+ 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
+ ""
end
end
@@ -68,119 +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.
- def tables(name = nil)
- if name
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing arguments to #tables is deprecated without replacement.
- MSG
- end
-
- select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA')
- end
-
- def data_sources # :nodoc
- select_values(<<-SQL, 'SCHEMA')
- SELECT c.relname
- FROM pg_class c
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
- WHERE c.relkind IN ('r', 'v','m') -- (r)elation/table, (v)iew, (m)aterialized view
- AND n.nspname = ANY (current_schemas(false))
- SQL
- end
-
- # Returns true if table exists.
- # If the schema is not specified as part of +name+ then it will only find tables within
- # the current schema search path (regardless of permissions to access tables in other schemas)
- def table_exists?(name)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- #table_exists? currently checks both tables and views.
- This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
- Use #data_source_exists? instead.
- MSG
-
- data_source_exists?(name)
- end
-
- def data_source_exists?(name)
- name = Utils.extract_schema_qualified_name(name.to_s)
- return false unless name.identifier
-
- select_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 views # :nodoc:
- select_values(<<-SQL, 'SCHEMA')
- SELECT c.relname
- FROM pg_class c
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
- WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
- AND n.nspname = ANY (current_schemas(false))
- SQL
- end
-
- def view_exists?(view_name) # :nodoc:
- name = Utils.extract_schema_qualified_name(view_name.to_s)
- return false unless name.identifier
-
- select_values(<<-SQL, 'SCHEMA').any?
- SELECT c.relname
- FROM pg_class c
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
- WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
- AND c.relname = '#{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)
- table = Utils.extract_schema_qualified_name(table_name.to_s)
- index = Utils.extract_schema_qualified_name(index_name.to_s)
+ def index_name_exists?(table_name, index_name)
+ table = quoted_scope(table_name)
+ index = quoted_scope(index_name)
- select_value(<<-SQL, 'SCHEMA').to_i > 0
+ 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.identifier}'
- AND t.relname = '#{table.identifier}'
- AND n.nspname = #{index.schema ? "'#{index.schema}'" : 'ANY (current_schemas(false))'}
+ AND i.relname = #{index[:name]}
+ AND t.relname = #{table[:name]}
+ AND n.nspname = #{index[:schema]}
SQL
end
# Returns an array of indexes for the given table.
- def indexes(table_name, name = nil)
- table = Utils.extract_schema_qualified_name(table_name.to_s)
+ def indexes(table_name) # :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
+ 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 = '#{table.identifier}'
- AND n.nspname = #{table.schema ? "'#{table.schema}'" : 'ANY (current_schemas(false))'}
+ AND t.relname = #{scope[:name]}
+ AND n.nspname = #{scope[:schema]}
ORDER BY i.relname
SQL
@@ -190,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) # :nodoc:
- table_name = table_name.to_s
- 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, table_name, 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, null, table_name, default_function = nil, collation = nil) # :nodoc:
- PostgreSQLColumn.new(name, default, sql_type_metadata, null, table_name, 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_.*'
@@ -266,7 +206,7 @@ module ActiveRecord
end
# Creates a schema for the given schema name.
- def create_schema schema_name
+ def create_schema(schema_name)
execute "CREATE SCHEMA #{quote_schema_name(schema_name)}"
end
@@ -277,42 +217,42 @@ module ActiveRecord
# 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.
@@ -323,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
@@ -340,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
@@ -356,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(<<~SQL, "SCHEMA")[0]
SELECT attr.attname, nsp.nspname, seq.relname
FROM pg_class seq,
pg_attribute attr,
@@ -372,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
- end_sql
+ AND dep.refobjid = #{quote(quote_table_name(table))}::regclass
+ SQL
- if result.nil? or result.empty?
- result = query(<<-end_sql, 'SCHEMA')[0]
+ if result.nil? || result.empty?
+ result = query(<<~SQL, "SCHEMA")[0]
SELECT attr.attname, nsp.nspname,
CASE
WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
@@ -390,10 +336,10 @@ 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
+ SQL
end
pk = result.shift
@@ -407,20 +353,46 @@ module ActiveRecord
end
def primary_keys(table_name) # :nodoc:
- select_values(<<-SQL.strip_heredoc, 'SCHEMA')
- WITH pk_constraint AS (
- SELECT conrelid, unnest(conkey) AS connum FROM pg_constraint
- WHERE contype = 'p'
- AND conrelid = '#{quote_table_name(table_name)}'::regclass
- ), cons AS (
- SELECT conrelid, connum, row_number() OVER() AS rownum FROM pk_constraint
- )
- SELECT attr.attname FROM pg_attribute attr
- INNER JOIN cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.connum
- ORDER BY cons.rownum
+ 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.
# Also renames a table's primary key sequence if the sequence name exists and
# matches the Active Record default.
@@ -431,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 #{seq.quoted} 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.
@@ -502,8 +462,10 @@ module ActiveRecord
end
def add_index(table_name, column_name, options = {}) #:nodoc:
- index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
- execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}"
+ index_name, index_type, index_columns_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, options = {}) #:nodoc:
@@ -539,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
@@ -548,94 +511,269 @@ 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}. The limit on binary can be at most 1GB - 1byte."
+ 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, "No text type has byte size #{limit}. 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 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)
+ 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
+
+ # 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, to_table = nil, **options)
+ return unless supports_validate_constraints?
+
+ fk_name_to_validate = foreign_key_for!(from_table, to_table: to_table, **options).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(self, *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 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 = {})
+ td = create_table_definition(table_name)
+ cd = td.new_column_definition(column_name, type, options)
+ sqls = [schema_creation.accept(ChangeColumnDefinition.new(cd, column_name))]
+ sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment)
+ sqls
+ end
+
+ def change_column_default_for_alter(table_name, column_name, default_or_changes)
+ 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)
+ "ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
+ end
+
+ def add_timestamps_for_alter(table_name, options = {})
+ options[:null] = false if options[:null].nil?
+
+ if !options.key?(:precision) && supports_datetime_with_precision?
+ options[:precision] = 6
+ end
+
+ [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
+ end
+
+ 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 b2c49989a4..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,11 +13,11 @@ 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
- super.gsub(/\[\]$/, "".freeze)
+ super.gsub(/\[\]$/, "")
end
def ==(other)
@@ -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..f2f4701500 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:
#
@@ -64,7 +68,7 @@ module ActiveRecord
# * <tt>"schema_name".table_name</tt>
# * <tt>"schema.name"."table name"</tt>
def extract_schema_qualified_name(string)
- schema, table = string.scan(/[^".\s]+|"[^"]*"/)
+ schema, table = string.scan(/[^".]+|"[^"]*"/)
if table.nil?
table = schema
schema = nil
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 6497b1cc31..29f764e8f4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1,22 +1,31 @@
+# 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:
@@ -30,13 +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.
- valid_conn_param_keys = PGconn.conndefaults_hash.keys + [:requiressl]
+ # Forward only valid config params to PG::Connection.connect.
+ valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl]
conn_params.slice!(*valid_conn_param_keys)
- # The postgres drivers don't allow the creation of an unconnected PGconn object,
- # 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
@@ -63,19 +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",
+ 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" },
@@ -110,6 +137,8 @@ module ActiveRecord
bit: { name: "bit" },
bit_varying: { name: "bit varying" },
money: { name: "money" },
+ interval: { name: "interval" },
+ oid: { name: "oid" },
}
OID = PostgreSQL::OID #:nodoc:
@@ -118,16 +147,8 @@ module ActiveRecord
include PostgreSQL::ReferentialIntegrity
include PostgreSQL::SchemaStatements
include PostgreSQL::DatabaseStatements
- include PostgreSQL::ColumnDumper
- include Savepoints
- def schema_creation # :nodoc:
- PostgreSQL::SchemaCreation.new self
- end
-
- # Returns true, since this connection adapter supports prepared statement
- # caching.
- def supports_statement_cache?
+ def supports_bulk_alter?
true
end
@@ -139,6 +160,10 @@ module ActiveRecord
true
end
+ def supports_expression_index?
+ true
+ end
+
def supports_transaction_isolation?
true
end
@@ -147,6 +172,10 @@ module ActiveRecord
true
end
+ def supports_validate_constraints?
+ true
+ end
+
def supports_views?
true
end
@@ -156,14 +185,33 @@ module ActiveRecord
end
def supports_json?
- postgresql_version >= 90200
+ true
+ end
+
+ def supports_comments?
+ true
end
+ def supports_savepoints?
+ true
+ end
+
+ def supports_insert_returning?
+ true
+ end
+
+ def supports_insert_on_conflict?
+ postgresql_version >= 90500
+ end
+ alias supports_insert_on_duplicate_skip? supports_insert_on_conflict?
+ alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
+ alias supports_insert_conflict_target? supports_insert_on_conflict?
+
def index_algorithms
- { concurrently: 'CONCURRENTLY' }
+ { concurrently: "CONCURRENTLY" }
end
- class StatementPool < ConnectionAdapters::StatementPool
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
def initialize(connection, max)
super(max)
@connection = connection
@@ -179,14 +227,14 @@ 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
@@ -195,94 +243,75 @@ module ActiveRecord
def initialize(connection, logger, connection_parameters, config)
super(connection, logger, config)
- @visitor = Arel::Visitors::PostgreSQL.new self
- if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
- @prepared_statements = true
- @visitor.extend(DetermineIfPreparableVisitor)
- else
- @prepared_statements = false
- end
-
@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 < 90100
- raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1."
- end
-
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
- end
-
- def truncate(table_name, name = nil)
- exec_query "TRUNCATE TABLE #{quote_table_name(table_name)}", name, []
- end
-
# 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
+ rescue PG::ConnectionBad
+ connect
+ 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
- end
-
- def native_database_types #:nodoc:
- NATIVE_DATABASE_TYPES
+ @lock.synchronize do
+ super
+ @connection.close rescue nil
+ end
end
- # Returns true, since this connection adapter supports migrations.
- def supports_migrations?
- true
+ def discard! # :nodoc:
+ @connection.socket_io.reopen(IO::NULL) rescue nil
+ @connection = nil
end
- # Does PostgreSQL support finding primary key on non-Active Record tables?
- def supports_primary_key? #:nodoc:
- true
+ def native_database_types #:nodoc:
+ NATIVE_DATABASE_TYPES
end
def set_standard_conforming_strings
- execute('SET standard_conforming_strings = on', 'SCHEMA')
+ execute("SET standard_conforming_strings = on", "SCHEMA")
end
def supports_ddl_transactions?
@@ -301,27 +330,46 @@ module ActiveRecord
true
end
- # Range datatypes weren't introduced until PostgreSQL 9.2
def supports_ranges?
- postgresql_version >= 90200
+ true
end
+ deprecate :supports_ranges?
def supports_materialized_views?
- postgresql_version >= 90300
+ true
+ end
+
+ def supports_foreign_tables?
+ true
+ end
+
+ def supports_pgcrypto_uuid?
+ postgresql_version >= 90400
+ end
+
+ def supports_optimizer_hints?
+ unless defined?(@has_pg_hint_plan)
+ @has_pg_hint_plan = extension_available?("pg_hint_plan")
+ end
+ @has_pg_hint_plan
+ 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, "Postgres requires advisory lock ids to be a signed 64 bit integer")
+ raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer")
end
- select_value("SELECT pg_try_advisory_lock(#{lock_id});")
+ query_value("SELECT pg_try_advisory_lock(#{lock_id})")
end
def release_advisory_lock(lock_id) # :nodoc:
unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63
- raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer")
+ raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer")
end
- select_value("SELECT pg_advisory_unlock(#{lock_id})")
+ query_value("SELECT pg_advisory_unlock(#{lock_id})")
end
def enable_extension(name)
@@ -336,50 +384,35 @@ module ActiveRecord
}
end
+ def extension_available?(name)
+ query_value("SELECT true FROM pg_available_extensions WHERE name = #{quote(name)}", "SCHEMA")
+ 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
+ query_value("SELECT installed_version IS NOT NULL FROM pg_available_extensions WHERE name = #{quote(name)}", "SCHEMA")
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
@@ -395,86 +428,130 @@ module ActiveRecord
@connection.server_version
end
- protected
+ def default_index_type?(index) # :nodoc:
+ index.using == :btree || super
+ end
+
+ def build_insert_sql(insert) # :nodoc:
+ sql = +"INSERT #{insert.into} #{insert.values_list}"
- # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html
+ if insert.skip_duplicates?
+ sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING"
+ elsif insert.update_duplicates?
+ sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET "
+ sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",")
+ end
+
+ sql << " RETURNING #{insert.returning}" if insert.returning
+ sql
+ end
+
+ private
+ def check_version
+ if postgresql_version < 90300
+ raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.3."
+ end
+ end
+
+ # 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)
+ def translate_exception(exception, message:, sql:, binds:)
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)
+ RecordNotUnique.new(message, sql: sql, binds: binds)
when FOREIGN_KEY_VIOLATION
- InvalidForeignKey.new(message)
+ InvalidForeignKey.new(message, sql: sql, binds: binds)
+ when VALUE_LIMIT_VIOLATION
+ ValueTooLong.new(message, sql: sql, binds: binds)
+ when NUMERIC_VALUE_OUT_OF_RANGE
+ RangeError.new(message, sql: sql, binds: binds)
+ when NOT_NULL_VIOLATION
+ NotNullViolation.new(message, sql: sql, binds: binds)
+ when SERIALIZATION_FAILURE
+ SerializationFailure.new(message, sql: sql, binds: binds)
+ when DEADLOCK_DETECTED
+ Deadlocked.new(message, sql: sql, binds: binds)
+ when LOCK_NOT_AVAILABLE
+ LockWaitTimeout.new(message, sql: sql, binds: binds)
+ when QUERY_CANCELED
+ QueryCanceled.new(message, sql: sql, binds: binds)
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)
- 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)
-
- # FIXME: why are we keeping these types as strings?
- m.alias_type 'interval', '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)
@@ -494,78 +571,60 @@ 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]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
- # The default 'now'::date is CURRENT_DATE
- if $1 == "now".freeze && $2 == "date".freeze
- nil
- else
- $1.gsub("''".freeze, "'".freeze)
- end
+ 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+\(.*\)|\(.*\)::\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?
- query = <<-SQL
- SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
- FROM pg_type as t
- LEFT JOIN pg_range as r ON oid = rngtypid
- SQL
- else
- query = <<-SQL
- SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype
- FROM pg_type as t
- SQL
- end
+ query = <<~SQL
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
+ FROM pg_type as t
+ LEFT JOIN pg_range as r ON oid = rngtypid
+ SQL
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
@@ -573,6 +632,10 @@ module ActiveRecord
FEATURE_NOT_SUPPORTED = "0A000" #:nodoc:
def execute_and_clear(sql, name, binds, prepare: false)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
if without_prepared_statement?(binds)
result = exec_no_cache(sql, name, [])
elsif !prepare
@@ -586,16 +649,31 @@ module ActiveRecord
end
def exec_no_cache(sql, name, binds)
- type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
- log(sql, name, binds) { @connection.async_exec(sql, type_casted_binds) }
+ materialize_transactions
+
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
+ # made since we established the connection
+ update_typemap_for_default_timezone
+
+ type_casted_binds = type_casted_binds(binds)
+ log(sql, name, binds, type_casted_binds) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @connection.exec_params(sql, type_casted_binds)
+ end
+ end
end
def exec_cache(sql, name, binds)
- stmt_key = prepare_statement(sql)
- type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) }
+ materialize_transactions
+ update_typemap_for_default_timezone
+
+ stmt_key = prepare_statement(sql, binds)
+ 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
raise unless is_cached_plan_failure?(e)
@@ -605,8 +683,10 @@ module ActiveRecord
if in_transaction?
raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
else
- # outside of transactions we can simply flush this query and retry
- @statements.delete sql_key(sql)
+ @lock.synchronize do
+ # outside of transactions we can simply flush this query and retry
+ @statements.delete sql_key(sql)
+ end
retry
end
end
@@ -619,11 +699,11 @@ module ActiveRecord
# ActiveRecord::PreparedStatementCacheExpired
#
# Check here for more details:
- # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
- CACHED_PLAN_HEURISTIC = 'cached plan must not change result type'.freeze
+ # 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(PGresult::PG_DIAG_SQLSTATE)
+ code = pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE)
code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC)
rescue
false
@@ -641,33 +721,31 @@ 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)
+ def prepare_statement(sql, binds)
+ @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, binds)
+ end
+ # Clear the queue
+ @connection.get_last_result
+ @statements[sql_key] = nextkey
end
- # Clear the queue
- @connection.get_last_result
- @statements[sql_key] = nextkey
+ @statements[sql_key]
end
- @statements[sql_key]
end
# Connects to a PostgreSQL server and sets up the adapter depending on the
# connected server's characteristics.
def connect
- @connection = PGconn.connect(@connection_parameters)
+ @connection = PG.connect(@connection_parameters)
configure_connection
- rescue ::PG::Error => error
- if error.message.include?("does not exist")
- raise ActiveRecord::NoDatabaseError
- else
- raise
- end
+ add_pg_encoders
+ add_pg_decoders
end
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -676,43 +754,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 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_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
@@ -727,41 +802,50 @@ 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(<<~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
+ SQL
end
- def extract_table_ref_from_insert_sql(sql) # :nodoc:
+ def extract_table_ref_from_insert_sql(sql)
sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im]
$1.strip if $1
end
- def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
- PostgreSQL::TableDefinition.new(name, temporary, options, as)
+ def arel_visitor
+ Arel::Visitors::PostgreSQL.new(self)
+ end
+
+ def build_statement_pool
+ StatementPool.new(@connection, self.class.type_cast_config_to_integer(@config[:statement_limit]))
end
def can_perform_case_insensitive_comparison_for?(column)
@case_insensitive_cache ||= {}
@case_insensitive_cache[column.sql_type] ||= begin
- sql = <<-end_sql
+ sql = <<~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
+ SQL
execute_and_clear(sql, "SCHEMA", []) do |result|
result.getvalue(0, 0)
end
@@ -773,40 +857,65 @@ 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 update_typemap_for_default_timezone
+ if @default_timezone != ActiveRecord::Base.default_timezone && @timestamp_decoder
+ decoder_class = ActiveRecord::Base.default_timezone == :utc ?
+ PG::TextDecoder::TimestampUtc :
+ PG::TextDecoder::TimestampWithoutTimeZone
+
+ @timestamp_decoder = decoder_class.new(@timestamp_decoder.to_h)
+ @connection.type_map_for_results.add_coder(@timestamp_decoder)
+ @default_timezone = ActiveRecord::Base.default_timezone
+ end
+ end
+
def add_pg_decoders
+ @default_timezone = nil
+ @timestamp_decoder = nil
+
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,
}
+
+ if defined?(PG::TextDecoder::TimestampUtc)
+ # Use native PG encoders available since pg-1.1
+ coders_by_name["timestamp"] = PG::TextDecoder::TimestampUtc
+ coders_by_name["timestamptz"] = PG::TextDecoder::TimestampWithTimeZone
+ end
+
known_coder_types = coders_by_name.keys.map { |n| quote(n) }
- query = <<-SQL % known_coder_types.join(", ")
+ query = <<~SQL % known_coder_types.join(", ")
SELECT t.oid, t.typname
FROM pg_type as t
WHERE t.typname IN (%s)
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
map = PG::TypeMapByOid.new
coders.each { |coder| map.add_coder(coder) }
@connection.type_map_for_results = map
+
+ # extract timestamp decoder for use in update_typemap_for_default_timezone
+ @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
+ update_typemap_for_default_timezone
end
def construct_coder(row, coder_class)
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)
@@ -815,16 +924,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, 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::Rails51Point, adapter: :postgresql)
- ActiveRecord::Type.register(:legacy_point, OID::Point, adapter: :postgresql)
+ ActiveRecord::Type.register(:point, OID::Point, 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 eee142378c..07453b4403 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
@@ -11,6 +13,7 @@ module ActiveRecord
@columns_hash = {}
@primary_keys = {}
@data_sources = {}
+ @indexes = {}
end
def initialize_dup(other)
@@ -19,6 +22,25 @@ module ActiveRecord
@columns_hash = @columns_hash.dup
@primary_keys = @primary_keys.dup
@data_sources = @data_sources.dup
+ @indexes = @indexes.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["indexes"] = @indexes
+ 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"]
+ @indexes = coder["indexes"] || {}
+ @version = coder["version"]
end
def primary_keys(table_name)
@@ -32,9 +54,6 @@ module ActiveRecord
@data_sources[name] = connection.data_source_exists?(name)
end
- alias table_exists? data_source_exists?
- deprecate :table_exists? => "use #data_source_exists? instead"
-
# Add internal cache for table with +table_name+.
def add(table_name)
@@ -42,14 +61,13 @@ module ActiveRecord
primary_keys(table_name)
columns(table_name)
columns_hash(table_name)
+ indexes(table_name)
end
end
def data_sources(name)
@data_sources[name]
end
- alias tables data_sources
- deprecate :tables => "use #data_sources instead"
# Get the columns for a table
def columns(table_name)
@@ -64,17 +82,27 @@ module ActiveRecord
}]
end
+ # Checks whether the columns hash is already cached for a table.
+ def columns_hash?(table_name)
+ @columns_hash.key?(table_name)
+ end
+
+ def indexes(table_name)
+ @indexes[table_name] ||= connection.indexes(table_name)
+ end
+
# Clears out internal caches
def clear!
@columns.clear
@columns_hash.clear
@primary_keys.clear
@data_sources.clear
+ @indexes.clear
@version = nil
end
def size
- [@columns, @columns_hash, @primary_keys, @data_sources].map(&:size).inject :+
+ [@columns, @columns_hash, @primary_keys, @data_sources].sum(&:size)
end
# Clear out internal caches for the data source +name+.
@@ -83,22 +111,21 @@ module ActiveRecord
@columns_hash.delete name
@primary_keys.delete name
@data_sources.delete name
+ @indexes.delete name
end
- alias clear_table_cache! clear_data_source_cache!
- deprecate :clear_table_cache! => "use #clear_data_source_cache! instead"
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
- @version = ActiveRecord::Migrator.current_version
- [@version, @columns, @columns_hash, @primary_keys, @data_sources]
+ @version = connection.migration_context.current_version
+ [@version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes]
end
def marshal_load(array)
- @version, @columns, @columns_hash, @primary_keys, @data_sources = array
+ @version, @columns, @columns_hash, @primary_keys, @data_sources, @indexes = array
+ @indexes = @indexes || {}
end
private
-
def prepare_data_sources
connection.data_sources.each { |source| @data_sources[source] = true }
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/database_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
new file mode 100644
index 0000000000..84dcae49b9
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ module DatabaseStatements
+ private
+ def execute_batch(sql, name = nil)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
+ materialize_transactions
+
+ log(sql, name) do
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
+ @connection.execute_batch(sql)
+ end
+ end
+ end
+
+ def build_fixture_statements(fixture_set)
+ fixture_set.flat_map do |table_name, fixtures|
+ next if fixtures.empty?
+ fixtures.map { |fixture| build_fixture_sql([fixture], table_name) }
+ end.compact
+ end
+
+ def build_truncate_statements(*table_names)
+ truncate_tables = table_names.map do |table_name|
+ "DELETE FROM #{quote_table_name(table_name)}"
+ end
+ combine_multi_statements(truncate_tables)
+ end
+ 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
index a946f5ebd0..832fdfe5c4 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
module SQLite3
@@ -10,7 +12,7 @@ module ActiveRecord
#
def pp(result)
result.rows.map do |row|
- row.join('|')
+ row.join("|")
end.join("\n") + "\n"
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
index faf2f375dc..cb9d32a577 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -1,35 +1,66 @@
+# 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_table_name(name)
+ @quoted_table_names[name] ||= super.gsub(".", "\".\"").freeze
+ end
+
def quote_column_name(name)
@quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}")
end
- private
+ 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 _quote(value)
- if value.is_a?(Type::Binary::Data)
- "x'#{value.hex}'"
- else
- super
- end
+ def quoted_binary(value)
+ "x'#{value.hex}'"
end
- def _type_cast(value)
- case value
- when BigDecimal
- value.to_f
- when String
- if value.encoding == Encoding::ASCII_8BIT
- super(value.encode(Encoding::UTF_8))
+ def quoted_true
+ "1"
+ end
+
+ def unquoted_true
+ 1
+ end
+
+ def quoted_false
+ "0"
+ end
+
+ def unquoted_false
+ 0
+ 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
- else
- super
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..e64e995e1a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -0,0 +1,137 @@
+# 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 add_foreign_key(from_table, to_table, **options)
+ alter_table(from_table) do |definition|
+ to_table = strip_table_name_prefix_and_suffix(to_table)
+ definition.foreign_key(to_table, options)
+ end
+ end
+
+ def remove_foreign_key(from_table, to_table = nil, **options)
+ to_table ||= options[:to_table]
+ options = options.except(:name, :to_table)
+ foreign_keys = foreign_keys(from_table)
+
+ fkey = foreign_keys.detect do |fk|
+ table = to_table || begin
+ table = options[:column].to_s.delete_suffix("_id")
+ Base.pluralize_table_names ? table.pluralize : table
+ end
+ table = strip_table_name_prefix_and_suffix(table)
+ fk_to_table = strip_table_name_prefix_and_suffix(fk.to_table)
+ fk_to_table == table && options.all? { |k, v| fk.options[k].to_s == v.to_s }
+ end || raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table || options}")
+
+ foreign_keys.delete(fkey)
+ alter_table(from_table, foreign_keys)
+ 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(self, *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 5c8e428bef..ff23a525b9 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,15 +1,23 @@
-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'
+# 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/database_statements"
+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", ">= 1.3.6"
+require "sqlite3"
module ActiveRecord
module ConnectionHandling # :nodoc:
def sqlite3_connection(config)
+ config = config.symbolize_keys
+
# Require database.
unless config[:database]
raise ArgumentError, "No database file specified. Missing argument: database"
@@ -18,7 +26,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)
@@ -26,11 +34,9 @@ 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]
-
ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config)
rescue Errno::ENOENT => error
if error.message.include?("No such file or directory")
@@ -49,13 +55,14 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLite3Adapter < AbstractAdapter
- ADAPTER_NAME = 'SQLite'.freeze
+ ADAPTER_NAME = "SQLite"
include SQLite3::Quoting
- include Savepoints
+ include SQLite3::SchemaStatements
+ include SQLite3::DatabaseStatements
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" },
@@ -65,35 +72,30 @@ module ActiveRecord
time: { name: "time" },
date: { name: "date" },
binary: { name: "blob" },
- boolean: { name: "boolean" }
+ boolean: { name: "boolean" },
+ json: { name: "json" },
}
- class StatementPool < ConnectionAdapters::StatementPool
- private
-
- def dealloc(stmt)
- stmt[:stmt].close unless stmt[:stmt].closed?
+ def self.represent_boolean_as_integer=(value) # :nodoc:
+ if value == false
+ raise "`.represent_boolean_as_integer=` is now always true, so make sure your application can work with it and remove this settings."
end
+
+ ActiveSupport::Deprecation.warn(
+ "`.represent_boolean_as_integer=` is now always true, so setting this is deprecated and will be removed in Rails 6.1."
+ )
end
- def schema_creation # :nodoc:
- SQLite3::SchemaCreation.new self
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
+ private
+ def dealloc(stmt)
+ stmt.close unless stmt.closed?
+ end
end
def initialize(connection, logger, connection_options, config)
super(connection, logger, config)
-
- @active = nil
- @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
-
- @visitor = Arel::Visitors::SQLite.new self
-
- if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
- @prepared_statements = true
- @visitor.extend(DetermineIfPreparableVisitor)
- else
- @prepared_statements = false
- end
+ configure_connection
end
def supports_ddl_transactions?
@@ -105,25 +107,18 @@ module ActiveRecord
end
def supports_partial_index?
- sqlite_version >= '3.8.0'
- end
-
- # Returns true, since this connection adapter supports prepared statement
- # caching.
- def supports_statement_cache?
true
end
- # Returns true, since this connection adapter supports migrations.
- def supports_migrations? #:nodoc:
- true
+ def supports_expression_index?
+ sqlite_version >= "3.9.0"
end
- def supports_primary_key? #:nodoc:
+ def requires_reloading?
true
end
- def requires_reloading?
+ def supports_foreign_keys?
true
end
@@ -135,29 +130,39 @@ module ActiveRecord
true
end
+ def supports_json?
+ true
+ end
+
+ def supports_insert_on_conflict?
+ sqlite_version >= "3.24.0"
+ end
+ alias supports_insert_on_duplicate_skip? supports_insert_on_conflict?
+ alias supports_insert_on_duplicate_update? supports_insert_on_conflict?
+ alias supports_insert_conflict_target? supports_insert_on_conflict?
+
def active?
- @active != false
+ !@connection.closed?
+ end
+
+ def reconnect!
+ super
+ connect if @connection.closed?
end
# Disconnects from the database if already connected. Otherwise, this
# method does nothing.
def disconnect!
super
- @active = false
@connection.close rescue nil
end
- # Clears the prepared statements cache.
- def clear_cache!
- @statements.clear
- end
-
def supports_index_sort_order?
true
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
@@ -176,57 +181,79 @@ module ActiveRecord
true
end
- # QUOTING ==================================================
-
- def quote_string(s) #:nodoc:
- @connection.class.quote(s)
+ def supports_lazy_transactions?
+ true
end
- def quote_table_name_for_assignment(table, attr)
- quote_column_name(attr)
+ # REFERENTIAL INTEGRITY ====================================
+
+ def disable_referential_integrity # :nodoc:
+ old_foreign_keys = query_value("PRAGMA foreign_keys")
+ old_defer_foreign_keys = query_value("PRAGMA defer_foreign_keys")
+
+ 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
#--
# DATABASE STATEMENTS ======================================
#++
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :explain, :select, :pragma, :release, :savepoint, :rollback) # :nodoc:
+ private_constant :READ_QUERY
+
+ def write_query?(sql) # :nodoc:
+ !READ_QUERY.match?(sql)
+ end
+
def explain(arel, binds = [])
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
- SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', []))
+ SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", []))
end
def exec_query(sql, name = nil, binds = [], prepare: false)
- 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
- unless prepare
- stmt = @connection.prepare(sql)
- begin
- cols = stmt.columns
- unless without_prepared_statement?(binds)
- stmt.bind_params(type_casted_binds)
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
+
+ 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
@@ -237,127 +264,36 @@ module ActiveRecord
end
def execute(sql, name = nil) #:nodoc:
- log(sql, name) { @connection.execute(sql) }
- end
+ if preventing_writes? && write_query?(sql)
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
+ end
- def select_rows(sql, name = nil, binds = [])
- exec_query(sql, name, binds).rows
+ materialize_transactions
+
+ 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) # :nodoc:
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- #tables currently returns both tables and views.
- This behavior is deprecated and will be changed with Rails 5.1 to only return tables.
- Use #data_sources instead.
- MSG
-
- if name
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing arguments to #tables is deprecated without replacement.
- MSG
- end
-
- data_sources
- end
-
- def data_sources
- select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", 'SCHEMA')
- end
-
- def table_exists?(table_name)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- #table_exists? currently checks both tables and views.
- This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
- Use #data_source_exists? instead.
- MSG
-
- data_source_exists?(table_name)
- end
-
- def data_source_exists?(table_name)
- return false unless table_name.present?
-
- sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'"
- sql << " AND name = #{quote(table_name)}"
-
- select_values(sql, 'SCHEMA').any?
- end
-
- def views # :nodoc:
- select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", 'SCHEMA')
- end
-
- def view_exists?(view_name) # :nodoc:
- return false unless view_name.present?
-
- sql = "SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'"
- sql << " AND name = #{quote(view_name)}"
-
- select_values(sql, 'SCHEMA').any?
- end
-
- # Returns an array of +Column+ objects for the table specified by +table_name+.
- def columns(table_name) # :nodoc:
- table_name = table_name.to_s
- table_structure(table_name).map do |field|
- case field["dflt_value"]
- when /^null$/i
- 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, table_name, 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_keys(table_name) # :nodoc:
- pks = table_structure(table_name).select { |f| f['pk'] > 0 }
- pks.sort_by { |f| f['pk'] }.map { |f| f['name'] }
+ 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, options = {}) #:nodoc:
@@ -374,25 +310,22 @@ 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
- end
-
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
def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc:
alter_table(table_name) do |definition|
definition.remove_column column_name
+ definition.foreign_keys.delete_if do |_, fk_options|
+ fk_options[:column] == column_name.to_s
+ end
end
end
@@ -415,14 +348,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
@@ -430,52 +362,123 @@ 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 build_insert_sql(insert) # :nodoc:
+ sql = +"INSERT #{insert.into} #{insert.values_list}"
+
+ if insert.skip_duplicates?
+ sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING"
+ elsif insert.update_duplicates?
+ sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET "
+ sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",")
+ end
+
+ sql
+ 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
- def alter_table(table_name, options = {}) #:nodoc:
+ # 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, foreign_keys = foreign_keys(table_name), **options)
altered_table_name = "a#{table_name}"
- caller = lambda {|definition| yield definition if block_given?}
+
+ caller = lambda do |definition|
+ rename = options[:rename] || {}
+ foreign_keys.each do |fk|
+ if column = rename[fk.options[:column]]
+ fk.options[:column] = column
+ end
+ to_table = strip_table_name_prefix_and_suffix(fk.to_table)
+ definition.foreign_key(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] || {})
@@ -484,7 +487,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}"
@@ -493,89 +496,132 @@ 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)
+ def translate_exception(exception, message:, sql:, binds:)
case exception.message
# SQLite 3.8.2 returns a newly formatted error message:
# UNIQUE constraint failed: *table_name*.*column_name*
# Older versions of SQLite return:
# column *column_name* is not unique
when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/
- RecordNotUnique.new(message)
+ RecordNotUnique.new(message, sql: sql, binds: binds)
+ when /.* may not be NULL/, /NOT NULL constraint failed: .*/
+ NotNullViolation.new(message, sql: sql, binds: binds)
+ when /FOREIGN KEY constraint failed/i
+ InvalidForeignKey.new(message, sql: sql, binds: binds)
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 build_statement_pool
+ StatementPool.new(self.class.type_cast_config_to_integer(@config[:statement_limit]))
+ end
+
+ def connect
+ @connection = ::SQLite3::Database.new(
+ @config[:database].to_s,
+ @config.merge(results_as_hash: true)
+ )
+ configure_connection
+ end
+
+ def configure_connection
+ @connection.busy_timeout(self.class.type_cast_config_to_integer(@config[:timeout])) if @config[:timeout]
+
+ 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 57463dd749..46bd831da7 100644
--- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
@@ -1,11 +1,15 @@
+# frozen_string_literal: true
+
module ActiveRecord
module ConnectionAdapters
- class StatementPool
+ class StatementPool # :nodoc:
include Enumerable
- def initialize(max = 1000)
- @cache = Hash.new { |h,pid| h[pid] = {} }
- @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)
@@ -25,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
@@ -45,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 a8b3d03ba5..53069cd899 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
@@ -44,40 +46,143 @@ module ActiveRecord
#
# 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
+ config_hash = resolve_config_for_connection(database)
+ handler = lookup_connection_handler(role)
+
+ 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
+
+ # Returns true if role is the current connected role.
+ #
+ # ActiveRecord::Base.connected_to(role: :writing) do
+ # ActiveRecord::Base.connected_to?(role: :writing) #=> true
+ # ActiveRecord::Base.connected_to?(role: :reading) #=> false
+ # end
+ def connected_to?(role:)
+ current_role == role.to_sym
+ end
+
+ # Returns the symbol representing the current connected role.
+ #
+ # ActiveRecord::Base.connected_to(role: :writing) do
+ # ActiveRecord::Base.current_role #=> :writing
+ # end
+ #
+ # ActiveRecord::Base.connected_to(role: :reading) do
+ # ActiveRecord::Base.current_role #=> :reading
+ # end
+ def current_role
+ connection_handlers.key(connection_handler)
+ 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
- private
- def config
- @raw_config.dup.tap do |cfg|
- if url = ENV['DATABASE_URL']
- cfg[@env] ||= {}
- cfg[@env]["url"] ||= url
- end
- end
+ 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
+
+ # Clears the query cache for all connections associated with the current thread.
+ def clear_query_caches_for_current_thread
+ ActiveRecord::Base.connection_handlers.each_value do |handler|
+ handler.connection_pool_list.each do |pool|
+ pool.connection.clear_query_cache if pool.active_connection?
end
+ end
end
# Returns the connection currently associated with the class. This can
@@ -87,12 +192,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 +213,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 +242,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 c8343dd97f..eb4b48bc37 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,32 +71,37 @@ 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 on query limit or order being
+ # Specifies if an error should be raised if the query has an order being
# ignored when doing batch queries. Useful in applications where the
- # limit or scope being ignored is error-worthy, rather than a warning.
- mattr_accessor :error_on_ignored_order_or_limit, instance_writer: false
- self.error_on_ignored_order_or_limit = false
+ # 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
+ mattr_accessor :database_selector, instance_writer: false
##
# :singleton-method:
# Specifies which database schemas to dump when calling db:structure:dump.
@@ -100,8 +109,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:
@@ -110,33 +118,35 @@ 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: {}
+
+ mattr_accessor :writing_role, instance_accessor: false, default: :writing
+
+ mattr_accessor :reading_role, instance_accessor: false, default: :reading
+
class_attribute :default_connection_handler, instance_writer: false
+ self.filter_attributes = []
+
def self.connection_handler
- ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
+ Thread.current.thread_variable_get("ar_connection_handler") || default_connection_handler
end
def self.connection_handler=(handler)
- ActiveRecord::RuntimeRegistry.connection_handler = handler
+ Thread.current.thread_variable_set("ar_connection_handler", handler)
end
self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
end
module ClassMethods
- def allocate
- define_attribute_methods
- super
- end
-
def initialize_find_by_cache # :nodoc:
- @find_by_statement_cache = { true => {}.extend(Mutex_m), false => {}.extend(Mutex_m) }
+ @find_by_statement_cache = { true => Concurrent::Map.new, false => Concurrent::Map.new }
end
def inherited(child_class) # :nodoc:
@@ -151,41 +161,34 @@ 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.key?(inheritance_column) && !base_class?
+
+ 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.new("Couldn't find #{name} with '#{primary_key}'=#{id}",
name, primary_key, id)
end
record
- 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? ||
+ columns_hash.key?(inheritance_column) && !base_class?
hash = args.first
- return super if hash.values.any? { |v|
- v.nil? || Array === v || Hash === v || Relation === v
+ return super if !(Hash === hash) || hash.values.any? { |v|
+ StatementCache.unsupported_value?(v)
}
# We can't cache Post.find_by(author: david) ...yet
@@ -200,32 +203,44 @@ module ActiveRecord
where(wheres).limit(1)
}
begin
- statement.execute(hash.values, self, connection).first
+ 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}", 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?
@@ -233,37 +248,27 @@ 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
- end
-
def arel_attribute(name, table = arel_table) # :nodoc:
name = attribute_alias(name) if attribute_alias?(name)
table[name]
@@ -277,28 +282,31 @@ module ActiveRecord
TypeCaster::Map.new(self)
end
+ def _internal? # :nodoc:
+ false
+ end
+
private
- def cached_find_by_statement(key, &block) # :nodoc:
- cache = @find_by_statement_cache[connection.prepared_statements]
- cache[key] || cache.synchronize {
- 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? && !ignore_default_scope?
- 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
@@ -310,8 +318,8 @@ module ActiveRecord
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
def initialize(attributes = nil)
- @attributes = self.class._default_attributes.deep_dup
self.class.define_attribute_methods
+ @attributes = self.class._default_attributes.deep_dup
init_internals
initialize_internals_callback
@@ -336,16 +344,26 @@ 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.
+ def init_with_attributes(attributes, new_record = false) # :nodoc:
init_internals
- @new_record = coder['new_record']
+ @new_record = new_record
+ @attributes = attributes
self.class.define_attribute_methods
+ yield self if block_given?
+
_run_find_callbacks
_run_initialize_callbacks
@@ -385,8 +403,10 @@ module ActiveRecord
_run_initialize_callbacks
- @new_record = true
- @destroyed = false
+ @new_record = true
+ @destroyed = false
+ @_start_transaction_state = {}
+ @transaction_state = nil
super
end
@@ -404,11 +424,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+
@@ -432,7 +450,7 @@ module ActiveRecord
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
def hash
if id
- id.hash
+ self.class.hash ^ id.hash
else
super
end
@@ -454,12 +472,20 @@ 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
end
+ def present? # :nodoc:
+ true
+ end
+
+ def blank? # :nodoc:
+ false
+ end
+
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
# attributes will be marked as read only since they cannot be saved.
def readonly?
@@ -480,14 +506,22 @@ 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
@@ -497,65 +531,75 @@ module ActiveRecord
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 1b6817554d..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,11 +47,17 @@ 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
@@ -55,6 +71,9 @@ module ActiveRecord
# * +id+ - The id of the object you wish to update a counter on or an array of ids.
# * +counters+ - A Hash containing the names of the fields
# to update as keys and the amount to update the field by as values.
+ # * <tt>:touch</tt> option - Touch timestamp columns when updating.
+ # If attribute names are passed, they are updated along with updated_at/on
+ # attributes.
#
# ==== Examples
#
@@ -73,14 +92,17 @@ 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.
@@ -94,13 +116,20 @@ module ActiveRecord
#
# * +counter_name+ - The name of the field that should be incremented.
# * +id+ - The id of the object that should be incremented or an array of ids.
+ # * <tt>:touch</tt> - Touch timestamp columns when updating.
+ # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
+ # touch that column or an array of symbols to touch just those ones.
#
# ==== Examples
#
# # Increment the posts_count column for the record with an id of 5
# DiscussionBoard.increment_counter(:posts_count, 5)
- def increment_counter(counter_name, id)
- update_counters(id, counter_name => 1)
+ #
+ # # Increment the posts_count column for the record with an id of 5
+ # # and update the updated_at value.
+ # DiscussionBoard.increment_counter(:posts_count, 5, touch: true)
+ def increment_counter(counter_name, id, touch: nil)
+ update_counters(id, counter_name => 1, touch: touch)
end
# Decrement a numeric field by one, via a direct SQL update.
@@ -112,26 +141,29 @@ module ActiveRecord
#
# * +counter_name+ - The name of the field that should be decremented.
# * +id+ - The id of the object that should be decremented or an array of ids.
+ # * <tt>:touch</tt> - Touch timestamp columns when updating.
+ # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to
+ # touch that column or an array of symbols to touch just those ones.
#
# ==== Examples
#
# # Decrement the posts_count column for the record with an id of 5
# DiscussionBoard.decrement_counter(:posts_count, 5)
- def decrement_counter(counter_name, id)
- update_counters(id, counter_name => -1)
+ #
+ # # Decrement the posts_count column for the record with an id of 5
+ # # and update the updated_at value.
+ # DiscussionBoard.decrement_counter(:posts_count, 5, touch: true)
+ def decrement_counter(counter_name, id, touch: nil)
+ update_counters(id, counter_name => -1, touch: touch)
end
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..44b5cfc738
--- /dev/null
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -0,0 +1,204 @@
+# 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 <tt>include_replicas: true</tt>.
+ #
+ # 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 (i.e. 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)
+ return configs if configs.is_a?(Array)
+
+ build_db_config = configs.each_pair.flat_map do |env_name, config|
+ walk_configs(env_name.to_s, "primary", config)
+ end.flatten.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)
+ 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
+
+ def build_db_config_from_hash(env_name, spec_name, config)
+ if config.has_key?("url")
+ url = config["url"]
+ config_without_url = config.dup
+ config_without_url.delete "url"
+
+ ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url)
+ elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String })
+ ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
+ 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(config.env_name, 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)
+ case method
+ when :each, :first
+ throw_getter_deprecation(method)
+ configurations.send(method, *args, &blk)
+ when :fetch
+ throw_getter_deprecation(method)
+ configs_for(env_name: args.first)
+ when :values
+ throw_getter_deprecation(method)
+ configurations.map(&:config)
+ when :[]=
+ throw_setter_deprecation(method)
+
+ env_name = args[0]
+ config = args[1]
+
+ remaining_configs = configurations.reject { |db_config| db_config.env_name == env_name }
+ new_config = build_configs(env_name => config)
+ new_configs = remaining_configs + new_config
+
+ ActiveRecord::Base.configurations = new_configs
+ else
+ raise NotImplementedError, "`ActiveRecord::Base.configurations` in Rails 6 now returns an object instead of a hash. The `#{method}` method is not supported. Please use `configs_for` or consult the documentation for supported methods."
+ end
+ end
+
+ def throw_setter_deprecation(method)
+ ActiveSupport::Deprecation.warn("Setting `ActiveRecord::Base.configurations` with `#{method}` is deprecated. Use `ActiveRecord::Base.configurations=` directly to set the configurations instead.")
+ end
+
+ def throw_getter_deprecation(method)
+ ActiveSupport::Deprecation.warn("`ActiveRecord::Base.configurations` no longer returns a hash. Methods that act on the hash like `#{method}` are deprecated and will be removed in Rails 6.1. Use the `configs_for` method to collect and iterate over the database configurations.")
+ 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..e31ff09391
--- /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
+ #
+ # * <tt>:env_name</tt> - The Rails environment, i.e. "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..e2d30ae416
--- /dev/null
+++ b/activerecord/lib/active_record/database_configurations/url_config.rb
@@ -0,0 +1,79 @@
+# 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
+ #
+ # * <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_url_hash(url)
+ if url.nil? || /^jdbc:/.match?(url)
+ { "url" => url }
+ else
+ ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash
+ end
+ end
+
+ def build_config(original_config, url)
+ hash = build_url_hash(url)
+
+ if original_config[env_name]
+ original_config[env_name].merge(hash)
+ 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 7be332fb97..8077630aeb 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,
@@ -29,7 +31,9 @@ module ActiveRecord
# as well. With the above example:
#
# Conversation.active
+ # Conversation.not_active
# Conversation.archived
+ # Conversation.not_archived
#
# Of course, you can also query them directly if the scopes don't fit your
# needs:
@@ -95,8 +99,7 @@ module ActiveRecord
module Enum
def self.extended(base) # :nodoc:
- base.class_attribute(:defined_enums, instance_writer: false)
- base.defined_enums = {}
+ base.class_attribute(:defined_enums, instance_writer: false, default: {})
end
def inherited(base) # :nodoc:
@@ -105,6 +108,8 @@ module ActiveRecord
end
class EnumType < Type::Value # :nodoc:
+ delegate :type, to: :subtype
+
def initialize(name, mapping, subtype)
@name = name
@mapping = mapping
@@ -138,34 +143,37 @@ module ActiveRecord
end
end
- protected
-
- attr_reader :name, :mapping, :subtype
+ private
+ attr_reader :name, :mapping, :subtype
end
def enum(definitions)
klass = self
enum_prefix = definitions.delete(:_prefix)
enum_suffix = definitions.delete(:_suffix)
+ enum_scopes = definitions.delete(:_scopes)
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 }
+ detect_enum_conflict!(name, name.pluralize, true)
+ singleton_class.define_method(name.pluralize) { enum_values }
+ defined_enums[name] = enum_values
detect_enum_conflict!(name, name)
detect_enum_conflict!(name, "#{name}=")
- decorate_attribute_type(name, :enum) do |subtype|
- EnumType.new(name, enum_values, subtype)
+ 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
@@ -177,23 +185,30 @@ 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 }
- klass.send(:detect_enum_conflict!, name, value_method_name, true)
- klass.scope value_method_name, -> { where(name => value) }
+ # scope :active, -> { where(status: 0) }
+ # scope :not_active, -> { where.not(status: 0) }
+ if enum_scopes != false
+ klass.send(:detect_enum_conflict!, name, value_method_name, true)
+ klass.scope value_method_name, -> { where(attr => value) }
+
+ klass.send(:detect_enum_conflict!, name, "not_#{value_method_name}", true)
+ klass.scope "not_#{value_method_name}", -> { where.not(attr => value) }
+ end
end
end
- defined_enums[name.to_s] = enum_values
+ enum_values.freeze
end
end
@@ -206,25 +221,41 @@ 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_conflict_error(enum_name, method_name, type: 'class')
+ raise_conflict_error(enum_name, method_name, type: "class")
+ elsif klass_method && method_defined_within?(method_name, Relation)
+ raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name)
elsif !klass_method && dangerous_attribute_method?(method_name)
raise_conflict_error(enum_name, method_name)
elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
- raise_conflict_error(enum_name, 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')
+ def raise_conflict_error(enum_name, method_name, type: "instance", source: "Active Record")
raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
enum: enum_name,
- klass: self.name,
+ klass: name,
type: type,
method: method_name,
source: source
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 2ec9bf3d67..60cf9818c1 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.
@@ -44,10 +45,14 @@ module ActiveRecord
# 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).
+ # is given a +nil+ object).
class ConnectionNotEstablished < ActiveRecordError
end
+ # Raised when a write to the database is attempted on a read only connection.
+ class ReadOnlyError < ActiveRecordError
+ end
+
# Raised when Active Record cannot find a record by given id or set of ids.
class RecordNotFound < ActiveRecordError
attr_reader :model, :primary_key, :id
@@ -63,7 +68,7 @@ module ActiveRecord
# 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.
+ # methods when a record is invalid and cannot be saved.
class RecordNotSaved < ActiveRecordError
attr_reader :record
@@ -96,20 +101,13 @@ module ActiveRecord
#
# Wraps the underlying database error as +cause+.
class StatementInvalid < ActiveRecordError
-
- def initialize(message = nil, original_exception = nil)
- if original_exception
- ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \
- "Exceptions will automatically capture the original exception.", caller)
- end
-
+ def initialize(message = nil, sql: nil, binds: nil)
super(message || $!.try(:message))
+ @sql = sql
+ @binds = binds
end
- def original_exception
- ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller)
- cause
- end
+ attr_reader :sql, :binds
end
# Defunct wrapper class kept for compatibility.
@@ -117,14 +115,60 @@ module ActiveRecord
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(
+ message: nil,
+ sql: nil,
+ binds: nil,
+ table: nil,
+ foreign_key: nil,
+ target_table: nil,
+ primary_key: nil,
+ primary_key_column: nil
+ )
+ if table
+ type = primary_key_column.bigint? ? :bigint : primary_key_column.type
+ msg = <<~EOM.squish
+ Column `#{foreign_key}` on table `#{table}` does not match column `#{primary_key}` on `#{target_table}`,
+ which has type `#{primary_key_column.sql_type}`.
+ To resolve this issue, change the type of the `#{foreign_key}` column on `#{table}` to be :#{type}.
+ (For example `t.#{type} :#{foreign_key}`).
+ EOM
+ else
+ msg = <<~EOM.squish
+ 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, sql: sql, binds: binds)
+ end
+ end
+
+ # Raised when a record cannot be inserted or updated because it would violate a not null constraint.
+ class NotNullViolation < StatementInvalid
+ end
+
+ # Raised when a record cannot be inserted or updated because a value too long for a column type.
+ class ValueTooLong < StatementInvalid
+ end
+
+ # Raised when values that executed are out of range.
+ class RangeError < StatementInvalid
+ end
+
# Raised when number of bind variables in statement given to +:condition+ key
# (for example, when using {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method)
# does not match number of expected values supplied.
@@ -139,7 +183,7 @@ module ActiveRecord
class NoDatabaseError < StatementInvalid
end
- # Raised when Postgres returns 'cached plan must not change result type' and
+ # 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
@@ -162,7 +206,6 @@ module ActiveRecord
super("Stale object error.")
end
end
-
end
# Raised when association is being configured improperly or user tries to use
@@ -281,8 +324,65 @@ module ActiveRecord
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 b652932f9c..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
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index 90bcf5a205..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:
@@ -18,10 +20,13 @@ module ActiveRecord
#
# 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 e4a44244e2..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
@@ -24,21 +26,21 @@ module ActiveRecord
end
def model_class
- config_row['model_class']
+ config_row["model_class"]
end
private
def rows
- @rows ||= raw_rows.reject { |fixture_name, _| fixture_name == '_fixture' }
+ @rows ||= raw_rows.reject { |fixture_name, _| fixture_name == "_fixture" }
end
def config_row
@config_row ||= begin
- row = raw_rows.find { |fixture_name, _| fixture_name == '_fixture' }
+ row = raw_rows.find { |fixture_name, _| fixture_name == "_fixture" }
if row
row.last
else
- {'model_class': nil}
+ { 'model_class': nil }
end
end
end
@@ -66,10 +68,13 @@ module ActiveRecord
# Validate our unmarshalled data.
def validate(data)
unless Hash === data || YAML::Omap === data
- raise Fixture::FormatError, 'fixture is not a hash'
+ raise Fixture::FormatError, "fixture is not a hash: #{@file}"
end
- raise Fixture::FormatError unless data.all? { |name, row| Hash === row }
+ invalid = data.reject { |_, row| Hash === row }
+ if invalid.any?
+ raise Fixture::FormatError, "fixture key is not a hash: #{@file}, keys: #{invalid.keys.inspect}"
+ end
data
end
end
diff --git a/activerecord/lib/active_record/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 ed1bbf5dcd..327121a2a2 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,7 +112,7 @@ 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
+ # 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)
@@ -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:
#
@@ -126,7 +150,7 @@ module ActiveRecord
# unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module
# 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
@@ -415,180 +439,186 @@ module ActiveRecord
# possibly in a folder with the same name.
#++
- MAX_ID = 2 ** 30 - 1
-
- @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
+ MAX_ID = 2**30 - 1
- 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
-
- 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
+ @@all_cached_fixtures = Hash.new { |h, k| h[k] = {} }
- def self.reset_cache
- @@all_cached_fixtures.clear
- end
+ cattr_accessor :all_loaded_fixtures, default: {}
- def self.cache_for_connection(connection)
- @@all_cached_fixtures[connection]
- end
+ class ClassCache
+ def initialize(class_names, config)
+ @class_names = class_names.stringify_keys
+ @config = config
- def self.fixture_is_cached?(connection, table_name)
- cache_for_connection(connection)[table_name]
- 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.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
+ 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
- end
- def self.cache_fixtures(connection, fixtures_map)
- cache_for_connection(connection).update(fixtures_map)
- end
+ private
- def self.instantiate_fixtures(object, fixture_set, load_instances = true)
- if load_instances
- fixture_set.each do |fixture_name, fixture|
- begin
- object.instance_variable_set "@#{fixture_name}", fixture.find
- rescue FixtureClassNotFound
- nil
+ 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
- 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)
- end
+ def default_fixture_model(fs_name, config)
+ ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config)
+ end
end
- cattr_accessor :all_loaded_fixtures
- self.all_loaded_fixtures = {}
+ 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
- class ClassCache
- def initialize(class_names, config)
- @class_names = class_names.stringify_keys
- @config = config
+ 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
- # 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) }
+ def reset_cache
+ @@all_cached_fixtures.clear
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)
- }
+ def cache_for_connection(connection)
+ @@all_cached_fixtures[connection]
end
- private
+ def fixture_is_cached?(connection, table_name)
+ cache_for_connection(connection)[table_name]
+ end
- def insert_class(class_names, name, klass)
- # We only want to deal with AR objects.
- if klass && klass < ActiveRecord::Base
- class_names[name] = klass
+ def cached_fixtures(connection, keys_to_fetch = nil)
+ if keys_to_fetch
+ cache_for_connection(connection).values_at(*keys_to_fetch)
else
- class_names[name] = nil
+ cache_for_connection(connection).values
end
end
- def default_fixture_model(fs_name, config)
- ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config)
+ 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|
+ object.instance_variable_set "@#{fixture_name}", fixture.find
+ rescue FixtureClassNotFound
+ nil
+ end
+ end
+
+ 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
- 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
+ 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
- # FIXME: Apparently JK uses this.
- connection = block_given? ? yield : ActiveRecord::Base.connection
+ # 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)
- }
+ fixture_files_to_read = fixture_set_names.reject do |fs_name|
+ fixture_is_cached?(connection, fs_name)
+ end
- unless files_to_read.empty?
- connection.disable_referential_integrity do
- fixtures_map = {}
+ 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
+
+ # 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
+ Zlib.crc32(label.to_s) % MAX_ID
+ end
+ end
+
+ # Superclass for the evaluation contexts used by ERB fixtures.
+ def context_class
+ @context_class ||= Class.new
+ end
+
+ 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,
+ 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, fs_name))
+ ::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)
+ set.each do |fixture_set|
+ fixture_set.table_rows.each do |table, rows|
+ table_rows_for_connection[table].unshift(*rows)
end
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
- 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
- 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)
+ def update_all_loaded_fixtures(fixtures_map) # :nodoc:
+ all_loaded_fixtures.update(fixtures_map)
+ end
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
@@ -597,18 +627,14 @@ module ActiveRecord
@fixtures = read_fixture_files(path)
- @connection = connection
-
- @table_name = ( model_class.respond_to?(:table_name) ?
- model_class.table_name :
- self.class.default_fixture_table_name(name, config) )
+ @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
@@ -623,152 +649,18 @@ 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
-
- 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
+ fixtures.delete("DEFAULTS")
- 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) }
- }
- 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 model_class=(class_name)
if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
@@ -799,7 +691,6 @@ module ActiveRecord
def yaml_file_path(path)
"#{path}.yml"
end
-
end
class Fixture #:nodoc:
@@ -833,212 +724,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 # :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
- class_attribute :fixture_class_names
- class_attribute :use_transactional_tests
- class_attribute :use_transactional_fixtures
- class_attribute :use_instantiated_fixtures # true, false, or :no_instances
- class_attribute :pre_loaded_fixtures
- class_attribute :config
-
- singleton_class.deprecate 'use_transactional_fixtures=' => 'use use_transactional_tests= instead'
-
- self.fixture_table_names = []
- self.use_instantiated_fixtures = false
- self.pre_loaded_fixtures = false
- self.config = ActiveRecord::Base
-
- self.fixture_class_names = {}
-
- silence_warnings do
- define_singleton_method :use_transactional_tests do
- if use_transactional_fixtures.nil?
- true
- else
- use_transactional_fixtures
- end
- end
- end
- 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 73be4cb271..f77bc2e3c1 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 = "beta3"
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 899683ee4f..9570bc6f86 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,37 +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 has_attribute?(inheritance_column)
- subclass = subclass_from_attributes(attrs)
+ 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 == self
+ if subclass.nil? && base_class?
subclass = subclass_from_attributes(column_defaults)
end
end
if subclass && subclass != self
- subclass.new(*args, &block)
+ subclass.new(attributes, &block)
else
super
end
@@ -103,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.
+ #
+ # Consider the following default behaviour:
#
- # class SuperClass < ActiveRecord::Base
+ # 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.
@@ -129,87 +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? && has_attribute?(inheritance_column)
- end
+ private
- 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)
+ # 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
- compute_type(type_name)
+ super
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}"
+
+ def using_single_table_inheritance?(record)
+ record[inheritance_column].present? && has_attribute?(inheritance_column)
+ 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
- subclass
- end
- def type_condition(table = arel_table)
- sti_column = arel_attribute(inheritance_column, table)
- sti_names = ([self] + descendants).map(&:sti_name)
+ def type_condition(table = arel_table)
+ sti_column = arel_attribute(inheritance_column, table)
+ sti_names = ([self] + descendants).map(&:sti_name)
- sti_column.in(sti_names)
- end
+ predicate_builder.build(sti_column, sti_names)
+ 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
- def subclass_from_attributes(attrs)
- attrs = attrs.to_h if attrs.respond_to?(:permitted?)
- if attrs.is_a?(Hash)
- subclass_name = attrs.with_indifferent_access[inheritance_column]
+ # 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.present?
- find_sti_class(subclass_name)
+ if subclass_name.present?
+ find_sti_class(subclass_name)
+ end
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/insert_all.rb b/activerecord/lib/active_record/insert_all.rb
new file mode 100644
index 0000000000..4b02d40aa0
--- /dev/null
+++ b/activerecord/lib/active_record/insert_all.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ class InsertAll # :nodoc:
+ attr_reader :model, :connection, :inserts, :keys
+ attr_reader :on_duplicate, :returning, :unique_by
+
+ def initialize(model, inserts, on_duplicate:, returning: nil, unique_by: nil)
+ raise ArgumentError, "Empty list of attributes passed" if inserts.blank?
+
+ @model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s).to_set
+ @on_duplicate, @returning, @unique_by = on_duplicate, returning, unique_by
+
+ @returning = (connection.supports_insert_returning? ? primary_keys : false) if @returning.nil?
+ @returning = false if @returning == []
+
+ @unique_by = find_unique_index_for(unique_by) if unique_by
+ @on_duplicate = :skip if @on_duplicate == :update && updatable_columns.empty?
+
+ ensure_valid_options_for_connection!
+ end
+
+ def execute
+ connection.exec_query to_sql, "Bulk Insert"
+ end
+
+ def updatable_columns
+ keys - readonly_columns - unique_by_columns
+ end
+
+ def primary_keys
+ Array(model.primary_key)
+ end
+
+
+ def skip_duplicates?
+ on_duplicate == :skip
+ end
+
+ def update_duplicates?
+ on_duplicate == :update
+ end
+
+ def map_key_with_value
+ inserts.map do |attributes|
+ attributes = attributes.stringify_keys
+ verify_attributes(attributes)
+
+ keys.map do |key|
+ yield key, attributes[key]
+ end
+ end
+ end
+
+ private
+ def find_unique_index_for(unique_by)
+ match = Array(unique_by).map(&:to_s)
+
+ if index = unique_indexes.find { |i| match.include?(i.name) || i.columns == match }
+ index
+ else
+ raise ArgumentError, "No unique index found for #{unique_by}"
+ end
+ end
+
+ def unique_indexes
+ connection.schema_cache.indexes(model.table_name).select(&:unique)
+ end
+
+
+ def ensure_valid_options_for_connection!
+ if returning && !connection.supports_insert_returning?
+ raise ArgumentError, "#{connection.class} does not support :returning"
+ end
+
+ if skip_duplicates? && !connection.supports_insert_on_duplicate_skip?
+ raise ArgumentError, "#{connection.class} does not support skipping duplicates"
+ end
+
+ if update_duplicates? && !connection.supports_insert_on_duplicate_update?
+ raise ArgumentError, "#{connection.class} does not support upsert"
+ end
+
+ if unique_by && !connection.supports_insert_conflict_target?
+ raise ArgumentError, "#{connection.class} does not support :unique_by"
+ end
+ end
+
+
+ def to_sql
+ connection.build_insert_sql(ActiveRecord::InsertAll::Builder.new(self))
+ end
+
+
+ def readonly_columns
+ primary_keys + model.readonly_attributes.to_a
+ end
+
+ def unique_by_columns
+ Array(unique_by&.columns)
+ end
+
+
+ def verify_attributes(attributes)
+ if keys != attributes.keys.to_set
+ raise ArgumentError, "All objects being inserted must have the same keys"
+ end
+ end
+
+
+ class Builder
+ attr_reader :model
+
+ delegate :skip_duplicates?, :update_duplicates?, to: :insert_all
+
+ def initialize(insert_all)
+ @insert_all, @model, @connection = insert_all, insert_all.model, insert_all.connection
+ end
+
+ def into
+ "INTO #{model.quoted_table_name}(#{columns_list})"
+ end
+
+ def values_list
+ types = extract_types_from_columns_on(model.table_name, keys: insert_all.keys)
+
+ values_list = insert_all.map_key_with_value do |key, value|
+ bind = Relation::QueryAttribute.new(key, value, types[key])
+ connection.with_yaml_fallback(bind.value_for_database)
+ end
+
+ Arel::InsertManager.new.create_values_list(values_list).to_sql
+ end
+
+ def returning
+ format_columns(insert_all.returning) if insert_all.returning
+ end
+
+ def conflict_target
+ if index = insert_all.unique_by
+ sql = +"(#{format_columns(index.columns)})"
+ sql << " WHERE #{index.where}" if index.where
+ sql
+ elsif update_duplicates?
+ "(#{format_columns(insert_all.primary_keys)})"
+ end
+ end
+
+ def updatable_columns
+ quote_columns(insert_all.updatable_columns)
+ end
+
+ private
+ attr_reader :connection, :insert_all
+
+ def columns_list
+ format_columns(insert_all.keys)
+ end
+
+ def extract_types_from_columns_on(table_name, keys:)
+ columns = connection.schema_cache.columns_hash(table_name)
+
+ unknown_column = (keys - columns.keys).first
+ raise UnknownAttributeError.new(model.new, unknown_column) if unknown_column
+
+ keys.map { |key| [ key, connection.lookup_cast_type_from_column(columns[key]) ] }.to_h
+ end
+
+ def format_columns(columns)
+ quote_columns(columns).join(",")
+ end
+
+ def quote_columns(columns)
+ columns.map(&connection.method(:quote_column_name))
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 466c8509a4..fa6f0d36ec 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
- self.cache_timestamp_format = :usec
+ 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 +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 a 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,64 @@ 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)
- def cache_key(*timestamp_names)
- case
- when new_record?
+ # Product.cache_versioning = false
+ # Product.find(5).cache_key # => "products/5-20071224150000" (updated_at available)
+ def cache_key
+ 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
+ "#{model_name.cache_key}/#{id}"
+ else
+ timestamp = max_updated_column_timestamp
+
+ 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
+ return unless cache_versioning
+
+ if has_attribute?("updated_at")
+ timestamp = updated_at_before_type_cast
+ if can_use_fast_cache_version?(timestamp)
+ raw_timestamp_to_cache_version(timestamp)
+ elsif timestamp = updated_at
+ timestamp.utc.to_s(cache_timestamp_format)
+ end
+ else
+ if self.class.has_attribute?("updated_at")
+ raise ActiveModel::MissingAttributeError, "missing attribute: updated_at"
+ end
+ 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
@@ -86,7 +130,7 @@ module ActiveRecord
#
# 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 +144,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
@@ -109,5 +153,43 @@ module ActiveRecord
end
end
end
+
+ private
+ # Detects if the value before type cast
+ # can be used to generate a cache_version.
+ #
+ # The fast cache version only works with a
+ # string value directly from the database.
+ #
+ # We also must check if the timestamp format has been changed
+ # or if the timezone is not set to UTC then
+ # we cannot apply our transformations correctly.
+ def can_use_fast_cache_version?(timestamp)
+ timestamp.is_a?(String) &&
+ cache_timestamp_format == :usec &&
+ default_timezone == :utc &&
+ !updated_at_came_from_user?
+ end
+
+ # Converts a raw database string to `:usec`
+ # format.
+ #
+ # Example:
+ #
+ # timestamp = "2018-10-15 20:02:15.266505"
+ # raw_timestamp_to_cache_version(timestamp)
+ # # => "20181015200215266505"
+ #
+ # Postgres truncates trailing zeros,
+ # https://github.com/postgres/postgres/commit/3e1beda2cde3495f41290e1ece5d544525810214
+ # to account for this we pad the output with zeros
+ def raw_timestamp_to_cache_version(timestamp)
+ key = timestamp.delete("- :.")
+ if key.length < 20
+ key.ljust(20, "0")
+ else
+ key
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb
index 81db96bffd..88b0c828ae 100644
--- a/activerecord/lib/active_record/internal_metadata.rb
+++ b/activerecord/lib/active_record/internal_metadata.rb
@@ -1,11 +1,17 @@
-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
# 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 _internal?
+ true
+ end
+
def primary_key
"key"
end
@@ -14,12 +20,8 @@ module ActiveRecord
"#{table_name_prefix}#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}"
end
- def original_table_name
- "#{table_name_prefix}active_record_internal_metadatas#{table_name_suffix}"
- end
-
def []=(key, value)
- first_or_initialize(key: key).update_attributes!(value: value)
+ find_or_initialize_by(key: key).update!(value: value)
end
def [](key)
@@ -27,20 +29,11 @@ module ActiveRecord
end
def table_exists?
- ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) }
- end
-
- def original_table_exists?
- # This method will be removed in Rails 5.1
- # Since it is only necessary when `active_record_internal_metadatas` could exist
- ActiveSupport::Deprecation.silence { connection.table_exists?(original_table_name) }
+ connection.table_exists?(table_name)
end
# Creates an internal metadata table with columns +key+ and +value+
def create_table
- if original_table_exists?
- connection.rename_table(original_table_name, table_name)
- end
unless table_exists?
key_options = connection.internal_string_options_for_primary_key
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/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 2336d23a1c..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
@@ -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 8ecdf76b72..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.7/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 efa2a4df02..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,36 +19,24 @@ 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 = colorize_payload_name(name, payload[:name])
@@ -54,17 +46,30 @@ module ActiveRecord
end
private
+ def type_casted_binds(casted_binds)
+ casted_binds.respond_to?(:call) ? casted_binds.call : casted_binds
+ 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
- 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)
+ [attr && attr.name, value]
end
- end
- def sql_color(sql)
- case sql
+ 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
@@ -81,12 +86,32 @@ module ActiveRecord
CYAN
else
MAGENTA
+ end
end
- end
- def logger
- ActiveRecord::Base.logger
- 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/middleware/database_selector.rb b/activerecord/lib/active_record/middleware/database_selector.rb
new file mode 100644
index 0000000000..b5b5df074c
--- /dev/null
+++ b/activerecord/lib/active_record/middleware/database_selector.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require "active_record/middleware/database_selector/resolver"
+
+module ActiveRecord
+ module Middleware
+ # The DatabaseSelector Middleware provides a framework for automatically
+ # swapping from the primary to the replica database connection. Rails
+ # provides a basic framework to determine when to swap and allows for
+ # applications to write custom strategy classes to override the default
+ # behavior.
+ #
+ # The resolver class defines when the application should switch (i.e. read
+ # from the primary if a write occurred less than 2 seconds ago) and a
+ # resolver context class that sets a value that helps the resolver class
+ # decide when to switch.
+ #
+ # Rails default middleware uses the request's session to set a timestamp
+ # that informs the application when to read from a primary or read from a
+ # replica.
+ #
+ # To use the DatabaseSelector in your application with default settings add
+ # the following options to your environment config:
+ #
+ # config.active_record.database_selector = { delay: 2.seconds }
+ # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
+ # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
+ #
+ # New applications will include these lines commented out in the production.rb.
+ #
+ # The default behavior can be changed by setting the config options to a
+ # custom class:
+ #
+ # config.active_record.database_selector = { delay: 2.seconds }
+ # config.active_record.database_resolver = MyResolver
+ # config.active_record.database_resolver_context = MyResolver::MySession
+ class DatabaseSelector
+ def initialize(app, resolver_klass = Resolver, context_klass = Resolver::Session, options = {})
+ @app = app
+ @resolver_klass = resolver_klass
+ @context_klass = context_klass
+ @options = options
+ end
+
+ attr_reader :resolver_klass, :context_klass, :options
+
+ # Middleware that determines which database connection to use in a multiple
+ # database application.
+ def call(env)
+ request = ActionDispatch::Request.new(env)
+
+ select_database(request) do
+ @app.call(env)
+ end
+ end
+
+ private
+
+ def select_database(request, &blk)
+ context = context_klass.call(request)
+ resolver = resolver_klass.call(context, options)
+
+ if reading_request?(request)
+ resolver.read(&blk)
+ else
+ resolver.write(&blk)
+ end
+ end
+
+ def reading_request?(request)
+ request.get? || request.head?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver.rb b/activerecord/lib/active_record/middleware/database_selector/resolver.rb
new file mode 100644
index 0000000000..80b8cd7cae
--- /dev/null
+++ b/activerecord/lib/active_record/middleware/database_selector/resolver.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require "active_record/middleware/database_selector/resolver/session"
+
+module ActiveRecord
+ module Middleware
+ class DatabaseSelector
+ # The Resolver class is used by the DatabaseSelector middleware to
+ # determine which database the request should use.
+ #
+ # To change the behavior of the Resolver class in your application,
+ # create a custom resolver class that inherits from
+ # DatabaseSelector::Resolver and implements the methods that need to
+ # be changed.
+ #
+ # By default the Resolver class will send read traffic to the replica
+ # if it's been 2 seconds since the last write.
+ class Resolver # :nodoc:
+ SEND_TO_REPLICA_DELAY = 2.seconds
+
+ def self.call(context, options = {})
+ new(context, options)
+ end
+
+ def initialize(context, options = {})
+ @context = context
+ @options = options
+ @delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY
+ @instrumenter = ActiveSupport::Notifications.instrumenter
+ end
+
+ attr_reader :context, :delay, :instrumenter
+
+ def read(&blk)
+ if read_from_primary?
+ read_from_primary(&blk)
+ else
+ read_from_replica(&blk)
+ end
+ end
+
+ def write(&blk)
+ write_to_primary(&blk)
+ end
+
+ private
+
+ def read_from_primary(&blk)
+ ActiveRecord::Base.connection.while_preventing_writes do
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
+ instrumenter.instrument("database_selector.active_record.read_from_primary") do
+ yield
+ end
+ end
+ end
+ end
+
+ def read_from_replica(&blk)
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.reading_role) do
+ instrumenter.instrument("database_selector.active_record.read_from_replica") do
+ yield
+ end
+ end
+ end
+
+ def write_to_primary(&blk)
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
+ instrumenter.instrument("database_selector.active_record.wrote_to_primary") do
+ yield
+ ensure
+ context.update_last_write_timestamp
+ end
+ end
+ end
+
+ def read_from_primary?
+ !time_since_last_write_ok?
+ end
+
+ def send_to_replica_delay
+ delay
+ end
+
+ def time_since_last_write_ok?
+ Time.now - context.last_write_timestamp >= send_to_replica_delay
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb
new file mode 100644
index 0000000000..df7af054b7
--- /dev/null
+++ b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Middleware
+ class DatabaseSelector
+ class Resolver
+ # The session class is used by the DatabaseSelector::Resolver to save
+ # timestamps of the last write in the session.
+ #
+ # The last_write is used to determine whether it's safe to read
+ # from the replica or the request needs to be sent to the primary.
+ class Session # :nodoc:
+ def self.call(request)
+ new(request.session)
+ end
+
+ # Converts time to a timestamp that represents milliseconds since
+ # epoch.
+ def self.convert_time_to_timestamp(time)
+ time.to_i * 1000 + time.usec / 1000
+ end
+
+ # Converts milliseconds since epoch timestamp into a time object.
+ def self.convert_timestamp_to_time(timestamp)
+ timestamp ? Time.at(timestamp / 1000, (timestamp % 1000) * 1000) : Time.at(0)
+ end
+
+ def initialize(session)
+ @session = session
+ end
+
+ attr_reader :session
+
+ def last_write_timestamp
+ self.class.convert_timestamp_to_time(session[:last_write])
+ end
+
+ def update_last_write_timestamp
+ session[:last_write] = self.class.convert_time_to_timestamp(Time.now)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index a5c2985132..997b7f763a 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:
@@ -19,7 +23,7 @@ module ActiveRecord
# t.string :zipcode
# end
#
- # execute <<-SQL
+ # execute <<~SQL
# ALTER TABLE distributors
# ADD CONSTRAINT zipchk
# CHECK (char_length(zipcode) = 5) NO INHERIT;
@@ -37,7 +41,7 @@ module ActiveRecord
# t.string :zipcode
# end
#
- # execute <<-SQL
+ # execute <<~SQL
# ALTER TABLE distributors
# ADD CONSTRAINT zipchk
# CHECK (char_length(zipcode) = 5) NO INHERIT;
@@ -45,7 +49,7 @@ module ActiveRecord
# end
#
# def down
- # execute <<-SQL
+ # execute <<~SQL
# ALTER TABLE distributors
# DROP CONSTRAINT zipchk
# SQL
@@ -64,7 +68,7 @@ module ActiveRecord
#
# reversible do |dir|
# dir.up do
- # execute <<-SQL
+ # execute <<~SQL
# ALTER TABLE distributors
# ADD CONSTRAINT zipchk
# CHECK (char_length(zipcode) = 5) NO INHERIT;
@@ -72,7 +76,7 @@ module ActiveRecord
# end
#
# dir.down do
- # execute <<-SQL
+ # execute <<~SQL
# ALTER TABLE distributors
# DROP CONSTRAINT zipchk
# SQL
@@ -126,9 +130,9 @@ module ActiveRecord
class PendingMigrationError < MigrationError#:nodoc:
def initialize(message = nil)
if !message && defined?(Rails.env)
- super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate RAILS_ENV=#{::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\tbin/rails db:migrate")
+ super("Migrations are pending. To resolve this issue, run:\n\n rails db:migrate")
else
super
end
@@ -136,7 +140,8 @@ module ActiveRecord
end
class ConcurrentMigrationError < MigrationError #:nodoc:
- DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze
+ 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
@@ -145,7 +150,7 @@ module ActiveRecord
class NoEnvironmentInSchemaError < MigrationError #:nodoc:
def initialize
- msg = "Environment data not found in the schema. To resolve this issue, run: \n\n\tbin/rails db:environment:set"
+ 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
@@ -156,8 +161,8 @@ module ActiveRecord
class ProtectedEnvironmentError < ActiveRecordError #:nodoc:
def initialize(env = "production")
- msg = "You are attempting to run a destructive action against your '#{env}' database\n"
- msg << "If you are sure you want to continue, run the same command with the environment variable\n"
+ msg = +"You are attempting to run a destructive action against your '#{env}' database.\n"
+ msg << "If you are sure you want to continue, run the same command with the environment variable:\n"
msg << "DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
super(msg)
end
@@ -165,14 +170,14 @@ module ActiveRecord
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 = +"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 << "\tbin/rails db:environment:set"
+ msg << " rails db:environment:set"
if defined?(Rails.env)
- super("#{msg} RAILS_ENV=#{::Rails.env}")
+ super("#{msg} RAILS_ENV=#{::Rails.env}\n\n")
else
- super(msg)
+ super("#{msg}\n\n")
end
end
end
@@ -276,8 +281,10 @@ module ActiveRecord
#
# * <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)</tt>: Sets a
- # default value for +column_name+ definded by +default+ on +table_name+.
+ # * <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
@@ -301,7 +308,7 @@ module ActiveRecord
# 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
+ # * <tt>remove_foreign_key(from_table, to_table = nil, **options)</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+.
@@ -345,13 +352,13 @@ module ActiveRecord
# <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>rails 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>rails 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,
@@ -509,8 +516,8 @@ 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 :Compatibility, 'active_record/migration/compatibility'
+ 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:
@@ -519,22 +526,19 @@ module ActiveRecord
def self.inherited(subclass) # :nodoc:
super
if subclass.superclass == Migration
- subclass.include Compatibility::Legacy
+ raise StandardError, "Directly inheriting from ActiveRecord::Migration is not supported. " \
+ "Please specify the Rails release the migration was written for:\n" \
+ "\n" \
+ " class #{subclass} < ActiveRecord::Migration[4.2]"
end
end
def self.[](version)
- version = version.to_s
- name = "V#{version.tr('.', '_')}"
- unless Compatibility.const_defined?(name)
- versions = Compatibility.constants.grep(/\AV[0-9_]+\z/).map { |s| s.to_s.delete('V').tr('_', '.').inspect }
- raise ArgumentError, "Unknown migration version #{version.inspect}; expected one of #{versions.sort.join(', ')}"
- end
- Compatibility.const_get(name)
+ Compatibility.find(version)
end
def self.current_version
- Rails.version.to_f
+ ActiveRecord::VERSION::STRING.to_f
end
MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc:
@@ -548,21 +552,19 @@ 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
@@ -575,13 +577,14 @@ module ActiveRecord
# Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending.
def check_pending!(connection = Base.connection)
- raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection)
+ raise ActiveRecord::PendingMigrationError if connection.migration_context.needs_migration?
end
def load_schema_if_pending!
- if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations?
+ if Base.connection.migration_context.needs_migration? || !Base.connection.migration_context.any_migrations?
# Roundtrip to Rake to allow plugins to hook into database initialization.
- FileUtils.cd Rails.root do
+ root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root
+ FileUtils.cd(root) do
current_config = Base.connection_config
Base.clear_all_connections!
system("bin/rails db:test:prepare")
@@ -675,15 +678,13 @@ module ActiveRecord
if connection.respond_to? :revert
connection.revert { yield }
else
- recorder = CommandRecorder.new(connection)
+ recorder = command_recorder
@connection = recorder
suppress_messages do
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
@@ -692,7 +693,7 @@ module ActiveRecord
connection.respond_to?(:reverting) && connection.reverting
end
- class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc:
+ ReversibleBlockHelper = Struct.new(:reverting) do # :nodoc:
def up
yield unless reverting
end
@@ -730,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.
@@ -772,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)
@@ -800,7 +819,7 @@ module ActiveRecord
@connection = nil
end
- def write(text="")
+ def write(text = "")
puts(text) if verbose
end
@@ -810,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
@@ -823,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
@@ -835,7 +859,7 @@ 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
@@ -857,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
@@ -927,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
@@ -965,7 +994,6 @@ module ActiveRecord
require(File.expand_path(filename))
name.constantize.new(name, version)
end
-
end
class NullMigration < MigrationProxy #:nodoc:
@@ -978,132 +1006,178 @@ 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
- new(:up, migrations, target_version).migrate
+ def forward(steps = 1)
+ move(:up, steps)
+ end
+
+ def up(target_version = nil)
+ selected_migrations = if block_given?
+ migrations.select { |m| yield m }
+ else
+ migrations
end
- def down(migrations_paths, target_version = nil)
- migrations = migrations(migrations_paths)
- migrations.select! { |m| yield m } if block_given?
+ Migrator.new(:up, selected_migrations, target_version).migrate
+ end
- new(:down, migrations, target_version).migrate
+ def down(target_version = nil)
+ selected_migrations = if block_given?
+ migrations.select { |m| yield m }
+ else
+ migrations
end
- def run(direction, migrations_paths, target_version)
- new(direction, migrations(migrations_paths), target_version).run
- end
+ Migrator.new(:down, selected_migrations, target_version).migrate
+ end
- def open(migrations_paths)
- new(:up, migrations(migrations_paths), nil)
- end
+ def run(direction, target_version)
+ Migrator.new(direction, migrations, target_version).run
+ end
- def schema_migrations_table_name
- SchemaMigration.table_name
- end
+ def open
+ Migrator.new(:up, migrations, nil)
+ end
- def get_all_versions(connection = Base.connection)
- ActiveSupport::Deprecation.silence do
- if connection.table_exists?(schema_migrations_table_name)
- SchemaMigration.all.map { |x| x.version.to_i }.sort
- else
- []
- end
- end
+ def get_all_versions
+ if SchemaMigration.table_exists?
+ SchemaMigration.all_versions.map(&:to_i)
+ else
+ []
end
+ end
- def current_version(connection = Base.connection)
- get_all_versions(connection).max || 0
- end
+ def current_version
+ get_all_versions.max || 0
+ rescue ActiveRecord::NoDatabaseError
+ end
- def needs_migration?(connection = Base.connection)
- (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0
- end
+ def needs_migration?
+ (migrations.collect(&:version) - get_all_versions).size > 0
+ end
- def any_migrations?
- migrations(migrations_paths).any?
- end
+ def any_migrations?
+ migrations.any?
+ end
- def last_migration #:nodoc:
- migrations(migrations_paths).last || NullMigration.new
- end
+ def last_migration #:nodoc:
+ migrations.last || NullMigration.new
+ end
- def migrations_paths
- @migrations_paths ||= ['db/migrate']
- # just to not break things if someone uses: migrations_path = some_string
- Array(@migrations_paths)
- end
+ def 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 match_to_migration_filename?(filename) # :nodoc:
- File.basename(filename) =~ Migration::MigrationFilenameRegexp
+ MigrationProxy.new(name, version, file, scope)
end
- def parse_migration_filename(filename) # :nodoc:
- File.basename(filename).scan(Migration::MigrationFilenameRegexp).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 current_environment
+ ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+ end
- migrations = 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 protected_environment?
+ ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment
+ end
- MigrationProxy.new(name, version, file, scope)
- 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
- migrations.sort_by(&:version)
+ private
+ def migration_files
+ paths = Array(migrations_paths)
+ Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
end
- private
+ def parse_migration_filename(filename)
+ File.basename(filename).scan(Migration::MigrationFilenameRegexp).first
+ end
- def move(direction, migrations_paths, steps)
- migrator = new(direction, migrations(migrations_paths))
- start_index = migrator.migrations.index(migrator.current_migration)
+ def move(direction, steps)
+ migrator = Migrator.new(direction, migrations)
- if start_index
- finish = migrator.migrations[start_index + steps]
- version = finish ? finish.version : 0
- send(direction, migrations_paths, version)
+ if current_version != 0 && !migrator.current_migration
+ raise UnknownMigrationVersionError.new(current_version)
end
+
+ 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
+
+ class Migrator # :nodoc:
+ class << self
+ attr_accessor :migrations_paths
+
+ # 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
@@ -1111,8 +1185,8 @@ module ActiveRecord
validate(@migrations)
- Base.connection.initialize_schema_migrations_table
- Base.connection.initialize_internal_metadata_table
+ ActiveRecord::SchemaMigration.create_table
+ ActiveRecord::InternalMetadata.create_table
end
def current_version
@@ -1165,151 +1239,141 @@ module ActiveRecord
end
def load_migrated
- @migrated_versions = Set.new(self.class.get_all_versions)
+ @migrated_versions = Set.new(Base.connection.migration_context.get_all_versions)
end
private
- # 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?
- execute_migration_in_transaction(migration, @direction)
+ # 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
- 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)
+ record_environment
+ result
end
- runnable.each do |migration|
- execute_migration_in_transaction(migration, @direction)
- end
-
- record_environment
- end
-
- # Stores the current environment in the database.
- def record_environment
- return if down?
- ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
- end
-
- def ran?(migration)
- migrated.include?(migration.version.to_i)
- 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
- # Return true if a valid version is not provided.
- def invalid_target?
- !target && @target_version && @target_version > 0
- end
+ result = runnable.each do |migration|
+ execute_migration_in_transaction(migration, @direction)
+ 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)
+ record_environment
+ result
+ end
- Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
+ # Stores the current environment in the database.
+ def record_environment
+ return if down?
+ ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment
+ end
- ddl_transaction(migration) do
- migration.migrate(direction)
- record_version_state_after_migrating(migration.version)
+ def ran?(migration)
+ migrated.include?(migration.version.to_i)
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
+ # Return true if a valid version is not provided.
+ def invalid_target?
+ @target_version && @target_version != 0 && !target
+ end
- def finish
- migrations.index(target) || migrations.size - 1
- 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 start
- up? ? 0 : (migrations.index(current) || 0)
- end
+ Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
- def validate(migrations)
- name ,= migrations.group_by(&:name).find { |_,v| v.length > 1 }
- raise DuplicateMigrationNameError.new(name) if name
+ 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
- version ,= migrations.group_by(&:version).find { |_,v| v.length > 1 }
- raise DuplicateMigrationVersionError.new(version) if version
- end
+ def target
+ migrations.detect { |m| m.version == @target_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)
+ def finish
+ migrations.index(target) || migrations.size - 1
end
- end
- def self.last_stored_environment
- return nil if current_version == 0
- raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists?
+ def start
+ up? ? 0 : (migrations.index(current) || 0)
+ end
- environment = ActiveRecord::InternalMetadata[:environment]
- raise NoEnvironmentInSchemaError unless environment
- environment
- end
+ def validate(migrations)
+ name, = migrations.group_by(&:name).find { |_, v| v.length > 1 }
+ raise DuplicateMigrationNameError.new(name) if name
- def self.current_environment
- ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
- end
+ version, = migrations.group_by(&:version).find { |_, v| v.length > 1 }
+ raise DuplicateMigrationVersionError.new(version) if version
+ end
- def self.protected_environment?
- ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment
- end
+ def record_version_state_after_migrating(version)
+ if down?
+ migrated.delete(version)
+ ActiveRecord::SchemaMigration.delete_by(version: version.to_s)
+ else
+ migrated << version
+ ActiveRecord::SchemaMigration.create!(version: version.to_s)
+ end
+ end
- def up?
- @direction == :up
- end
+ def up?
+ @direction == :up
+ end
- def down?
- @direction == :down
- 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
+ # 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
- end
- def use_transaction?(migration)
- !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions?
- end
+ def use_transaction?(migration)
+ !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions?
+ end
- def use_advisory_lock?
- Base.connection.supports_advisory_locks?
- end
+ def use_advisory_lock?
+ Base.connection.advisory_locks_enabled?
+ end
- def with_advisory_lock
- lock_id = generate_migrator_advisory_lock_id
- got_lock = Base.connection.get_advisory_lock(lock_id)
- raise ConcurrentMigrationError unless got_lock
- load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
- yield
- ensure
- Base.connection.release_advisory_lock(lock_id) if got_lock
- 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
- MIGRATOR_SALT = 2053462845
- def generate_migrator_advisory_lock_id
- db_name_hash = Zlib.crc32(Base.connection.current_database)
- MIGRATOR_SALT * db_name_hash
- 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 0fa665c7e0..8e7f596076 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
@@ -83,7 +85,7 @@ module ActiveRecord
# invert the +command+.
def inverse_of(command, args, &block)
method = :"invert_#{command}"
- raise IrreversibleMigration, <<-MSG.strip_heredoc unless respond_to?(method, true)
+ raise IrreversibleMigration, <<~MSG unless respond_to?(method, true)
This migration uses #{command}, which is not automatically reversible.
To make the migration reversible you can either:
1. Define #up and #down methods in place of the #change method.
@@ -92,10 +94,6 @@ module ActiveRecord
send(method, args, &block)
end
- def respond_to?(*args) # :nodoc:
- super || delegate.respond_to?(*args)
- end
-
ReversibleAndIrreversibleMethods.each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def create_table(*args, &block)
@@ -110,129 +108,154 @@ 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 }
- 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)."
+ invertions_proc = proc {
+ sub_recorder.replay(self)
+ }
+
+ [: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]
+
+ [: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."
+ 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)
+ options = args.extract_options!
+ from_table, to_table = 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 ||= options.delete(:to_table)
- reversed_args = [from_table, to_table]
- reversed_args << remove_options if remove_options
+ raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil?
- [:add_foreign_key, reversed_args]
- end
+ reversed_args = [from_table, to_table]
+ reversed_args << options unless options.empty?
- # 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
+ [: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
index a20d7e0820..abc939826b 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -1,55 +1,185 @@
+# frozen_string_literal: true
+
module ActiveRecord
class Migration
module Compatibility # :nodoc: all
- V5_0 = Current
+ 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
- module FourTwoShared
+ class V5_2 < V6_0
module TableDefinition
- def references(*, **options)
- options[:index] ||= false
+ def timestamps(**options)
+ options[:precision] ||= nil
super
end
- alias :belongs_to :references
+ end
- def timestamps(*, **options)
- options[:null] = true if options[:null].nil?
+ module CommandRecorder
+ def invert_transaction(args, &block)
+ [:transaction, args, block]
+ end
+ end
+
+ def create_table(table_name, **options)
+ if block_given?
+ super { |t| yield compatible_table_definition(t) }
+ else
super
end
end
- def create_table(table_name, options = {})
+ def change_table(table_name, **options)
if block_given?
- super(table_name, options) do |t|
- class << t
- prepend TableDefinition
- end
- yield t
- end
+ super { |t| yield compatible_table_definition(t) }
else
super
end
end
- def change_table(table_name, options = {})
+ def create_join_table(table_1, table_2, **options)
if block_given?
- super(table_name, options) do |t|
- class << t
- prepend TableDefinition
- end
- yield t
+ super { |t| yield compatible_table_definition(t) }
+ else
+ super
+ end
+ end
+
+ def add_timestamps(table_name, **options)
+ options[:precision] ||= nil
+ super
+ end
+
+ private
+ def compatible_table_definition(t)
+ class << t
+ prepend TableDefinition
+ end
+ t
+ end
+
+ def command_recorder
+ recorder = super
+ class << recorder
+ prepend CommandRecorder
end
+ recorder
+ end
+ end
+
+ class V5_1 < V5_2
+ def change_column(table_name, column_name, type, options = {})
+ if connection.adapter_name == "PostgreSQL"
+ super(table_name, column_name, type, options.except(:default, :null, :comment))
+ connection.change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
+ connection.change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ connection.change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
+ else
+ super
+ end
+ end
+
+ def create_table(table_name, options = {})
+ if connection.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 connection.adapter_name == "PostgreSQL"
+ if options[:id] == :uuid && !options.key?(:default)
+ options[:default] = "uuid_generate_v4()"
+ end
+ end
+
+ unless connection.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
+
+ super
+ end
+
+ def create_join_table(table_1, table_2, column_options: {}, **options)
+ column_options.reverse_merge!(type: :integer)
+ super
+ 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
+ super
+ 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 add_reference(*, **options)
+ def add_reference(table_name, ref_name, **options)
options[:index] ||= false
super
end
alias :add_belongs_to :add_reference
- def add_timestamps(*, **options)
+ def add_timestamps(table_name, **options)
options[:null] = true if options[:null].nil?
super
end
@@ -60,7 +190,7 @@ module ActiveRecord
if options[:name].present?
options[:name].to_s
else
- index_name(table_name, column: column_names)
+ connection.index_name(table_name, column: column_names)
end
super
end
@@ -72,44 +202,32 @@ module ActiveRecord
end
private
-
- def index_name_for_remove(table_name, options = {})
- index_name = index_name(table_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)
-
- return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false)
+ def compatible_table_definition(t)
+ class << t
+ prepend TableDefinition
end
-
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
+ super
end
- index_name
- end
- end
+ def index_name_for_remove(table_name, options = {})
+ index_name = connection.index_name(table_name, options)
- class V4_2 < V5_0
- # 4.2 is defined as a module because it needs to be shared with
- # Legacy. When the time comes, V5_0 should be defined straight
- # in its class.
- include FourTwoShared
- end
+ unless connection.index_name_exists?(table_name, index_name)
+ if options.is_a?(Hash) && options.has_key?(:name)
+ options_without_column = options.dup
+ options_without_column.delete :column
+ index_name_without_column = connection.index_name(table_name, options_without_column)
- module Legacy
- include FourTwoShared
+ if connection.index_name_exists?(table_name, index_name_without_column)
+ return index_name_without_column
+ end
+ end
- def migrate(*)
- ActiveSupport::Deprecation.warn \
- "Directly inheriting from ActiveRecord::Migration is deprecated. " \
- "Please specify the Rails release the migration was written for:\n" \
- "\n" \
- " class #{self.class.name} < ActiveRecord::Migration[4.2]"
- super
- end
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
+ end
+
+ index_name
+ 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 52eab952e1..55fc58e339 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -1,78 +1,139 @@
+# 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.
+
+ ##
+ # :singleton-method: implicit_order_column
+ # :call-seq: implicit_order_column
+ #
+ # The name of the column records are ordered by if no explicit order clause
+ # is used during an ordered finder call. If not set the primary key is used.
+
+ ##
+ # :singleton-method: implicit_order_column=
+ # :call-seq: implicit_order_column=(column_name)
+ #
+ # Sets the column to sort records by when no explicit order clause is used
+ # during an ordered finder call. Useful when the primary key is not an
+ # auto-incrementing integer, for example when it's a UUID. Note that using
+ # a non-unique column can result in non-deterministic results.
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:
- # Accessor for the name of the internal metadata table. By default, the value is "ar_internal_metadata"
- class_attribute :internal_metadata_table_name, instance_accessor: false
- self.internal_metadata_table_name = "ar_internal_metadata"
-
- ##
- # :singleton-method:
- # Accessor for an array of names of environments where destructive actions should be prohibited. By default,
- # the value is ["production"]
- class_attribute :protected_environments, instance_accessor: false
- self.protected_environments = ["production"]
+ 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
+ class_attribute :implicit_order_column, instance_accessor: false
- ##
- # :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
-
- ##
- # :singleton-method:
- # Accessor for 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.
- class_attribute :ignored_columns, instance_accessor: false
+ self.protected_environments = ["production"]
+ self.inheritance_column = "type"
self.ignored_columns = [].freeze
- self.inheritance_column = 'type'
-
delegate :type_for_attribute, to: :class
+
+ initialize_load_schema_monitor
end
# Derives the join table name for +first_table+ and +second_table+. The
@@ -173,11 +234,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
@@ -199,8 +275,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
@@ -213,7 +305,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.
#
@@ -238,7 +330,7 @@ module ActiveRecord
end
# Returns the next value that will be used as the primary key on
- # an insert statment.
+ # an insert statement.
def next_sequence_value
connection.next_sequence_value(sequence_name)
end
@@ -249,7 +341,11 @@ module ActiveRecord
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:
@@ -264,7 +360,11 @@ module ActiveRecord
def attribute_types # :nodoc:
load_schema
- @attribute_types ||= Hash.new(Type::Value.new)
+ @attribute_types ||= Hash.new(Type.default_value)
+ end
+
+ 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
@@ -277,20 +377,26 @@ module ActiveRecord
# it).
#
# +attr_name+ The name of the attribute to retrieve the type for. Must be
- # a string
- def type_for_attribute(attr_name)
- attribute_types[attr_name]
+ # 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 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.
@@ -298,10 +404,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
@@ -332,99 +448,95 @@ module ActiveRecord
# end
def reset_column_information
connection.clear_cache!
- undefine_attribute_methods
+ ([self] + descendants).each(&:undefine_attribute_methods)
connection.schema_cache.clear_data_source_cache!(table_name)
reload_schema_from_cache
+ initialize_find_by_cache
end
- private
-
- def schema_loaded?
- defined?(@columns_hash) && @columns_hash
- end
+ protected
- def load_schema
- unless schema_loaded?
- load_schema!
+ def initialize_load_schema_monitor
+ @load_schema_monitor = Monitor.new
end
- end
- def load_schema!
- @columns_hash = connection.schema_cache.columns_hash(table_name).except(*ignored_columns)
- @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
- )
+ private
+
+ def inherited(child_class)
+ super
+ child_class.initialize_load_schema_monitor
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
- direct_descendants.each do |descendant|
- descendant.send(:reload_schema_from_cache)
+ def schema_loaded?
+ defined?(@schema_loaded) && @schema_loaded
end
- end
- # 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
+ def load_schema
+ return if schema_loaded?
+ @load_schema_monitor.synchronize do
+ return if defined?(@columns_hash) && @columns_hash
- # 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 += '_'
- end
+ load_schema!
- "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
- else
- # STI subclasses always use their superclass' table.
- base.table_name
+ @schema_loaded = true
+ end
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}, :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 fe68869143..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
@@ -267,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).
#
@@ -317,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
@@ -341,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
@@ -375,213 +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]
- if attributes.respond_to?(:permitted?)
- attributes = attributes.to_h
- end
- attributes = attributes.with_indifferent_access
- existing_record = send(association_name)
+ # 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)
+ 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 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)
+ 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)
+ 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]
- if attributes_collection.respond_to?(:permitted?)
- attributes_collection = attributes_collection.to_h
- 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 = 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)
- 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
+ association = association(association_name)
- attributes_collection.each do |attributes|
- if attributes.respond_to?(:permitted?)
- attributes = attributes.to_h
+ 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 = attributes.with_indifferent_access
- if attributes['id'].blank?
- unless reject_new_record?(association_name, attributes)
- association.build(attributes.except(*UNASSIGNABLE_KEYS))
+ attributes_collection.each do |attributes|
+ if attributes.respond_to?(:permitted?)
+ attributes = attributes.to_h
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
+ attributes = attributes.with_indifferent_access
- assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
+ 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
- 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
+ # 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
- if limit && attributes_collection.size > limit
- raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
+ if limit && attributes_collection.size > limit
+ raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
+ end
end
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
+ # 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 hash contains a truthy _destroy key.
- def has_destroy_flag?(hash)
- Type::Boolean.new.cast(hash['_destroy'])
- 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 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 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 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 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)
- 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)
+ 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
- end
- # 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
+ # 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
- def allow_destroy?(association_name)
- self.nested_attributes_options[association_name][:allow_destroy]
- 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
+ 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 1ab4e0404f..cf0de0fdeb 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -1,14 +1,12 @@
+# frozen_string_literal: true
+
module ActiveRecord
module NullRelation # :nodoc:
- def exec_queries
- @records = [].freeze
- end
-
def pluck(*column_names)
[]
end
- def delete_all(_conditions = nil)
+ def delete_all
0
end
@@ -20,10 +18,6 @@ module ActiveRecord
0
end
- def size
- calculate :size, nil
- end
-
def empty?
true
end
@@ -48,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
@@ -85,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 afed5e5e85..7705cefa59 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_record/insert_all"
+
module ActiveRecord
# = Active Record \Persistence
module Persistence
@@ -53,6 +57,192 @@ module ActiveRecord
end
end
+ # Inserts a single record into the database in a single SQL INSERT
+ # statement. It does not instantiate any models nor does it trigger
+ # Active Record callbacks or validations. Though passed values
+ # go through Active Record's type casting and serialization.
+ #
+ # See <tt>ActiveRecord::Persistence#insert_all</tt> for documentation.
+ def insert(attributes, returning: nil, unique_by: nil)
+ insert_all([ attributes ], returning: returning, unique_by: unique_by)
+ end
+
+ # Inserts multiple records into the database in a single SQL INSERT
+ # statement. It does not instantiate any models nor does it trigger
+ # Active Record callbacks or validations. Though passed values
+ # go through Active Record's type casting and serialization.
+ #
+ # The +attributes+ parameter is an Array of Hashes. Every Hash determines
+ # the attributes for a single row and must have the same keys.
+ #
+ # Rows are considered to be unique by every unique index on the table. Any
+ # duplicate rows are skipped.
+ # Override with <tt>:unique_by</tt> (see below).
+ #
+ # Returns an <tt>ActiveRecord::Result</tt> with its contents based on
+ # <tt>:returning</tt> (see below).
+ #
+ # ==== Options
+ #
+ # [:returning]
+ # (Postgres-only) An array of attributes to return for all successfully
+ # inserted records, which by default is the primary key.
+ # Pass <tt>returning: %w[ id name ]</tt> for both id and name
+ # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
+ # clause entirely.
+ #
+ # [:unique_by]
+ # (Postgres and SQLite only) By default rows are considered to be unique
+ # by every unique index on the table. Any duplicate rows are skipped.
+ #
+ # To skip rows according to just one unique index pass <tt>:unique_by</tt>.
+ #
+ # Consider a Book model where no duplicate ISBNs make sense, but if any
+ # row has an existing id, or is not unique by another unique index,
+ # <tt>ActiveRecord::RecordNotUnique</tt> is raised.
+ #
+ # Unique indexes can be identified by columns or name:
+ #
+ # unique_by: :isbn
+ # unique_by: %i[ author_id name ]
+ # unique_by: :index_books_on_isbn
+ #
+ # Because it relies on the index information from the database
+ # <tt>:unique_by</tt> is recommended to be paired with
+ # Active Record's schema_cache.
+ #
+ # ==== Example
+ #
+ # # Insert records and skip inserting any duplicates.
+ # # Here "Eloquent Ruby" is skipped because its id is not unique.
+ #
+ # Book.insert_all([
+ # { id: 1, title: "Rework", author: "David" },
+ # { id: 1, title: "Eloquent Ruby", author: "Russ" }
+ # ])
+ def insert_all(attributes, returning: nil, unique_by: nil)
+ InsertAll.new(self, attributes, on_duplicate: :skip, returning: returning, unique_by: unique_by).execute
+ end
+
+ # Inserts a single record into the database in a single SQL INSERT
+ # statement. It does not instantiate any models nor does it trigger
+ # Active Record callbacks or validations. Though passed values
+ # go through Active Record's type casting and serialization.
+ #
+ # See <tt>ActiveRecord::Persistence#insert_all!</tt> for more.
+ def insert!(attributes, returning: nil)
+ insert_all!([ attributes ], returning: returning)
+ end
+
+ # Inserts multiple records into the database in a single SQL INSERT
+ # statement. It does not instantiate any models nor does it trigger
+ # Active Record callbacks or validations. Though passed values
+ # go through Active Record's type casting and serialization.
+ #
+ # The +attributes+ parameter is an Array of Hashes. Every Hash determines
+ # the attributes for a single row and must have the same keys.
+ #
+ # Raises <tt>ActiveRecord::RecordNotUnique</tt> if any rows violate a
+ # unique index on the table. In that case, no rows are inserted.
+ #
+ # To skip duplicate rows, see <tt>ActiveRecord::Persistence#insert_all</tt>.
+ # To replace them, see <tt>ActiveRecord::Persistence#upsert_all</tt>.
+ #
+ # Returns an <tt>ActiveRecord::Result</tt> with its contents based on
+ # <tt>:returning</tt> (see below).
+ #
+ # ==== Options
+ #
+ # [:returning]
+ # (Postgres-only) An array of attributes to return for all successfully
+ # inserted records, which by default is the primary key.
+ # Pass <tt>returning: %w[ id name ]</tt> for both id and name
+ # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
+ # clause entirely.
+ #
+ # ==== Examples
+ #
+ # # Insert multiple records
+ # Book.insert_all!([
+ # { title: "Rework", author: "David" },
+ # { title: "Eloquent Ruby", author: "Russ" }
+ # ])
+ #
+ # # Raises ActiveRecord::RecordNotUnique because "Eloquent Ruby"
+ # # does not have a unique id.
+ # Book.insert_all!([
+ # { id: 1, title: "Rework", author: "David" },
+ # { id: 1, title: "Eloquent Ruby", author: "Russ" }
+ # ])
+ def insert_all!(attributes, returning: nil)
+ InsertAll.new(self, attributes, on_duplicate: :raise, returning: returning).execute
+ end
+
+ # Updates or inserts (upserts) multiple records into the database in a
+ # single SQL INSERT statement. It does not instantiate any models nor does
+ # it trigger Active Record callbacks or validations. Though passed values
+ # go through Active Record's type casting and serialization.
+ #
+ # See <tt>ActiveRecord::Persistence#upsert_all</tt> for documentation.
+ def upsert(attributes, returning: nil, unique_by: nil)
+ upsert_all([ attributes ], returning: returning, unique_by: unique_by)
+ end
+
+ # Updates or inserts (upserts) multiple records into the database in a
+ # single SQL INSERT statement. It does not instantiate any models nor does
+ # it trigger Active Record callbacks or validations. Though passed values
+ # go through Active Record's type casting and serialization.
+ #
+ # The +attributes+ parameter is an Array of Hashes. Every Hash determines
+ # the attributes for a single row and must have the same keys.
+ #
+ # Returns an <tt>ActiveRecord::Result</tt> with its contents based on
+ # <tt>:returning</tt> (see below).
+ #
+ # ==== Options
+ #
+ # [:returning]
+ # (Postgres-only) An array of attributes to return for all successfully
+ # inserted records, which by default is the primary key.
+ # Pass <tt>returning: %w[ id name ]</tt> for both id and name
+ # or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
+ # clause entirely.
+ #
+ # [:unique_by]
+ # (Postgres and SQLite only) By default rows are considered to be unique
+ # by every unique index on the table. Any duplicate rows are skipped.
+ #
+ # To skip rows according to just one unique index pass <tt>:unique_by</tt>.
+ #
+ # Consider a Book model where no duplicate ISBNs make sense, but if any
+ # row has an existing id, or is not unique by another unique index,
+ # <tt>ActiveRecord::RecordNotUnique</tt> is raised.
+ #
+ # Unique indexes can be identified by columns or name:
+ #
+ # unique_by: :isbn
+ # unique_by: %i[ author_id name ]
+ # unique_by: :index_books_on_isbn
+ #
+ # Because it relies on the index information from the database
+ # <tt>:unique_by</tt> is recommended to be paired with
+ # Active Record's schema_cache.
+ #
+ # ==== Examples
+ #
+ # # Inserts multiple records, performing an upsert when records have duplicate ISBNs.
+ # # Here "Eloquent Ruby" overwrites "Rework" because its ISBN is duplicate.
+ #
+ # Book.upsert_all([
+ # { title: "Rework", author: "David", isbn: "1" },
+ # { title: "Eloquent Ruby", author: "Russ", isbn: "1" }
+ # ], unique_by: :isbn)
+ #
+ # Book.find_by(isbn: "1").title # => "Eloquent Ruby"
+ def upsert_all(attributes, returning: nil, unique_by: nil)
+ InsertAll.new(self, attributes, on_duplicate: :update, returning: returning, unique_by: unique_by).execute
+ end
+
# Given an attributes hash, +instantiate+ returns a new instance of
# the appropriate class. Accepts only keys as strings.
#
@@ -63,13 +253,155 @@ module ActiveRecord
#
# 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 = :all, attributes)
+ if id.is_a?(Array)
+ id.map { |one_id| find(one_id) }.each_with_index { |object, idx|
+ object.update(attributes[idx])
+ }
+ elsif id == :all
+ all.each { |record| record.update(attributes) }
+ else
+ 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 an
+ # SQL +DELETE+ statement, and returns the number of rows deleted. Active
+ # Record objects are not instantiated, so the object's callbacks are not
+ # executed, including any <tt>:dependent</tt> association options.
+ #
+ # 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)
+ delete_by(primary_key => id_or_array)
+ 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 +410,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,6 +440,10 @@ 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
@@ -107,7 +451,7 @@ module ActiveRecord
#
# 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
- # validate: false, validations are bypassed altogether. See
+ # <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,12 +465,16 @@ 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
@@ -134,7 +482,7 @@ module ActiveRecord
#
# 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
- # validate: false, validations are bypassed altogether. See
+ # <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
@@ -148,8 +496,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
@@ -165,7 +515,7 @@ module ActiveRecord
# 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
@@ -178,10 +528,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
@@ -210,9 +564,10 @@ module ActiveRecord
# Any change to the attributes on either instance will affect both instances.
# If you want to change the sti column as well, use #becomes! instead.
def becomes(klass)
- became = klass.new
+ became = klass.allocate
+ became.send(:initialize)
became.instance_variable_set("@attributes", @attributes)
- became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker)
+ became.instance_variable_set("@mutations_from_database", @mutations_from_database ||= 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?)
@@ -252,7 +607,8 @@ module ActiveRecord
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
@@ -268,6 +624,7 @@ module ActiveRecord
end
alias update_attributes update
+ deprecate update_attributes: "please, use update instead"
# Updates its receiver just like #update but calls #save! instead
# of +save+, so an exception is raised if the record is invalid and saving will fail.
@@ -281,6 +638,7 @@ module ActiveRecord
end
alias update_attributes! update!
+ deprecate update_attributes!: "please, use update! instead"
# Equivalent to <code>update_columns(name => value)</code>.
def update_column(name, value)
@@ -311,13 +669,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).
@@ -329,14 +691,16 @@ module ActiveRecord
self
end
- # Wrapper around #increment that saves the record. This method differs from
- # its non-bang version in that it passes through the attribute setter.
- # Saving is not subjected to validation checks. Returns +true+ if the
- # record could be saved.
- def increment!(attribute, by = 1)
+ # Wrapper around #increment that writes the update to the database.
+ # Only +attribute+ is updated; the record itself is not saved.
+ # This means that any other modified attributes will still be dirty.
+ # Validations and callbacks are skipped. Supports the +touch+ option from
+ # +update_counters+, see that for more.
+ # Returns +self+.
+ def increment!(attribute, by = 1, touch: nil)
increment(attribute, by)
- change = public_send(attribute) - (attribute_was(attribute.to_s) || 0)
- self.class.update_counters(id, attribute => change)
+ 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
@@ -348,12 +712,14 @@ module ActiveRecord
increment(attribute, -by)
end
- # Wrapper around #decrement that saves the record. This method differs from
- # its non-bang version in the sense that it passes through the attribute setter.
- # Saving is not subjected to validation checks. Returns +true+ if the
- # record could be saved.
- def decrement!(attribute, by = 1)
- increment!(attribute, -by)
+ # Wrapper around #decrement that writes the update to the database.
+ # Only +attribute+ is updated; the record itself is not saved.
+ # This means that any other modified attributes will still be dirty.
+ # Validations and callbacks are skipped. Supports the +touch+ option from
+ # +update_counters+, see that for more.
+ # Returns +self+.
+ def decrement!(attribute, by = 1, touch: nil)
+ increment!(attribute, -by, touch: touch)
end
# Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
@@ -383,8 +749,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>
@@ -439,7 +805,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
@@ -479,37 +845,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
@@ -522,39 +870,71 @@ module ActiveRecord
end
def destroy_row
- relation_for_destroy.delete_all
+ _delete_row
+ end
+
+ def _delete_row
+ self.class._delete_record(self.class.primary_key => id_in_database)
end
- def relation_for_destroy
- self.class.unscoped.where(self.class.primary_key => id)
+ 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 create_or_update(*args)
- raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
- result = new_record? ? _create_record : _update_record(*args)
+ 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(**, &block)
+ _raise_readonly_record_error if readonly?
+ return false if destroyed?
+ result = new_record? ? _create_record(&block) : _update_record(&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
@@ -569,8 +949,14 @@ module ActiveRecord
@_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 f451ed1764..43a21e629e 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,26 +25,28 @@ module ActiveRecord
end
end
- def self.install_executor_hooks(executor = ActiveSupport::Executor)
- executor.to_run do
- connection = ActiveRecord::Base.connection
- enabled = connection.query_cache_enabled
- connection_id = ActiveRecord::Base.connection_id
- connection.enable_query_cache!
-
- @restore_query_cache_settings = lambda do
- ActiveRecord::Base.connection_id = connection_id
- ActiveRecord::Base.connection.clear_query_cache
- ActiveRecord::Base.connection.disable_query_cache! unless enabled
- end
+ def self.run
+ pools = []
+
+ ActiveRecord::Base.connection_handlers.each do |key, handler|
+ pools << handler.connection_pool_list.reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! }
end
- executor.to_complete do
- @restore_query_cache_settings.call if defined?(@restore_query_cache_settings)
+ pools.flatten
+ end
+
+ def self.complete(pools)
+ pools.each { |pool| pool.disable_query_cache! }
- # FIXME: This should be skipped when env['rack.test']
- ActiveRecord::Base.clear_active_connections!
+ ActiveRecord::Base.connection_handlers.each do |_, handler|
+ handler.connection_pool_list.each do |pool|
+ pool.release_connection if pool.active_connection? && !pool.connection.transaction_open?
+ end
end
end
+
+ 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 4e32d73001..ae1501f5a1 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -1,30 +1,37 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Querying
- delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :empty?, :none?, :one?, to: :all
- delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all
- delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
- delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
- 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, :in_batches, to: :all
- delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or,
- :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly,
- :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
- delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
- delegate :pluck, :ids, to: :all
+ QUERYING_METHODS = [
+ :find, :find_by, :find_by!, :take, :take!, :first, :first!, :last, :last!,
+ :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!,
+ :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!,
+ :exists?, :any?, :many?, :none?, :one?,
+ :first_or_create, :first_or_create!, :first_or_initialize,
+ :find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
+ :create_or_find_by, :create_or_find_by!,
+ :destroy_all, :delete_all, :update_all, :destroy_by, :delete_by,
+ :find_each, :find_in_batches, :in_batches,
+ :select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
+ :where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
+ :having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
+ :count, :average, :minimum, :maximum, :sum, :calculate, :annotate,
+ :pluck, :pick, :ids
+ ].freeze # :nodoc:
+ delegate(*QUERYING_METHODS, 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
- # this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in
+ # be returned as an array, with the requested columns encapsulated as attributes of the model you call
+ # this method from. For example, if you call <tt>Product.find_by_sql</tt>, then the results will be returned in
# a +Product+ object with the attributes you specified in the SQL query.
#
- # If you call a complicated SQL query which spans multiple tables the columns specified by the
+ # If you call a complicated SQL query which spans multiple tables, the columns specified by the
# SELECT will be attributes of the model, whether or not they are columns of the corresponding
# table.
#
- # The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be
- # no database agnostic conversions performed. This should be a last resort because using, for example,
- # MySQL specific terms will lock you to using that particular database engine or require you to
+ # The +sql+ parameter is a full SQL query as a string. It will be called as is; there will be
+ # no database agnostic conversions performed. This should be a last resort because using
+ # database-specific terms will lock you into using that particular database engine, or require you to
# change your call if you switch engines.
#
# # A simple SQL query spanning multiple tables
@@ -35,10 +42,10 @@ 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 = [], preparable: nil)
+ 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 }
+ attribute_types.each_key { |k| column_types.delete k }
message_bus = ActiveSupport::Notifications.instrumenter
payload = {
@@ -46,14 +53,21 @@ 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
# Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
# The use of this method should be restricted to complicated SQL queries that can't be executed
- # using the ActiveRecord::Calculations class methods. Look into those before using this.
+ # using the ActiveRecord::Calculations class methods. Look into those before using this method,
+ # as it could lock you into a specific database engine or require a code change to switch
+ # database engines.
#
# Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
# # => 12
@@ -62,8 +76,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 98ea425d16..a1d7c893bf 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,20 +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_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
@@ -35,8 +39,8 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration
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
+ if engine.paths["db/migrate"].existent
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths["db/migrate"].to_a
end
end
end
@@ -55,6 +59,7 @@ module ActiveRecord
console = ActiveSupport::Logger.new(STDERR)
Rails.logger.extend ActiveSupport::Logger.broadcast console
end
+ ActiveRecord::Base.verbose_query_logs = false
end
runner do
@@ -72,6 +77,10 @@ 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,
@@ -79,19 +88,56 @@ module ActiveRecord
end
end
+ initializer "active_record.database_selector" do
+ if options = config.active_record.delete(:database_selector)
+ resolver = config.active_record.delete(:database_resolver)
+ operations = config.active_record.delete(:database_resolver_context)
+ config.app_middleware.use ActiveRecord::Middleware::DatabaseSelector, resolver, operations, options
+ end
+ end
+
+ initializer "Check for cache versioning support" do
+ config.after_initialize do |app|
+ ActiveSupport.on_load(:active_record) do
+ 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
+
initializer "active_record.check_schema_cache_dump" do
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
@@ -99,17 +145,49 @@ module ActiveRecord
end
end
+ initializer "active_record.define_attribute_methods" do |app|
+ config.after_initialize do
+ ActiveSupport.on_load(:active_record) do
+ if app.config.eager_load
+ descendants.each do |model|
+ # SchemaMigration and InternalMetadata both override `table_exists?`
+ # to bypass the schema cache, so skip them to avoid the extra queries.
+ next if model._internal?
+
+ # If there's no connection yet, or the schema cache doesn't have the columns
+ # hash for the model cached, `define_attribute_methods` would trigger a query.
+ next unless model.connected? && model.connection.schema_cache.columns_hash?(model.table_name)
+
+ model.define_attribute_methods
+ end
+ end
+ 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
+
+ represent_boolean_as_integer = configs.sqlite3.delete(:represent_boolean_as_integer)
+
+ unless represent_boolean_as_integer.nil?
+ ActiveSupport.on_load(:active_record_sqlite3adapter) do
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = represent_boolean_as_integer
+ end
+ end
+
+ configs.delete(:sqlite3)
+
+ configs.each do |k, v|
send "#{k}=", v
end
end
@@ -119,22 +197,9 @@ module ActiveRecord
# and then establishes the connection.
initializer "active_record.initialize_database" do
ActiveSupport.on_load(:active_record) do
+ self.connection_handlers = { writing_role => ActiveRecord::Base.default_connection_handler }
self.configurations = Rails.application.config.database_configuration
-
- 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/rails db:create` to create the database.
- 3. Run `bin/rails db:setup` to load your database schema.
-end_warning
- raise
- end
+ establish_connection
end
end
@@ -146,6 +211,13 @@ end_warning
end
end
+ initializer "active_record.collection_cache_association_loading" do
+ require "active_record/railties/collection_cache_association_loading"
+ ActiveSupport.on_load(:action_view) do
+ ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading)
+ end
+ end
+
initializer "active_record.set_reloader_hooks" do
ActiveSupport.on_load(:active_record) do
ActiveSupport::Reloader.before_class_unload do
@@ -158,14 +230,35 @@ end_warning
end
initializer "active_record.set_executor_hooks" do
- ActiveSupport.on_load(:active_record) do
- ActiveRecord::QueryCache.install_executor_hooks
- end
+ 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.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..d57680aaaa
--- /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, as, 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 00cf8536e1..447def8d77 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -1,40 +1,58 @@
-require 'active_record'
+# frozen_string_literal: true
+
+require "active_record"
db_namespace = namespace :db do
desc "Set the environment value for the database"
- task "environment:set" => [:environment, :load_config] do
+ task "environment:set" => :load_config do
ActiveRecord::InternalMetadata.create_table
- ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
+ ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment
end
- task :check_protected_environments => [:environment, :load_config] do
+ task check_protected_environments: :load_config do
ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
end
- task :load_config do
+ task load_config: :environment do
ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {}
ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
end
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 or when RAILS_ENV is development, 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, :check_protected_environments] 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
+ 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
@@ -43,20 +61,28 @@ db_namespace = namespace :db do
end
namespace :purge do
- task :all => [:load_config, :check_protected_environments] do
+ task all: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.purge_all
end
end
+ # desc "Truncates tables of each database for current environment"
+ task truncate_all: [:load_config, :check_protected_environments] do
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all
+ end
+
# 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
+ 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
@@ -71,167 +97,188 @@ 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::Tasks::DatabaseTasks.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::Tasks::DatabaseTasks.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::Tasks::DatabaseTasks.migrations_paths.flat_map do |path|
- Dir.foreach(path).map do |file|
- next unless ActiveRecord::Migrator.match_to_migration_filename?(file)
-
- version, name, scope = ActiveRecord::Migrator.parse_migration_filename(file)
- version = ActiveRecord::SchemaMigration.normalize_migration_number(version)
- status = db_list.delete(version) ? 'up' : 'down'
- [status, version, (name + scope).humanize]
- end.compact
- end
+ 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::Tasks::DatabaseTasks.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::Tasks::DatabaseTasks.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
- 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.'
- end
+ task collation: :load_config do
+ puts ActiveRecord::Tasks::DatabaseTasks.collation_current
+ rescue NoMethodError
+ $stderr.puts "Sorry, your database adapter is not supported yet. Feel free to submit a patch."
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::Tasks::DatabaseTasks.migrations_paths).pending_migrations
+ task abort_if_pending_migrations: :load_config do
+ pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations
if pending_migrations.any?
puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}"
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 `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 "Runs setup if database does not exist, or runs migrations if it does"
+ task prepare: :load_config do
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ ActiveRecord::Base.establish_connection(db_config.config)
+ db_namespace["migrate"].invoke
+ rescue ActiveRecord::NoDatabaseError
+ db_namespace["setup"].invoke
+ end
+ end
- 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 :seed do
+ desc "Truncates tables of each database for current environment and loads the seeds"
+ task replant: [:load_config, :truncate_all, :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)
@@ -245,134 +292,124 @@ db_namespace = namespace :db do
end
namespace :schema do
- desc 'Creates a db/schema.rb file that is portable against any DB supported by Active Record'
- task :dump => [:environment, :load_config] do
- require '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.data_sources.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 => [:environment, :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
- 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']
- ensure
- if should_reconnect
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
- end
+ task load_schema: %w(db:test:purge) do
+ should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
+ ActiveRecord::Schema.verbose = false
+ 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.default_hash(ActiveRecord::Tasks::DatabaseTasks.env))
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 check_protected_environments) 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 'Load the test schema'
- task :prepare => %w(environment load_config) do
+ task prepare: :load_config do
unless ActiveRecord::Base.configurations.blank?
- db_namespace['test:load'].invoke
+ db_namespace["test:load"].invoke
end
end
end
@@ -381,15 +418,19 @@ 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
+
+ unless ENV["MIGRATIONS_PATH"].blank?
+ railties[railtie.railtie_name] = railtie.root + ENV["MIGRATIONS_PATH"]
+ end
end
on_skip = Proc.new do |name, migration|
@@ -401,7 +442,7 @@ namespace :railties do
end
ActiveRecord::Migration.copy(ActiveRecord::Tasks::DatabaseTasks.migrations_paths.first, railties,
- :on_skip => on_skip, :on_copy => on_copy)
+ 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 f8dffce2f1..3452cf971b 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,37 +9,41 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :_reflections, instance_writer: false
- class_attribute :aggregate_reflections, instance_writer: false
- 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.clear_reflections_cache
- 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
@@ -135,8 +141,8 @@ module ActiveRecord
# BelongsToReflection
# HasAndBelongsToManyReflection
# ThroughReflection
- # PolymorphicReflection
- # RuntimeReflection
+ # PolymorphicReflection
+ # RuntimeReflection
class AbstractReflection # :nodoc:
def through_reflection?
false
@@ -152,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>
@@ -170,12 +168,52 @@ 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 join_scope(table, foreign_table, foreign_klass)
+ predicate_builder = predicate_builder(table)
+ scope_chain_items = join_scopes(table, predicate_builder)
+ klass_scope = klass_join_scope(table, predicate_builder)
+
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
+
+ klass_scope.where!(table[key].eq(foreign_table[foreign_key]))
+
+ if type
+ klass_scope.where!(type => foreign_klass.polymorphic_name)
+ end
+
+ if klass.finder_needs_type_condition?
+ klass_scope.where!(klass.send(:type_condition, table))
+ 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
@@ -247,6 +285,40 @@ module ActiveRecord
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
@@ -281,7 +353,6 @@ module ActiveRecord
end
def autosave=(autosave)
- @automatic_inverse_of = false
@options[:autosave] = autosave
parent_reflection = self.parent_reflection
if parent_reflection
@@ -293,6 +364,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
@@ -311,14 +393,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
@@ -327,26 +412,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
@@ -355,22 +427,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:
@@ -416,7 +488,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
@@ -443,12 +515,6 @@ module ActiveRecord
false
end
- # An array of arrays of scopes. Each item in the outside array corresponds to a reflection
- # in the #chain.
- def scope_chain
- scope ? [[scope]] : [[]]
- end
-
def has_scope?
scope
end
@@ -505,7 +571,7 @@ module ActiveRecord
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]
def add_as_source(seed)
seed
@@ -519,11 +585,9 @@ module ActiveRecord
seed + [self]
end
- protected
-
- def actual_source_reflection # FIXME: this is a horrible name
- self
- end
+ def extensions
+ Array(options[:extend])
+ end
private
@@ -533,18 +597,16 @@ module ActiveRecord
# 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 false or the inverse association name that it finds.
+ # returns either +nil+ or the inverse association name that it finds.
def automatic_inverse_of
if can_find_inverse_of_automatically?(self)
inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym
@@ -561,20 +623,15 @@ module ActiveRecord
return inverse_name
end
end
-
- false
end
# Checks if the inverse reflection that is returned from the
# +automatic_inverse_of+ method is a valid reflection. We must
# make sure that the reflection's active_record name matches up
# with the current reflection's klass name.
- #
- # Note: klass will always be valid because when there's a NameError
- # from calling +klass+, +reflection+ will already be set to false.
def valid_inverse_reflection?(reflection)
reflection &&
- klass.name == reflection.active_record.name &&
+ klass <= reflection.active_record &&
can_find_inverse_of_automatically?(reflection)
end
@@ -582,9 +639,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.
@@ -614,10 +670,6 @@ 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:
@@ -632,6 +684,10 @@ module ActiveRecord
Associations::HasManyAssociation
end
end
+
+ def association_primary_key(klass = nil)
+ primary_key(klass || self.klass)
+ end
end
class HasOneReflection < AssociationReflection # :nodoc:
@@ -667,16 +723,18 @@ module ActiveRecord
end
end
- def join_keys(association_klass)
- key = polymorphic? ? association_primary_key(association_klass) : association_primary_key
- JoinKeys.new(key, foreign_key)
+ def join_primary_key(klass = nil)
+ polymorphic? ? association_primary_key(klass) : association_primary_key
end
- def join_id_for(owner) # :nodoc:
- owner[foreign_key]
+ def join_foreign_key
+ foreign_key
end
private
+ def can_find_inverse_of_automatically?(_)
+ !polymorphic? && super
+ end
def calculate_constructable(macro, options)
!polymorphic?
@@ -684,10 +742,6 @@ module ActiveRecord
end
class HasAndBelongsToManyReflection < AssociationReflection # :nodoc:
- def initialize(name, scope, options, active_record)
- super
- end
-
def macro; :has_and_belongs_to_many; end
def collection?
@@ -695,16 +749,15 @@ 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
@@ -782,45 +835,12 @@ module ActiveRecord
through_reflection.clear_association_scope_cache
end
- # Consider the following example:
- #
- # class Person
- # has_many :articles
- # has_many :comment_tags, through: :articles
- # end
- #
- # class Article
- # has_many :comments
- # has_many :comment_tags, through: :comments, source: :tags
- # end
- #
- # class Comment
- # has_many :tags
- # end
- #
- # There may be scopes on Person.comment_tags, Article.comment_tags and/or Comment.tags,
- # but only Comment.tags will be represented in the #chain. So this method creates an array
- # of scopes corresponding to the chain.
- def scope_chain
- @scope_chain ||= begin
- scope_chain = source_reflection.scope_chain.map(&:dup)
-
- # Add to it the scope from this reflection (if any)
- scope_chain.first << scope if scope
-
- through_scope_chain = through_reflection.scope_chain.map(&:dup)
-
- 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 has_scope?
@@ -829,10 +849,6 @@ module ActiveRecord
through_reflection.has_scope?
end
- def join_keys(association_klass)
- source_reflection.join_keys(association_klass)
- end
-
# A through association is nested if there would be more than one join table
def nested?
source_reflection.through_reflection? || through_reflection.through_reflection?
@@ -871,15 +887,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
@@ -893,10 +907,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)
@@ -926,6 +936,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
@@ -947,28 +965,25 @@ module ActiveRecord
collect_join_reflections(seed + [self])
end
- 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
-
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
def inverse_name; delegate_reflection.send(:inverse_name); end
- private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
options[:source_type] || source_reflection.class_name
@@ -978,52 +993,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
- @reflection.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
@@ -1034,24 +1032,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 c0ed89fc97..dd821431e1 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,16 +1,17 @@
-require "arel/collectors/bind"
+# frozen_string_literal: true
module ActiveRecord
# = Active Record \Relation
class Relation
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
- :order, :joins, :left_joins, :left_outer_joins, :references,
- :extending, :unscope]
+ :order, :joins, :left_outer_joins, :references,
+ :extending, :unscope, :optimizer_hints, :annotate]
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
@@ -18,95 +19,39 @@ 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 && klass.prefetch_primary_key?
- primary_key_value = klass.next_sequence_value
- values[arel_attribute(klass.primary_key)] = primary_key_value
- end
- end
-
- im = arel.create_insert
- im.into @table
-
- substitutes, 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 = []
- substitutes = []
-
- values.each do |arel_attr, value|
- binds.push QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name))
- substitutes.push [arel_attr, Arel::Nodes::BindParam.new]
+ def bind_attribute(name, value) # :nodoc:
+ if reflection = klass._reflect_on_association(name)
+ name = reflection.foreign_key
+ value = value.read_attribute(reflection.klass.primary_key) unless value.nil?
end
- [substitutes, binds]
- end
-
- def arel_attribute(name) # :nodoc:
- klass.arel_attribute(name, table)
+ 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
@@ -121,8 +66,9 @@ 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)
+ block = _deprecated_scope_block("new", &block)
+ scoping { klass.new(attributes, &block) }
end
alias build new
@@ -146,8 +92,13 @@ module ActiveRecord
#
# users.create(name: nil) # validation on name
# # => #<User id: nil, name: nil, ...>
- def create(*args, &block)
- scoping { @klass.create(*args, &block) }
+ def create(attributes = nil, &block)
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create(attr, &block) }
+ else
+ block = _deprecated_scope_block("create", &block)
+ scoping { klass.create(attributes, &block) }
+ end
end
# Similar to #create, but calls
@@ -156,8 +107,13 @@ module ActiveRecord
#
# Expects arguments in the same format as
# {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!].
- def create!(*args, &block)
- scoping { @klass.create!(*args, &block) }
+ def create!(attributes = nil, &block)
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create!(attr, &block) }
+ else
+ block = _deprecated_scope_block("create!", &block)
+ scoping { klass.create!(attributes, &block) }
+ end
end
def first_or_create(attributes = nil, &block) # :nodoc:
@@ -203,23 +159,12 @@ module ActiveRecord
# failed due to validation errors it won't be persisted, you get what
# #create returns in such situation.
#
- # Please note *this method is not atomic*, it runs first a SELECT, and if
+ # Please note <b>this method is not atomic</b>, it runs first a SELECT, and if
# there are no results an INSERT is attempted. If there are other threads
# or processes there is a race condition between both calls and it could
# be the case that you end up with two similar records.
#
- # Whether that is a problem or not depends on the logic of the
- # application, but in the particular case in which rows have a UNIQUE
- # constraint an exception may be raised, just retry:
- #
- # begin
- # CreditAccount.transaction(requires_new: true) do
- # CreditAccount.find_or_create_by(user_id: user.id)
- # end
- # rescue ActiveRecord::RecordNotUnique
- # retry
- # end
- #
+ # If this might be a problem for your application, please see #create_or_find_by.
def find_or_create_by(attributes, &block)
find_by(attributes) || create(attributes, &block)
end
@@ -231,6 +176,51 @@ module ActiveRecord
find_by(attributes) || create!(attributes, &block)
end
+ # Attempts to create a record with the given attributes in a table that has a unique constraint
+ # on one or several of its columns. If a row already exists with one or several of these
+ # unique constraints, the exception such an insertion would normally raise is caught,
+ # and the existing record with those attributes is found using #find_by!.
+ #
+ # This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT
+ # and the INSERT, as that method needs to first query the table, then attempt to insert a row
+ # if none is found.
+ #
+ # There are several drawbacks to #create_or_find_by, though:
+ #
+ # * The underlying table must have the relevant columns defined with unique constraints.
+ # * A unique constraint violation may be triggered by only one, or at least less than all,
+ # of the given attributes. This means that the subsequent #find_by! may fail to find a
+ # matching record, which will then raise an <tt>ActiveRecord::RecordNotFound</tt> exception,
+ # rather than a record with the given attributes.
+ # * While we avoid the race condition between SELECT -> INSERT from #find_or_create_by,
+ # we actually have another race condition between INSERT -> SELECT, which can be triggered
+ # if a DELETE between those two statements is run by another client. But for most applications,
+ # that's a significantly less likely condition to hit.
+ # * It relies on exception handling to handle control flow, which may be marginally slower.
+ # * The primary key may auto-increment on each create, even if it fails. This can accelerate
+ # the problem of running out of integers, if the underlying table is still stuck on a primary
+ # key of type int (note: All Rails apps since 5.1+ have defaulted to bigint, which is not liable
+ # to this problem).
+ #
+ # 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)
@@ -245,16 +235,16 @@ 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
@@ -266,10 +256,6 @@ module ActiveRecord
coder.represent_seq(nil, records)
end
- def as_json(options = nil) #:nodoc:
- records.as_json(options)
- end
-
# Returns size of the records.
def size
loaded? ? @records.length : count(:all)
@@ -278,13 +264,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.
@@ -347,10 +327,14 @@ 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
+ already_in_scope? ? yield : _scoping(self) { yield }
+ end
+
+ def _exec_scope(name, *args, &block) # :nodoc:
+ @delegate_to_klass = true
+ _scoping(_deprecated_spawn(name)) { 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
@@ -358,6 +342,8 @@ module ActiveRecord
# trigger Active Record callbacks or validations. However, values passed to #update_all will still go through
# Active Record's normal type casting and serialization.
#
+ # Note: As Active Record callbacks are not triggered, this method will not automatically update +updated_at+/+updated_on+ columns.
+ #
# ==== Parameters
#
# * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
@@ -372,69 +358,91 @@ 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)
+ if klass.locking_enabled? &&
+ !updates.key?(klass.locking_column) &&
+ !updates.key?(klass.locking_column.to_sym)
+ attr = arel_attribute(klass.locking_column)
+ updates[attr.name] = _increment_attribute(attr)
+ end
+ 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)
+ @klass.connection.update stmt, "#{@klass} Update All"
+ end
- if joins_values.any?
- @klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key))
+ def update(id = :all, attributes) # :nodoc:
+ if id == :all
+ each { |record| record.update(attributes) }
else
- stmt.key = arel_attribute(primary_key)
- 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)
+ updates[attr.name] = _increment_attribute(attr, value)
end
- @klass.connection.update stmt, 'SQL', bound_attributes
+ if touch
+ names = touch if touch != true
+ touch_updates = klass.touch_attributes_with_time(*names)
+ updates.merge!(touch_updates) unless touch_updates.empty?
+ end
+
+ 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+/+updated_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+/+updated_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'"
#
- # # 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. So if it is not needed to run callbacks for each update, it is
- # preferred to use #update_all 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
- records.each { |record| record.update(attributes) }
- else
- if ActiveRecord::Base === id
- id = id.id
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- You are passing an instance of ActiveRecord::Base to `update`.
- Please pass the id of the object by calling `.id`
- MSG
- end
- object = find(id)
- object.update(attributes)
- object
- end
+ # # Touch multiple records with a specified time
+ # Person.all.touch_all(time: Time.new(2020, 5, 16, 0, 0, 0))
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2020-05-16 00:00:00'"
+ #
+ # # Touch records with scope
+ # Person.where(name: 'David').touch_all
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'"
+ def touch_all(*names, time: nil)
+ update_all klass.touch_attributes_with_time(*names, time: time)
end
# Destroys the records by instantiating each
@@ -453,43 +461,8 @@ module ActiveRecord
# ==== Examples
#
# Person.where(age: 0..18).destroy_all
- def destroy_all(conditions = nil)
- if conditions
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- Passing conditions to destroy_all is deprecated and will be removed in Rails 5.1.
- To achieve the same use where(conditions).destroy_all
- MESSAGE
- where(conditions).destroy_all
- else
- records.each(&:destroy).tap { reset }
- end
- 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+ - 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 without instantiating the records
@@ -508,66 +481,60 @@ module ActiveRecord
#
# If an invalid method is supplied, #delete_all raises an ActiveRecordError:
#
- # Post.limit(100).delete_all
- # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
- 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
- }
+ # 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
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- Passing conditions to delete_all is deprecated and will be removed in Rails 5.1.
- To achieve the same use where(conditions).delete_all
- MESSAGE
- where(conditions).delete_all
- else
- stmt = Arel::DeleteManager.new
- stmt.from(table)
+ if eager_loading?
+ relation = apply_join_dependency
+ return relation.delete_all
+ end
- if joins_values.any?
- @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key))
- else
- stmt.wheres = arel.constraints
- end
+ 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
- affected = @klass.connection.delete(stmt, 'SQL', bound_attributes)
+ affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
- reset
- affected
- end
+ reset
+ affected
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.
+ # Finds and destroys all records matching the specified conditions.
+ # This is short-hand for <tt>relation.where(condition).destroy_all</tt>.
+ # Returns the collection of objects that were destroyed.
#
- # 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.
+ # If no record is found, returns empty array.
#
- # ==== Examples
+ # Person.destroy_by(id: 13)
+ # Person.destroy_by(name: 'Spartacus', rating: 4)
+ # Person.destroy_by("published_at < ?", 2.weeks.ago)
+ def destroy_by(*args)
+ where(*args).destroy_all
+ end
+
+ # Finds and deletes all records matching the specified conditions.
+ # This is short-hand for <tt>relation.where(condition).delete_all</tt>.
+ # Returns the number of rows affected.
#
- # # Delete a single row
- # Todo.delete(1)
+ # If no record is found, returns <tt>0</tt> as zero rows were affected.
#
- # # Delete multiple rows
- # Todo.delete([2,3,4])
- def delete(id_or_array)
- where(primary_key => id_or_array).delete_all
+ # Person.delete_by(id: 13)
+ # Person.delete_by(name: 'Spartacus', rating: 4)
+ # Person.delete_by("published_at < ?", 2.weeks.ago)
+ def delete_by(*args)
+ where(*args).delete_all
end
# Causes the records to be loaded from the database if they have not
@@ -576,8 +543,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
@@ -589,8 +556,9 @@ module ActiveRecord
end
def reset
- @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
- @should_eager_load = @join_dependency = nil
+ @delegate_to_klass = false
+ @_deprecated_scope_source = nil
+ @to_sql = @arel = @loaded = @should_eager_load = nil
@records = [].freeze
@offsets = {}
self
@@ -602,32 +570,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.
@@ -645,15 +609,6 @@ module ActiveRecord
includes_values & joins_values
end
- # {#uniq}[rdoc-ref:QueryMethods#uniq] and
- # {#uniq!}[rdoc-ref:QueryMethods#uniq!] are silently deprecated.
- # #uniq_value delegates to #distinct_value to maintain backwards compatibility.
- # Use #distinct_value instead.
- def uniq_value
- distinct_value
- end
- deprecate uniq_value: :distinct_value
-
# Compares two relations for equality.
def ==(other)
case other
@@ -667,7 +622,7 @@ module ActiveRecord
end
def pretty_print(q)
- q.pp(self.records)
+ q.pp(records)
end
# Returns true if relation is blank.
@@ -676,17 +631,45 @@ module ActiveRecord
end
def values
- Hash[@values]
+ @values.dup
end
def inspect
- entries = records.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
+ def empty_scope? # :nodoc:
+ @values == klass.unscoped.values
+ end
+
+ def has_limit_or_offset? # :nodoc:
+ limit_value || offset_value
+ end
+
+ def alias_tracker(joins = [], aliases = nil) # :nodoc:
+ joins += [aliases] if aliases
+ ActiveRecord::Associations::AliasTracker.create(connection, table.name, joins)
+ end
+
+ def preload_associations(records) # :nodoc:
+ preload = preload_values
+ preload += includes_values unless eager_loading?
+ preloader = nil
+ preload.each do |associations|
+ preloader ||= build_preloader
+ preloader.preload records, associations
+ end
+ end
+
+ attr_reader :_deprecated_scope_source # :nodoc:
+
protected
+ attr_writer :_deprecated_scope_source # :nodoc:
def load_records(records)
@records = records.freeze
@@ -694,49 +677,111 @@ module ActiveRecord
end
private
+ def already_in_scope?
+ @delegate_to_klass && begin
+ scope = klass.current_scope(true)
+ scope && !scope._deprecated_scope_source
+ end
+ end
- def exec_queries
- @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes).freeze
+ def _deprecated_spawn(name)
+ spawn.tap { |scope| scope._deprecated_scope_source = name }
+ end
- preload = preload_values
- preload += includes_values unless eager_loading?
- preloader = build_preloader
- preload.each do |associations|
- preloader.preload @records, associations
+ def _deprecated_scope_block(name, &block)
+ -> record do
+ klass.current_scope = _deprecated_spawn(name)
+ yield record if block_given?
+ end
end
- @records.each(&:readonly!) if readonly_value
+ def _scoping(scope)
+ previous, klass.current_scope = klass.current_scope(true), scope
+ yield
+ ensure
+ klass.current_scope = previous
+ end
- @loaded = true
- @records
- end
+ 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 build_preloader
- ActiveRecord::Associations::Preloader.new
- end
+ def _increment_attribute(attribute, value = 1)
+ bind = predicate_builder.build_bind_attribute(attribute.name, value.abs)
+ expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attribute), 0)
+ expr = value < 0 ? expr - bind : expr + bind
+ expr.expr
+ end
+
+ 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 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 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 b99807adf3..9c579843b1 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
require "active_record/relation/batches/batch_enumerator"
module ActiveRecord
module Batches
- ORDER_OR_LIMIT_IGNORED_MESSAGE = "Scoped order and limit are ignored, it's forced to be batch order and batch size"
+ 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 Scoping::Named::ClassMethods.all method, for example)
@@ -30,19 +32,28 @@ module ActiveRecord
# end
#
# ==== Options
- # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
+ # * <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
- # the order and limit have to be ignored due to batching.
+ # 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.
#
- # 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 +: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
#
- # # Let's process for a batch of 2000 records, skipping the first 2000 rows
- # Person.find_each(start: 2000, batch_size: 2000) do |person|
+ # # 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
#
@@ -51,8 +62,8 @@ 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.
+ # 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(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do |records|
@@ -85,19 +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>: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
- # the order and limit have to be ignored due to batching.
+ # 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.
#
- # 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 +:start+ and +:finish+ option on each worker).
+ # 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 the next 2000 records
- # Person.find_in_batches(start: 2000, batch_size: 2000) do |group|
+ # # 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
#
@@ -106,8 +121,8 @@ 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.
+ # 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?
@@ -132,9 +147,9 @@ module ActiveRecord
# If you do not provide a block to #in_batches, it will return a
# BatchEnumerator which is enumerable.
#
- # Person.in_batches.with_index do |relation, batch_index|
+ # Person.in_batches.each_with_index do |relation, batch_index|
# puts "Processing relation ##{batch_index}"
- # relation.each { |relation| relation.delete_all }
+ # relation.delete_all
# end
#
# Examples of calling methods on the returned BatchEnumerator object:
@@ -144,22 +159,24 @@ module ActiveRecord
# Person.in_batches.each_record(&:party_all_night!)
#
# ==== Options
- # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000.
- # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false.
+ # * <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
- # the order and limit have to be ignored due to batching.
+ # 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.
#
- # This is especially useful if you want to work with the
- # ActiveRecord::Relation object instead of the array of records, or 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 +:start+ and +:finish+
- # option on each worker).
+ # 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 the next 2000 records
- # Person.in_batches(of: 2000, start: 2000).update_all(awesome: true)
+ # # 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:
#
@@ -176,34 +193,41 @@ module ActiveRecord
#
# 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
+ # consistent. Therefore the primary key must be orderable, e.g. an integer
# or a string.
#
- # NOTE: You can't set the limit either, that's used to control the batch
- # sizes.
+ # 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? || arel.taken.present?
- act_on_order_or_limit_ignored(error_on_ignore)
+ 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(of)
+ 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 = self.where(primary_key => ids)
+ yielded_relation = where(primary_key => ids)
yielded_relation.load_records(records)
else
ids = batch_relation.pluck(primary_key)
- yielded_relation = self.where(primary_key => ids)
+ yielded_relation = where(primary_key => ids)
end
break if ids.empty?
@@ -213,31 +237,54 @@ module ActiveRecord
yield yielded_relation
- break if ids.length < of
- batch_relation = relation.where(arel_attribute(primary_key).gt(primary_key_offset))
+ break if ids.length < batch_limit
+
+ 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, start, finish)
- relation = relation.where(arel_attribute(primary_key).gteq(start)) if start
- relation = relation.where(arel_attribute(primary_key).lteq(finish)) if finish
- 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 act_on_order_or_limit_ignored(error_on_ignore)
- raise_error = (error_on_ignore.nil? ? self.klass.error_on_ignored_order_or_limit : error_on_ignore)
+ def apply_finish_limit(relation, finish)
+ relation.where(bind_attribute(primary_key, finish) { |attr, bind| attr.lteq(bind) })
+ end
- if raise_error
- raise ArgumentError.new(ORDER_OR_LIMIT_IGNORED_MESSAGE)
- elsif logger
- logger.warn(ORDER_OR_LIMIT_IGNORED_MESSAGE)
+ 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
end
diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb
index 13393dc605..49697da3bf 100644
--- a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb
+++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Batches
class BatchEnumerator
@@ -7,7 +9,7 @@ module ActiveRecord
@of = of
@relation = relation
@start = start
- @finish = finish
+ @finish = finish
end
# Looping through a collection of records from the database (using the
@@ -42,7 +44,7 @@ module ActiveRecord
# Delegates #delete_all, #update_all, #destroy_all methods to each batch.
#
# People.in_batches.delete_all
- # People.in_batches.destroy_all('age < 10')
+ # 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|
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 54c9af4898..4f9ddf302e 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.
@@ -37,7 +39,15 @@ module ActiveRecord
# Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ
# between databases. In invalid cases, an error from the database is thrown.
def count(column_name = nil)
- calculate(:count, column_name)
+ if block_given?
+ unless column_name.nil?
+ raise ArgumentError, "Column name argument is not supported when a block is passed."
+ end
+
+ super()
+ else
+ calculate(:count, column_name)
+ end
end
# Calculates the average value on a given column. Returns +nil+ if there's
@@ -71,9 +81,16 @@ module ActiveRecord
# #calculate for examples with options.
#
# Person.sum(:age) # => 4562
- def sum(column_name = nil, &block)
- return super(&block) if block_given?
- calculate(:sum, column_name)
+ def sum(column_name = nil)
+ if block_given?
+ unless column_name.nil?
+ raise ArgumentError, "Column name argument is not supported when a block is passed."
+ end
+
+ super()
+ else
+ calculate(:sum, column_name)
+ end
end
# This calculates aggregate values in the given column. Methods for #count, #sum, #average,
@@ -89,7 +106,7 @@ module ActiveRecord
#
# There are two basic forms of output:
#
- # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float
+ # * 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.
#
# * Grouped values: This returns an ordered hash of the values and groups them. It
@@ -108,12 +125,18 @@ module ActiveRecord
# ...
# 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
@@ -156,21 +179,39 @@ module ActiveRecord
#
def pluck(*column_names)
if loaded? && (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty?
- return @records.pluck(*column_names)
+ 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
+ klass.disallow_raw_sql!(column_names)
relation = spawn
- relation.select_values = column_names.map { |cn|
- @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn
- }
- result = klass.connection.select_all(relation.arel, nil, bound_attributes)
+ relation.select_values = column_names
+ result = skip_query_cache_if_necessary { klass.connection.select_all(relation.arel, nil) }
result.cast_values(klass.attribute_types)
end
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
@@ -180,202 +221,211 @@ module ActiveRecord
end
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
-
- # If #count is used with #distinct (i.e. `relation.distinct.count`) it is
- # considered distinct.
- distinct = self.distinct_value
-
- if operation == "count"
- column_name ||= select_for_count
-
- unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
- distinct = true
+ 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
+ distinct = distinct_select?(select_for_count) if group_values.empty?
+ elsif group_values.any? || select_values.empty? && order_values.empty?
+ column_name = primary_key
+ end
+ elsif distinct_select?(column_name)
+ distinct = nil
+ end
end
- column_name = primary_key if column_name == :all && distinct
- distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i
+ if group_values.any?
+ execute_grouped_calculation(operation, column_name, distinct)
+ else
+ execute_simple_calculation(operation, column_name, distinct)
+ end
end
- if group_values.any?
- execute_grouped_calculation(operation, column_name, distinct)
- else
- execute_simple_calculation(operation, column_name, distinct)
+ def distinct_select?(column_name)
+ column_name.is_a?(::String) && /\bDISTINCT[\s(]/i.match?(column_name)
end
- end
- def aggregate_column(column_name)
- return column_name if Arel::Expressions === column_name
+ def aggregate_column(column_name)
+ return column_name if Arel::Expressions === 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)
+ 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
end
- end
-
- def operation_over_aggregate_column(column, operation, distinct)
- operation == 'count' ? column.count(distinct) : column.send(operation)
- end
-
- def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
- # PostgreSQL doesn't like ORDER BY when there are no GROUP BY
- relation = unscope(:order)
- column_alias = column_name
+ def operation_over_aggregate_column(column, operation, distinct)
+ operation == "count" ? column.count(distinct) : column.send(operation)
+ end
- if operation == "count" && (relation.limit_value || relation.offset_value)
- # Shortcut when limit is zero.
- return 0 if relation.limit_value == 0
+ def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
+ column_alias = column_name
- query_builder = build_count_subquery(relation, column_name, distinct)
- else
- column = aggregate_column(column_name)
+ if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?)
+ # Shortcut when limit is zero.
+ return 0 if limit_value == 0
- select_value = operation_over_aggregate_column(column, operation, distinct)
+ 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 = select_value.alias
- column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
- relation.select_values = [select_value]
+ column = aggregate_column(column_name)
- query_builder = relation.arel
- end
+ select_value = operation_over_aggregate_column(column, operation, distinct)
+ if operation == "sum" && distinct
+ select_value.distinct = true
+ 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
+ column_alias = select_value.alias
+ column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
+ relation.select_values = [select_value]
- type_cast_calculated_value(value, column, operation)
- end
+ query_builder = relation.arel
+ end
- def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
- group_attrs = group_values
+ 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
- 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
+ type_cast_calculated_value(value, type, operation)
end
- group_fields = arel_columns(group_fields)
- group_aliases = group_fields.map { |field| column_alias_for(field) }
- group_columns = group_aliases.zip(group_fields)
+ def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
+ group_attrs = group_values
- if operation == 'count' && column_name == :all
- aggregate_alias = 'count_all'
- else
- aggregate_alias = column_alias_for([operation, column_name].join(' '))
- end
-
- 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_columns.map { |aliaz, field|
- if field.respond_to?(:as)
- field.as(aliaz)
+ 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
- "#{field} AS #{aliaz}"
+ group_fields = group_attrs
end
- }
+ group_fields = arel_columns(group_fields)
- relation = except(:group)
- relation.group_values = group_fields
- relation.select_values = select_values
+ group_aliases = group_fields.map { |field| column_alias_for(field) }
+ group_columns = group_aliases.zip(group_fields)
- 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] }]
- end
+ if operation == "count" && column_name == :all
+ aggregate_alias = "count_all"
+ else
+ aggregate_alias = column_alias_for([operation, column_name].join(" "))
+ 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)
+ 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
- type_cast_calculated_value(row[aliaz], column)
}
- key = key.first if key.size == 1
- key = key_records[key] if associated
- 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
+ relation = except(:group).distinct!(false)
+ relation.group_values = group_fields
+ relation.select_values = select_values
- # 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}"
+ calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, nil) }
+
+ 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
+
+ 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
- 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"
+ def column_alias_for(keys)
+ if keys.respond_to? :name
+ keys = "#{keys.relation.name}.#{keys.name}"
+ end
- @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!(/ +/, "_")
- 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
+ @klass.connection.table_alias_for(table_name)
+ 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_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
else type.deserialize(value)
+ end
end
- end
- def select_for_count
- if select_values.present?
- return select_values.first if select_values.one?
- 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 f2578f5f96..7a53a9d1c7 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -1,5 +1,6 @@
-require 'set'
-require 'active_support/concern'
+# frozen_string_literal: true
+
+require "mutex_m"
module ActiveRecord
module Delegation # :nodoc:
@@ -18,7 +19,11 @@ module ActiveRecord
delegate = Class.new(klass) {
include ClassSpecificRelation
}
- const_set klass.name.gsub('::'.freeze, '_'.freeze), 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,7 +32,45 @@ module ActiveRecord
child_class.initialize_relation_delegate_cache
super
end
+
+ def generate_relation_method(method)
+ generated_relation_methods.generate_method(method)
+ 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 ||= GeneratedRelationMethods.new
+ end
+ end
+
+ class GeneratedRelationMethods < Module # :nodoc:
+ include Mutex_m
+
+ def generate_method(method)
+ synchronize do
+ return if method_defined?(method)
+
+ if /\A[a-zA-Z_]\w*[!?]?\z/.match?(method)
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{method}(*args, &block)
+ scoping { klass.#{method}(*args, &block) }
+ end
+ RUBY
+ else
+ define_method(method) do |*args, &block|
+ scoping { klass.public_send(method, *args, &block) }
+ end
+ end
+ end
+ end
end
+ private_constant :GeneratedRelationMethods
extend ActiveSupport::Concern
@@ -36,64 +79,32 @@ module ActiveRecord
# may vary depending on the klass of a relation, so we create a subclass of Relation
# for each different klass, and the delegations are compiled into that subclass only.
- delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join,
- :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of,
- :shuffle, :split, to: :records
+ 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 :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
- included do
- @delegation_mutex = Mutex.new
- end
-
module ClassMethods # :nodoc:
def name
superclass.name
end
+ end
- def delegate_to_scoped_klass(method)
- @delegation_mutex.synchronize do
- return if method_defined?(method)
-
- if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
- def #{method}(*args, &block)
- scoping { @klass.#{method}(*args, &block) }
- end
- RUBY
- else
- define_method method do |*args, &block|
- scoping { @klass.public_send(method, *args, &block) }
- end
- end
- end
- end
+ private
- def delegate(method, opts = {})
- @delegation_mutex.synchronize do
- return if method_defined?(method)
+ def method_missing(method, *args, &block)
+ if @klass.respond_to?(method)
+ @klass.generate_relation_method(method)
+ scoping { @klass.public_send(method, *args, &block) }
+ else
super
end
end
- end
-
- protected
-
- 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
- end
- end
end
module ClassMethods # :nodoc:
@@ -103,26 +114,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) ||
- arel.respond_to?(method, include_private)
+ def relation_class_for(klass)
+ klass.relation_delegate_class(self)
+ end
end
- protected
-
- def method_missing(method, *args, &block)
- if @klass.respond_to?(method)
- scoping { @klass.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)
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 27dd0b4143..e2efd4aa0d 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -1,12 +1,14 @@
-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
- # is an integer, find by id coerces its arguments using +to_i+.
+ # If one or more records cannot be found for the requested ids, then ActiveRecord::RecordNotFound will be raised.
+ # If the primary key is an integer, find by id coerces its arguments by using +to_i+.
#
# Person.find(1) # returns the object for ID = 1
# Person.find("1") # returns the object for ID = 1
@@ -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 QueryMethods#order
- # 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
#
@@ -76,17 +79,12 @@ module ActiveRecord
# Post.find_by "published_at < ?", 2.weeks.ago
def find_by(arg, *args)
where(arg, *args).take
- rescue RangeError
- nil
end
# 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.new("Couldn't find #{@klass.name} with an out of range value",
- @klass.name)
end
# Gives a record (or N records if a parameter is supplied) without any implied
@@ -97,13 +95,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 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).
@@ -117,7 +115,7 @@ module ActiveRecord
#
def first(limit = nil)
if limit
- find_nth_with_limit_and_offset(0, limit, offset: offset_index)
+ find_nth_with_limit(0, limit)
else
find_nth 0
end
@@ -126,7 +124,7 @@ module ActiveRecord
# 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).
@@ -145,27 +143,18 @@ module ActiveRecord
#
# [#<Person id:4>, #<Person id:3>, #<Person id:2>]
def last(limit = nil)
- return find_last(limit) if loaded? || limit_value
+ return find_last(limit) if loaded? || has_limit_or_offset?
- result = limit(limit || 1)
- result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key
+ result = ordered_relation.limit(limit)
result = result.reverse_order!
limit ? result.reverse : result.first
- rescue ActiveRecord::IrreversibleOrderError
- ActiveSupport::Deprecation.warn(<<-WARNING.squish)
- Finding a last element by loading the relation when SQL ORDER
- can not be reversed is deprecated.
- Rails 5.1 will raise ActiveRecord::IrreversibleOrderError in this case.
- Please call `to_a.last` if you still want to load the relation.
- WARNING
- find_last(limit)
end
# Same as #last but raises ActiveRecord::RecordNotFound if no record
# 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.
@@ -181,7 +170,7 @@ module ActiveRecord
# 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.
@@ -197,7 +186,7 @@ module ActiveRecord
# 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.
@@ -213,7 +202,7 @@ module ActiveRecord
# 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.
@@ -229,7 +218,7 @@ module ActiveRecord
# 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".
@@ -245,7 +234,7 @@ module ActiveRecord
# 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.
@@ -261,7 +250,7 @@ module ActiveRecord
# Same as #third_to_last but raises ActiveRecord::RecordNotFound if no record
# is found.
def third_to_last!
- find_nth_from_last 3 or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
+ third_to_last || raise_record_not_found_exception!
end
# Find the second-to-last record.
@@ -277,7 +266,7 @@ module ActiveRecord
# Same as #second_to_last but raises ActiveRecord::RecordNotFound if no record
# is found.
def second_to_last!
- find_nth_from_last 2 or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
+ second_to_last || raise_record_not_found_exception!
end
# Returns true if a record exists in the table that matches the +id+ or
@@ -291,7 +280,7 @@ module ActiveRecord
# * Hash - Finds the record that matches these +find+-style conditions
# (such as <tt>{name: 'David'}</tt>).
# * +false+ - Returns always +false+.
- # * No args - Returns +false+ if the table is empty, +true+ otherwise.
+ # * No args - Returns +false+ if the relation is empty, +true+ otherwise.
#
# For more information about specifying conditions as a hash or array,
# see the Conditions section in the introduction to ActiveRecord::Base.
@@ -307,32 +296,25 @@ 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
-
- 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)
+ return false if !conditions || limit_value == 0
- 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_one(relation.arel, "#{name} Exists") } ? true : false
end
# This method is called whenever no records are found with either a single
@@ -343,256 +325,232 @@ module ActiveRecord
# of results obtained should be provided in the +result_size+ argument and
# the expected number of results should be provided in the +expected_size+
# argument.
- def raise_record_not_found_exception!(ids, 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)
+ conditions = sanitize_forbidden_attributes(conditions)
+
+ if distinct_value && offset_value
+ relation = limit(1)
else
- arel = relation.arel
- rows = connection.select_all(arel, 'SQL', relation.bound_attributes)
- join_dependency.instantiate(rows, aliases)
+ relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
end
- end
- end
- def construct_join_dependency(joins = [])
- including = eager_load_values + includes_values
- ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
- end
+ case conditions
+ when Array, Hash
+ relation.where!(conditions) unless conditions.empty?
+ else
+ relation.where!(primary_key => conditions) unless conditions == :none
+ 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))
+ relation
end
- end
- def apply_join_dependency(relation, join_dependency)
- relation = relation.except(:includes, :eager_load, :preload)
- relation = relation.joins join_dependency
+ def construct_join_dependency(associations)
+ ActiveRecord::Associations::JoinDependency.new(
+ klass, table, associations
+ )
+ end
- 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)
+ 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 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
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
- end
+ 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
+ relation = where(primary_key => id)
+ record = relation.take
- raise_record_not_found_exception!(id, 0, 1) unless record
+ raise_record_not_found_exception!(id, 0, 1) unless record
- record
- end
+ record
+ end
- def find_some(ids)
- return find_some_ordered(ids) unless order_values.present?
+ def find_some(ids)
+ return find_some_ordered(ids) unless order_values.present?
- result = where(primary_key => ids).to_a
+ result = where(primary_key => ids).to_a
- expected_size =
- if limit_value && ids.size > limit_value
- limit_value
- else
- ids.size
- end
+ expected_size =
+ if limit_value && ids.size > limit_value
+ limit_value
+ else
+ ids.size
+ end
- # 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
+ # 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
- if result.size == expected_size
- result
- else
- raise_record_not_found_exception!(ids, result.size, expected_size)
+ if result.size == expected_size
+ result
+ else
+ raise_record_not_found_exception!(ids, result.size, expected_size)
+ end
end
- end
- def find_some_ordered(ids)
- ids = ids.slice(offset_value || 0, limit_value || ids.size) || []
+ def find_some_ordered(ids)
+ ids = ids.slice(offset_value || 0, limit_value || ids.size) || []
- result = except(:limit, :offset).where(primary_key => ids).records
+ result = except(:limit, :offset).where(primary_key => ids).records
- if result.size == ids.size
- pk_type = @klass.type_for_attribute(primary_key)
+ if result.size == ids.size
+ pk_type = @klass.type_for_attribute(primary_key)
- 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)
+ 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
- end
- def find_take
- if loaded?
- @records.first
- else
- @take ||= limit(1).records.first
+ def find_take
+ if loaded?
+ records.first
+ else
+ @take ||= limit(1).records.first
+ end
end
- end
- def find_nth(index, offset = nil)
- # TODO: once the offset argument is removed we rely on offset_index
- # within find_nth_with_limit, rather than pass it in via
- # find_nth_with_limit_and_offset
- if offset
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing an offset argument to find_nth is deprecated,
- please use Relation#offset instead.
- MSG
+ def find_take_with_limit(limit)
+ if loaded?
+ records.take(limit)
+ else
+ limit(limit).to_a
+ end
end
- if loaded?
- @records[index]
- else
- offset ||= offset_index
- @offsets[offset + index] ||= find_nth_with_limit_and_offset(index, 1, offset: offset).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) 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
+
+ if limit_value
+ limit = [limit_value - index, limit].min
+ end
+
+ if limit > 0
+ relation = relation.offset(offset_index + index) unless index.zero?
+ relation.limit(limit).to_a
+ else
+ []
+ end
+ end
+ end
- def find_nth_with_limit(index, limit)
- # TODO: once the offset argument is removed from find_nth,
- # find_nth_with_limit_and_offset can be merged into this method
- relation = if order_values.empty? && primary_key
- order(arel_attribute(primary_key).asc)
- else
- self
- end
-
- relation = relation.offset(index) unless index.zero?
- relation.limit(limit).to_a
- end
+ def find_nth_from_last(index)
+ if loaded?
+ records[-index]
+ else
+ relation = ordered_relation
- def find_nth_from_last(index)
- if loaded?
- @records[-index]
- else
- relation = if order_values.empty? && primary_key
- order(arel_attribute(primary_key).asc)
- else
- self
- end
-
- relation.to_a[-index]
- # TODO: can be made more performant on large result sets by
- # for instance, last(index)[-index] (which would require
- # refactoring the last(n) finder method to make test suite pass),
- # or by using a combination of reverse_order, limit, and offset,
- # e.g., reverse_order.offset(index-1).first
+ if equal?(relation) || has_limit_or_offset?
+ relation.records[-index]
+ else
+ relation.last(index)[-index]
+ end
+ end
end
- end
-
- private
- def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc:
- if loaded?
- @records[index, limit]
- else
- index += offset
- find_nth_with_limit(index, limit)
+ def find_last(limit)
+ limit ? records.last(limit) : records.last
end
- end
- def find_last(limit)
- limit ? records.last(limit) : records.last
- end
+ def ordered_relation
+ if order_values.empty? && (implicit_order_column || primary_key)
+ order(arel_attribute(implicit_order_column || primary_key).asc)
+ else
+ self
+ 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 8945cb0cc5..c53a682aee 100644
--- a/activerecord/lib/active_record/relation/from_clause.rb
+++ b/activerecord/lib/active_record/relation/from_clause.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
class Relation
class FromClause # :nodoc:
@@ -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
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 396638d74d..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,91 +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
+ end
- unless other.includes_values.empty?
- relation.includes! reflection.name => other.includes_values
+ unless other.includes_values.empty?
+ relation.includes! reflection.name => other.includes_values
+ end
end
end
- end
- def merge_joins
- return if other.joins_values.blank?
+ def merge_joins
+ return if other.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.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
- join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass,
- joins_dependency,
- [])
- relation.joins! rest
+ def merge_outer_joins
+ return if other.left_outer_joins_values.blank?
- @relation = relation.joins join_dependency
- end
- end
+ 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
- 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
+ relation.left_outer_joins!(*joins_dependency)
+ end
end
- relation.extend(*other.extending_values) unless other.extending_values.blank?
- 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.any?
+ # merge in order_values from relation
+ relation.order!(*other.order_values)
+ end
- def merge_single_values
- if relation.from_clause.empty?
- relation.from_clause = other.from_clause
+ extensions = other.extensions - relation.extensions
+ relation.extending!(*extensions) if extensions.any?
end
- relation.lock_value ||= other.lock_value
- unless other.create_with_value.blank?
- relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value)
+ 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
- end
- CLAUSE_METHOD_NAMES = CLAUSE_METHODS.map do |name|
- ["#{name}_clause", "#{name}_clause="]
- end
+ def merge_clauses
+ relation.from_clause = other.from_clause if replace_from_clause?
+
+ where_clause = relation.where_clause.merge(other.where_clause)
+ relation.where_clause = where_clause unless where_clause.empty?
- def merge_clauses
- CLAUSE_METHOD_NAMES.each do |(reader, writer)|
- clause = relation.send(reader)
- other_clause = other.send(reader)
- relation.send(writer, clause.merge(other_clause))
+ having_clause = relation.having_clause.merge(other.having_clause)
+ relation.having_clause = having_clause unless having_clause.empty?
+ end
+
+ 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 550416238f..240de3bb69 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,14 +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/polymorphic_array_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)
@@ -16,14 +9,11 @@ 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(RangeHandler::RangeWithBinds, RangeHandler.new(self))
register_handler(Relation, RelationHandler.new)
register_handler(Array, ArrayHandler.new(self))
- register_handler(AssociationQueryValue, AssociationQueryHandler.new(self))
- register_handler(PolymorphicArrayValue, PolymorphicArrayHandler.new(self))
+ register_handler(Set, ArrayHandler.new(self))
end
def build_from_hash(attributes)
@@ -31,28 +21,13 @@ module ActiveRecord
expand_from_hash(attributes)
end
- def create_binds(attributes)
- attributes = convert_dot_notation_to_hash(attributes)
- 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)
- value = AssociationQueryHandler.value_for(table, column, value) if table.associated_with?(column)
- 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('.'.freeze).first if key.include?('.'.freeze)
+ key.split(".").first if key.include?(".")
end
end.compact
end
@@ -73,97 +48,103 @@ module ActiveRecord
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|
- key = key.to_s
- 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
- when Range
- first = value.begin
- last = value.end
- unless first.respond_to?(:infinite?) && first.infinite?
- binds << build_bind_param(column_name, first)
- first = Arel::Nodes::BindParam.new
- end
- unless last.respond_to?(:infinite?) && last.infinite?
- binds << build_bind_param(column_name, last)
- last = Arel::Nodes::BindParam.new
- end
-
- result[column_name] = RangeHandler::RangeWithBinds.new(first, last, value.exclude_end?)
- else
- if can_be_bound?(column_name, value)
- result[column_name] = Arel::Nodes::BindParam.new
- binds << build_bind_param(column_name, value)
+ 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
+ values = value.nil? ? [nil] : Array.wrap(value)
+ if mapping.length == 1 || values.empty?
+ column_name, aggr_attr = mapping.first
+ values = values.map do |object|
+ object.respond_to?(aggr_attr) ? object.public_send(aggr_attr) : object
+ end
+ build(table.arel_attribute(column_name), values)
+ else
+ queries = values.map do |object|
+ mapping.map do |field_attr, aggregate_attr|
+ build(table.arel_attribute(field_attr), object.try!(aggregate_attr))
+ end.reduce(&:and)
+ end
+ queries.reduce(&:or)
+ end
+ 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 do |s|
- s.respond_to?(:include?) && s.include?(".".freeze)
+ 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(".".freeze)
- 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
-
- def handler_for(object)
- @handlers.detect { |klass, _| klass === object }.last
- end
+ attributes[table_name] = attributes[table_name].merge(column_name => value)
+ end
- def can_be_bound?(column_name, value)
- !value.nil? &&
- handler_for(value).is_a?(BasicObjectHandler) &&
- !table.associated_with?(column_name)
- end
+ attributes
+ end
- def build_bind_param(column_name, value)
- Relation::QueryAttribute.new(column_name.to_s, value, table.type(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 d7fd878265..0000000000
--- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-module ActiveRecord
- class PredicateBuilder
- class AssociationQueryHandler # :nodoc:
- def self.value_for(table, column, value)
- klass = if table.associated_table(column).polymorphic_association? && ::Array === value && value.first.is_a?(Base)
- PolymorphicArrayValue
- else
- AssociationQueryValue
- end
-
- klass.new(table.associated_table(column), value)
- end
-
- def initialize(predicate_builder)
- @predicate_builder = predicate_builder
- end
-
- def call(attribute, value)
- queries = {}
-
- table = value.associated_table
- if value.base_class
- queries[table.association_foreign_type.to_s] = value.base_class.name
- end
-
- queries[table.association_foreign_key.to_s] = 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_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb
deleted file mode 100644
index b6c6240343..0000000000
--- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-module ActiveRecord
- class PredicateBuilder
- class PolymorphicArrayHandler # :nodoc:
- def initialize(predicate_builder)
- @predicate_builder = predicate_builder
- end
-
- def call(attribute, value)
- table = value.associated_table
- queries = value.type_to_ids_mapping.map do |type, ids|
- { table.association_foreign_type.to_s => type, table.association_foreign_key.to_s => ids }
- end
-
- predicates = queries.map { |query| predicate_builder.build_from_hash(query) }
-
- if predicates.size > 1
- type_and_ids_predicates = predicates.map { |type_predicate, id_predicate| Arel::Nodes::Grouping.new(type_predicate.and(id_predicate)) }
- type_and_ids_predicates.inject(&:or)
- else
- predicates.first
- end
- end
-
- protected
-
- attr_reader :predicate_builder
- end
-
- class PolymorphicArrayValue # :nodoc:
- attr_reader :associated_table, :values
-
- def initialize(associated_table, values)
- @associated_table = associated_table
- @values = values
- end
-
- def type_to_ids_mapping
- default_hash = Hash.new { |hsh, key| hsh[key] = [] }
- values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) }
- end
-
- private
-
- def primary_key(value)
- associated_table.association_primary_key(base_class(value))
- end
-
- def base_class(value)
- value.class.base_class
- end
-
- def convert_to_id(value)
- value._read_attribute(primary_key(value))
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/relation/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 306d4694ae..2ea27c8490 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
class PredicateBuilder
class RangeHandler # :nodoc:
@@ -8,26 +10,13 @@ module ActiveRecord
end
def call(attribute, value)
- if value.begin.respond_to?(:infinite?) && value.begin.infinite?
- if value.end.respond_to?(:infinite?) && value.end.infinite?
- attribute.not_in([])
- elsif value.exclude_end?
- attribute.lt(value.end)
- else
- attribute.lteq(value.end)
- end
- elsif value.end.respond_to?(:infinite?) && value.end.infinite?
- attribute.gteq(value.begin)
- elsif value.exclude_end?
- attribute.gteq(value.begin).and(attribute.lt(value.end))
- else
- 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)
+ attribute.between(RangeWithBinds.new(begin_bind, end_bind, value.exclude_end?))
end
- protected
-
- 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 8a910a82fe..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,7 +1,13 @@
+# 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.arel_attribute(value.klass.primary_key))
end
diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb
index 7ba964e802..cd18f27330 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 # :nodoc:
+ class QueryAttribute < ActiveModel::Attribute # :nodoc:
def type_cast(value)
value
end
@@ -14,6 +16,35 @@ module ActiveRecord
def with_cast_value(value)
QueryAttribute.new(name, value, type)
end
+
+ def nil?
+ unless value_before_type_cast.is_a?(StatementCache::Substitute)
+ value_before_type_cast.nil? ||
+ type.respond_to?(:subtype, true) && value_for_database.nil?
+ end
+ rescue ::RangeError
+ end
+
+ def infinite?
+ infinity?(value_before_type_cast) || infinity?(value_for_database)
+ rescue ::RangeError
+ end
+
+ def unboundable?
+ if defined?(@_unboundable)
+ @_unboundable
+ else
+ value_for_database unless value_before_type_cast.is_a?(StatementCache::Substitute)
+ @_unboundable = nil
+ end
+ rescue ::RangeError
+ @_unboundable = type.cast(value_before_type_cast) <=> 0
+ 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 4533f3263f..b0535cfff5 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
@@ -55,73 +56,26 @@ module ActiveRecord
end
FROZEN_EMPTY_ARRAY = [].freeze
- Relation::MULTI_VALUE_METHODS.each do |name|
- class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}_values
- @values[:#{name}] || FROZEN_EMPTY_ARRAY
- end
+ FROZEN_EMPTY_HASH = {}.freeze
- def #{name}_values=(values)
- assert_mutability!
- @values[:#{name}] = values
+ 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
- CODE
- end
-
- (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |name|
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
- result = from_clause.binds + arel.bind_values + where_clause.binds + having_clause.binds
- if limit_value && !string_containing_comma?(limit_value)
- result << Attribute.with_cast_value(
- "LIMIT".freeze,
- connection.sanitize_limit(limit_value),
- Type::Value.new,
- )
- end
- if offset_value
- result << Attribute.with_cast_value(
- "OFFSET".freeze,
- offset_value.to_i,
- Type::Value.new,
- )
- end
- result
- end
-
- FROZEN_EMPTY_HASH = {}.freeze
- def create_with_value # :nodoc:
- @values[:create_with] || FROZEN_EMPTY_HASH
- end
-
alias extensions extending_values
# Specify relationships to be included in the result set. For
@@ -146,7 +100,7 @@ module ActiveRecord
#
# === conditions
#
- # If you want to add conditions to your included models you'll have
+ # If you want to add string conditions to your included models, you'll have
# to explicitly reference them. For example:
#
# User.includes(:posts).where('posts.name = ?', 'example')
@@ -157,6 +111,12 @@ module ActiveRecord
#
# Note that #includes works with association names while #references needs
# the actual table name.
+ #
+ # If you pass the conditions via hash, you don't need to call #references
+ # explicitly, as #where references the tables for you. For example, this
+ # will work correctly:
+ #
+ # User.includes(:posts).where(posts: { name: 'example' })
def includes(*args)
check_if_method_has_arguments!(:includes, args)
spawn.includes!(*args)
@@ -200,6 +160,19 @@ module ActiveRecord
self
end
+ # Extracts a named +association+ from the relation. The named association is first preloaded,
+ # then the individual association records are collected from the relation. Like so:
+ #
+ # account.memberships.extract_associated(:user)
+ # # => Returns collection of User records
+ #
+ # This is short-hand for:
+ #
+ # account.memberships.preload(:user).collect(&:user)
+ def extract_associated(association)
+ preload(association).collect(&association)
+ end
+
# 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.
@@ -225,12 +198,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:
@@ -263,12 +237,20 @@ module ActiveRecord
# 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).to_sym : field
@@ -277,6 +259,27 @@ module ActiveRecord
self
end
+ # Allows you to change a previously set select statement.
+ #
+ # Post.select(:title, :body)
+ # # SELECT `posts`.`title`, `posts`.`body` FROM `posts`
+ #
+ # Post.select(:title, :body).reselect(:created_at)
+ # # SELECT `posts`.`created_at` FROM `posts`
+ #
+ # This is short-hand for <tt>unscope(:select).select(fields)</tt>.
+ # Note that we're unscoping the entire select statement.
+ def reselect(*args)
+ check_if_method_has_arguments!(:reselect, args)
+ spawn.reselect!(*args)
+ end
+
+ # Same as #reselect but operates on relation in-place instead of copying.
+ def reselect!(*args) # :nodoc:
+ self.select_values = args
+ self
+ end
+
# Allows to specify a group attribute:
#
# User.group(:name)
@@ -333,6 +336,7 @@ module ActiveRecord
spawn.order!(*args)
end
+ # Same as #order but operates on relation in-place instead of copying.
def order!(*args) # :nodoc:
preprocess_order_args(args)
@@ -354,6 +358,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)
@@ -363,8 +368,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, :annotate,
+ :includes, :from, :readonly, :having, :optimizer_hints])
# 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
@@ -411,7 +416,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
@@ -476,20 +485,17 @@ module ActiveRecord
# => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
#
def left_outer_joins(*args)
- check_if_method_has_arguments!(:left_outer_joins, args)
-
- args.compact!
- args.flatten!
-
+ check_if_method_has_arguments!(__callee__, args)
spawn.left_outer_joins!(*args)
end
alias :left_joins :left_outer_joins
def left_outer_joins!(*args) # :nodoc:
+ args.compact!
+ args.flatten!
self.left_outer_joins_values += args
self
end
- alias :left_joins! :left_outer_joins!
# Returns a new relation, which is the result of filtering the current relation
# according to the conditions in the arguments.
@@ -652,7 +658,7 @@ module ActiveRecord
# present). Neither relation may have a #limit, #offset, or #distinct set.
#
# Post.where("id = 1").or(Post.where("author_id = 3"))
- # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'author_id = 3'))
+ # # SELECT `posts`.* FROM `posts` WHERE ((id = 1) OR (author_id = 3))
#
def or(other)
unless other.is_a? Relation
@@ -670,7 +676,8 @@ module ActiveRecord
end
self.where_clause = self.where_clause.or(other.where_clause)
- self.having_clause = self.having_clause.or(other.having_clause)
+ self.having_clause = having_clause.or(other.having_clause)
+ self.references_values += other.references_values
self
end
@@ -701,13 +708,6 @@ module ActiveRecord
end
def limit!(value) # :nodoc:
- if string_containing_comma?(value)
- # Remove `string_containing_comma?` when removing this deprecation
- ActiveSupport::Deprecation.warn(<<-WARNING.squish)
- Passing a string to limit in the form "1,2" is deprecated and will be
- removed in Rails 5.1. Please call `offset` explicitly instead.
- WARNING
- end
self.limit_value = value
self
end
@@ -774,7 +774,7 @@ module ActiveRecord
# end
#
def none
- where("1=0").extending!(NullRelation)
+ spawn.none!
end
def none! # :nodoc:
@@ -818,7 +818,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
@@ -859,16 +859,12 @@ module ActiveRecord
def distinct(value = true)
spawn.distinct!(value)
end
- alias uniq distinct
- deprecate uniq: :distinct
# Like #distinct, but modifies relation in place.
def distinct!(value = true) # :nodoc:
self.distinct_value = value
self
end
- alias uniq! distinct!
- deprecate uniq!: :distinct!
# Used to extend a scope with additional methods, either through
# a module or through a block provided.
@@ -924,6 +920,29 @@ module ActiveRecord
self
end
+ # Specify optimizer hints to be used in the SELECT statement.
+ #
+ # Example (for MySQL):
+ #
+ # Topic.optimizer_hints("MAX_EXECUTION_TIME(50000)", "NO_INDEX_MERGE(topics)")
+ # # SELECT /*+ MAX_EXECUTION_TIME(50000) NO_INDEX_MERGE(topics) */ `topics`.* FROM `topics`
+ #
+ # Example (for PostgreSQL with pg_hint_plan):
+ #
+ # Topic.optimizer_hints("SeqScan(topics)", "Parallel(topics 8)")
+ # # SELECT /*+ SeqScan(topics) Parallel(topics 8) */ "topics".* FROM "topics"
+ def optimizer_hints(*args)
+ check_if_method_has_arguments!(:optimizer_hints, args)
+ spawn.optimizer_hints!(*args)
+ end
+
+ def optimizer_hints!(*args) # :nodoc:
+ args.flatten!
+
+ self.optimizer_hints_values += args
+ self
+ end
+
# Reverse the existing order clause on the relation.
#
# User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC'
@@ -938,299 +957,362 @@ 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)
-
- build_joins(arel, joins_values.flatten) unless joins_values.empty?
- build_left_outer_joins(arel, left_outer_joins_values.flatten) unless left_outer_joins_values.empty?
-
- arel.where(where_clause.ast) unless where_clause.empty?
- arel.having(having_clause.ast) unless having_clause.empty?
- if limit_value
- if string_containing_comma?(limit_value)
- arel.take(connection.sanitize_limit(limit_value))
- else
- arel.take(Arel::Nodes::BindParam.new)
- end
- end
- arel.skip(Arel::Nodes::BindParam.new) if offset_value
- arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty?
-
- build_order(arel)
-
- build_select(arel)
+ # Adds an SQL comment to queries generated from this relation. For example:
+ #
+ # User.annotate("selecting user names").select(:name)
+ # # SELECT "users"."name" FROM "users" /* selecting user names */
+ #
+ # User.annotate("selecting", "user", "names").select(:name)
+ # # SELECT "users"."name" FROM "users" /* selecting */ /* user */ /* names */
+ #
+ # The SQL block comment delimiters, "/*" and "*/", will be added automatically.
+ def annotate(*args)
+ check_if_method_has_arguments!(:annotate, args)
+ spawn.annotate!(*args)
+ end
- arel.distinct(distinct_value)
- arel.from(build_from) unless from_clause.empty?
- arel.lock(lock_value) if lock_value
+ # Like #annotate, but modifies relation in place.
+ def annotate!(*args) # :nodoc:
+ self.annotate_values += args
+ self
+ end
- arel
+ # Returns the Arel object associated with the relation.
+ def arel(aliases = nil) # :nodoc:
+ @arel ||= build_arel(aliases)
end
- 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(", :")}."
+ private
+ # Returns a relation value with a given name
+ def get_value(name)
+ @values.fetch(name, DEFAULT_VALUES[name])
end
- 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}="
+ # Sets the relation value with the given name
+ def set_value(name, value)
+ assert_mutability!
+ @values[name] = value
end
- case scope
- when :order
- result = []
- else
- result = [] if multi_val_method
+ def assert_mutability!
+ raise ImmutableRelation if @loaded
+ raise ImmutableRelation if defined?(@arel) && @arel
end
- self.send(unscope_code, result)
- end
+ 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?
- 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
+ build_order(arel)
- 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_select(arel)
+
+ arel.optimizer_hints(*optimizer_hints_values) unless optimizer_hints_values.empty?
+ arel.distinct(distinct_value)
+ arel.from(build_from) unless from_clause.empty?
+ arel.lock(lock_value) if lock_value
+ arel.comment(*annotate_values) unless annotate_values.empty?
+
+ arel
end
- end
- def build_left_outer_joins(manager, outer_joins)
- buckets = outer_joins.group_by do |join|
- case join
- when Hash, Symbol, Array
- :association_join
+ 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
- raise ArgumentError, 'only Hash, Symbol and Array are allowed'
+ opts
end
end
- build_join_query(manager, buckets, Arel::Nodes::OuterJoin)
- 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_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
+ build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases)
+ end
+
+ 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
+
+ build_join_query(manager, buckets, Arel::Nodes::InnerJoin, aliases)
end
- build_join_query(manager, buckets, Arel::Nodes::InnerJoin)
- end
+ def build_join_query(manager, buckets, join_type, aliases)
+ buckets.default = []
- def build_join_query(manager, buckets, join_type)
- buckets.default = []
+ association_joins = buckets[:association_join]
+ stashed_joins = buckets[:stashed_join]
+ join_nodes = buckets[:join_node].uniq
+ string_joins = buckets[:string_join].map(&:strip).uniq
- 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
+ join_list = join_nodes + convert_join_strings_to_ast(string_joins)
+ alias_tracker = alias_tracker(join_list, aliases)
- join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins)
+ join_dependency = construct_join_dependency(association_joins)
- join_dependency = ActiveRecord::Associations::JoinDependency.new(
- @klass,
- association_joins,
- join_list
- )
+ joins = join_dependency.join_constraints(stashed_joins, join_type, alias_tracker)
+ joins.each { |join| manager.from(join) }
- join_infos = join_dependency.join_constraints stashed_association_joins, join_type
+ manager.join_sources.concat(join_list)
- join_infos.each do |info|
- info.joins.each { |join| manager.from(join) }
- manager.bind_values.concat info.binds
+ alias_tracker.aliases
end
- manager.join_sources.concat(join_list)
-
- manager
- end
+ def convert_join_strings_to_ast(joins)
+ joins
+ .flatten
+ .reject(&:blank?)
+ .map { |join| table.create_string_join(Arel.sql(join)) }
+ end
- def convert_join_strings_to_ast(table, joins)
- joins
- .flatten
- .reject(&:blank?)
- .map { |join| table.create_string_join(Arel.sql(join)) }
- end
+ def build_select(arel)
+ if select_values.any?
+ arel.project(*arel_columns(select_values.uniq))
+ elsif klass.ignored_columns.any?
+ arel.project(*klass.column_names.map { |field| arel_attribute(field) })
+ else
+ arel.project(table[Arel.star])
+ end
+ 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 arel_columns(columns)
+ columns.flat_map do |field|
+ case field
+ when Symbol
+ field = field.to_s
+ arel_column(field) { connection.quote_table_name(field) }
+ when String
+ arel_column(field) { field }
+ when Proc
+ field.call
+ else
+ field
+ end
+ end
end
- end
- def arel_columns(columns)
- columns.map do |field|
- if (Symbol === field || String === field) && (klass.has_attribute?(field) || klass.attribute_alias?(field)) && !from_clause.value
+ def arel_column(field)
+ field = klass.attribute_alias(field) if klass.attribute_alias?(field)
+ from = from_clause.name || from_clause.value
+
+ if klass.columns_hash.key?(field) && (!from || table_name_matches?(from))
arel_attribute(field)
- elsif Symbol === field
- connection.quote_table_name(field.to_s)
else
- field
+ yield
end
end
- end
- 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"
+ def table_name_matches?(from)
+ /(?:\A|(?<!FROM)\s)(?:\b#{table.name}\b|#{connection.quote_table_name(table.name)})(?!\.)/i.match?(from.to_s)
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.concat(' DESC')
+ 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
+
+ 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} cannot 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
- else
- o
end
end
- end
- def does_not_support_reverse?(order)
- #uses sql function with multiple arguments
- order =~ /\([^()]*,[^()]*\)/ ||
- # uses "nulls first" like construction
- order =~ /nulls (first|last)\Z/i
- end
+ def does_not_support_reverse?(order)
+ # Account for String subclasses like Arel::Nodes::SqlLiteral that
+ # override methods like #count.
+ order = String.new(order) unless order.instance_of?(String)
- def build_order(arel)
- orders = order_values.uniq
- orders.reject!(&:blank?)
-
- arel.order(*orders) unless orders.empty?
- end
+ # Uses SQL function with multiple arguments.
+ (order.include?(",") && order.split(",").find { |section| section.count("(") != section.count(")") }) ||
+ # Uses "nulls first" like construction.
+ /\bnulls\s+(?:first|last)\b/i.match?(order)
+ end
- VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC,
- 'asc', 'desc', 'ASC', 'DESC'] # :nodoc:
+ def build_order(arel)
+ orders = order_values.uniq
+ orders.reject!(&:blank?)
- 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
+ arel.order(*orders) unless orders.empty?
end
- end
- def preprocess_order_args(order_args)
- order_args.map! do |arg|
- klass.send(:sanitize_sql_for_order, arg)
- end
- order_args.flatten!
- validate_order_args(order_args)
+ VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC,
+ "asc", "desc", "ASC", "DESC"].to_set # :nodoc:
- references = order_args.grep(String)
- references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
- references!(references) if references.any?
+ 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
- # 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|
- arel_attribute(field).send(dir.downcase)
- }
- else
- arg
+ def preprocess_order_args(order_args)
+ order_args.map! do |arg|
+ klass.sanitize_sql_for_order(arg)
end
- end.flatten!
- end
+ order_args.flatten!
- # 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."
+ @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
+ arg = arg.to_s
+ arel_column(arg) {
+ Arel.sql(connection.quote_table_name(arg))
+ }.asc
+ when Hash
+ arg.map { |field, dir|
+ case field
+ when Arel::Nodes::SqlLiteral
+ field.send(dir.downcase)
+ else
+ field = field.to_s
+ arel_column(field) {
+ Arel.sql(connection.quote_table_name(field))
+ }.send(dir.downcase)
+ end
+ }
+ else
+ arg
+ end
+ end.flatten!
end
- end
- def structurally_incompatible_values_for_or(other)
- Relation::SINGLE_VALUE_METHODS.reject { |m| send("#{m}_value") == other.send("#{m}_value") } +
- (Relation::MULTI_VALUE_METHODS - [:extending]).reject { |m| send("#{m}_values") == other.send("#{m}_values") } +
- (Relation::CLAUSE_METHODS - [:having, :where]).reject { |m| send("#{m}_clause") == other.send("#{m}_clause") }
- 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."
+ end
+ end
- def new_where_clause
- Relation::WhereClause.empty
- end
- alias new_having_clause new_where_clause
+ 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 where_clause_factory
- @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder)
- end
- alias having_clause_factory where_clause_factory
+ def where_clause_factory
+ @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder)
+ end
+ alias having_clause_factory where_clause_factory
- def new_from_clause
- Relation::FromClause.empty
- end
+ DEFAULT_VALUES = {
+ create_with: FROZEN_EMPTY_HASH,
+ where: Relation::WhereClause.empty,
+ having: Relation::WhereClause.empty,
+ from: Relation::FromClause.empty
+ }
- def string_containing_comma?(value)
- ::String === value && value.include?(",")
- end
+ 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 dbd08811fa..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
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index d5c18a2a4a..efc4b447aa 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -1,13 +1,14 @@
-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
+ already_in_scope? ? klass.all : clone
end
# Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation.
@@ -67,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 2c2d6cfa47..47728aac30 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,120 @@ 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
- @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|
+ Arel.fetch_attribute(node) { |attr| columns.include?(attr.name.to_s) }
+ 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 c0ccb00b6f..c1b3eea9df 100644
--- a/activerecord/lib/active_record/relation/where_clause_factory.rb
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
class Relation
class WhereClauseFactory # :nodoc:
@@ -7,31 +9,25 @@ module ActiveRecord
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)
when Arel::Nodes::Node
parts = [opts]
- binds = other
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 8e6cd6c82f..da6d10b6ec 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
###
# This class encapsulates a result returned from calling
@@ -19,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"},
# ...
@@ -32,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 = {})
@@ -43,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 }
@@ -56,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)
@@ -97,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 56e88bc661..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:
@@ -12,9 +14,9 @@ module ActiveRecord
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 a9e1fd0dad..750766714d 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -1,17 +1,10 @@
+# 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 {connection.quote}[rdoc-ref:ConnectionAdapters::Quoting#quote].
- 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.
#
@@ -34,8 +27,7 @@ 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.
@@ -46,12 +38,12 @@ module ActiveRecord
# sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
# # => "name=NULL and group_id=4"
#
- # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 })
+ # 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 = self.table_name)
+ 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)
@@ -68,47 +60,23 @@ module ActiveRecord
# sanitize_sql_for_order("id ASC")
# # => "id ASC"
def sanitize_sql_for_order(condition)
- if condition.is_a?(Array) && condition.first.to_s.include?('?')
- sanitize_sql_array(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
end
- # 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)
- 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
- end
- end
- expanded_attrs
- end
-
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
#
# sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
@@ -116,9 +84,10 @@ module ActiveRecord
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
@@ -153,9 +122,9 @@ module ActiveRecord
# # => "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
@@ -164,57 +133,54 @@ 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
+ 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_bind_variable(value, c = connection) # :nodoc:
- if ActiveRecord::Relation === value
- value.to_sql
- else
- quote_bound_value(value, c)
+ def replace_bind_variable(value, c = connection)
+ if ActiveRecord::Relation === value
+ value.to_sql
+ else
+ quote_bound_value(value, 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])
- else
- raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
+ 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
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)
+ def quote_bound_value(value, c = connection)
+ if value.respond_to?(:map) && !value.acts_like?(:string)
+ quoted = value.map { |v| c.quote(v) }
+ if quoted.empty?
+ c.quote(nil)
+ else
+ quoted.join(",")
+ end
else
- value.map { |v| c.quote(v) }.join(',')
+ c.quote(value)
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 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
-
- # TODO: Deprecate this
- def quoted_id # :nodoc:
- self.class.quote_value(@attributes[self.class.primary_key].value_for_database)
end
end
end
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index 784a02d2c3..76bf53387d 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
# = Active Record \Schema
#
@@ -37,10 +39,10 @@ module ActiveRecord
# 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
@@ -48,21 +50,12 @@ module ActiveRecord
instance_eval(&block)
if info[:version].present?
- initialize_schema_migrations_table
- connection.assume_migrated_upto_version(info[:version], migrations_paths)
+ ActiveRecord::SchemaMigration.create_table
+ connection.assume_migrated_upto_version(info[:version])
end
ActiveRecord::InternalMetadata.create_table
- ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
+ 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 # :nodoc:
- 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 affcd9aed1..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,20 +88,12 @@ 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)
- sorted_tables = @connection.data_sources.sort - @connection.views
+ sorted_tables = @connection.tables.sort
sorted_tables.each do |table_name|
table(table_name, stream) unless ignored?(table_name)
@@ -109,77 +113,46 @@ HEADER
tbl = StringIO.new
# first dump primary key column
- if @connection.respond_to?(:primary_keys)
- pk = @connection.primary_keys(table)
- pk = pk.first unless pk.size > 1
- else
- pk = @connection.primary_key(table)
- end
+ pk = @connection.primary_key(table)
tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
case pk
when String
- tbl.print ", primary_key: #{pk.inspect}" unless pk == 'id'
+ tbl.print ", primary_key: #{pk.inspect}" unless pk == "id"
pkcol = columns.detect { |c| c.name == pk }
- pkcolspec = @connection.column_spec_for_primary_key(pkcol)
+ pkcolspec = column_spec_for_primary_key(pkcol)
if pkcolspec.present?
- pkcolspec.each do |key, value|
- tbl.print ", #{key}: #{value}"
- end
+ 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_in_create(table, tbl)
+
tbl.puts " end"
tbl.puts
- indexes(table, tbl)
-
tbl.rewind
stream.print tbl.read
rescue => e
@@ -187,30 +160,14 @@ 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 = [
- "add_index #{remove_prefix_and_suffix(index.table).inspect}",
- 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")
@@ -218,6 +175,31 @@ HEADER
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|
@@ -234,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
@@ -248,8 +230,26 @@ 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)
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index b6cb233e03..1fca1a18f6 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -1,5 +1,7 @@
-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
# This class is used to create a table that keeps track of which migrations
@@ -8,6 +10,10 @@ module ActiveRecord
# to be executed the next time.
class SchemaMigration < ActiveRecord::Base # :nodoc:
class << self
+ def _internal?
+ true
+ end
+
def primary_key
"version"
end
@@ -17,7 +23,7 @@ module ActiveRecord
end
def table_exists?
- ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) }
+ connection.table_exists?(table_name)
end
def create_table
@@ -39,7 +45,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 7794af8ca4..35e9dcbffc 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,33 +11,32 @@ module ActiveRecord
include Named
end
- module ClassMethods
- def current_scope #:nodoc:
- ScopeRegistry.value_for(:current_scope, self)
- end
-
- def current_scope=(scope) #:nodoc:
- ScopeRegistry.set_value_for(:current_scope, self, 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
+
+ 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 # :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 # :nodoc:
@@ -75,8 +76,9 @@ module ActiveRecord
end
# Obtains the value for a given +scope_type+ and +model+.
- def value_for(scope_type, model)
+ def value_for(scope_type, model, skip_inherited_scope = false)
raise_invalid_scope_type!(scope_type)
+ return @registry[scope_type][model.name] if skip_inherited_scope
klass = model
base = model.base_class
while klass <= base
@@ -94,11 +96,11 @@ module ActiveRecord
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 9eab59ac78..87bcfd5181 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,11 +7,8 @@ module ActiveRecord
included do
# Stores the default scope for the class.
- class_attribute :default_scopes, instance_writer: false, instance_predicate: false
- class_attribute :default_scope_override, instance_writer: false, instance_predicate: false
-
- self.default_scopes = []
- self.default_scope_override = nil
+ 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
@@ -44,105 +43,108 @@ 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, &block) # :doc:
+ scope = block 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
- # 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.)"
+ self.default_scopes += [scope]
end
- self.default_scopes += [scope]
- end
-
- def build_default_scope(base_rel = nil) # :nodoc:
- return if abstract_class?
+ def build_default_scope(relation = relation())
+ return if abstract_class?
- if self.default_scope_override.nil?
- self.default_scope_override = !Base.is_a?(method(:default_scope).owner)
- end
+ if default_scope_override.nil?
+ self.default_scope_override = !Base.is_a?(method(:default_scope).owner)
+ end
- if self.default_scope_override
- # The user has defined their own default scope method, so call that
- evaluate_default_scope { default_scope }
- 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))
+ if default_scope_override
+ # The user has defined their own default scope method, so call that
+ evaluate_default_scope do
+ if scope = default_scope
+ relation.merge!(scope)
+ end
+ end
+ elsif default_scopes.any?
+ evaluate_default_scope do
+ default_scopes.inject(relation) do |default_scope, scope|
+ scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call)
+ default_scope.instance_exec(&scope) || default_scope
+ end
end
end
end
- end
- def ignore_default_scope? # :nodoc:
- ScopeRegistry.value_for(:ignore_default_scope, base_class)
- 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, base_class, 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 # :nodoc:
- return if ignore_default_scope?
-
- begin
- self.ignore_default_scope = true
- yield
- ensure
- self.ignore_default_scope = false
+ # 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?
+
+ 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 5395bd6076..681a5c6250 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
@@ -22,27 +24,51 @@ module ActiveRecord
# You can define a scope that applies to all finders using
# {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope].
def all
- if current_scope
- current_scope.clone
+ scope = current_scope
+
+ if scope
+ if scope._deprecated_scope_source
+ ActiveSupport::Deprecation.warn(<<~MSG.squish)
+ Class level methods will no longer inherit scoping from `#{scope._deprecated_scope_source}`
+ in Rails 6.1. To continue using the scoped relation, pass it into the block directly.
+ To instead access the full set of models, as Rails 6.1 will, use `#{name}.unscoped`.
+ MSG
+ end
+
+ 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.
# The method is intended to return an ActiveRecord::Relation
# object, which is composable with other scopes.
- # If it returns nil or false, an
+ # 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
@@ -142,7 +168,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)
@@ -151,34 +177,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) }
+ singleton_class.define_method(name) do |*args|
+ scope = all._exec_scope(name, *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) }
+ singleton_class.define_method(name) do |*args|
+ scope = body.call(*args) || all
scope = scope.extending(extension) if extension
-
- scope || all
+ scope
end
end
+
+ generate_relation_method(name)
end
- protected
+ private
- def valid_scope_name?(name)
- if respond_to?(name, true)
- logger.warn "Creating scope :#{name}. " \
- "Overwriting existing method #{self.name}.#{name}."
+ def valid_scope_name?(name)
+ if respond_to?(name, true) && logger
+ logger.warn "Creating scope :#{name}. " \
+ "Overwriting existing method #{self.name}.#{name}."
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb
index 8abda2ac49..bcdb33901b 100644
--- a/activerecord/lib/active_record/secure_token.rb
+++ b/activerecord/lib/active_record/secure_token.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module SecureToken
extend ActiveSupport::Concern
@@ -25,9 +27,9 @@ module ActiveRecord
# 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 5a408e7b8e..741fea43ce 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord #:nodoc:
# = Active Record \Serialization
module Serialization
@@ -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)
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index 6c896ccea6..93bce15230 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:
#
@@ -8,12 +9,12 @@ module ActiveRecord
# end
#
# The cached statement is executed by using the
- # [connection.execute]{rdoc-ref:ConnectionAdapters::DatabaseStatements#execute} method:
+ # {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]{rdoc-ref:ConnectionAdapters::DatabaseStatements#execute}
+ # {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
@@ -25,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:
@@ -40,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:
@@ -70,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|
@@ -82,32 +108,41 @@ 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
+ def self.create(connection, callable = nil, &block)
+ relation = (callable || 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, preparable: true)
+ klass.find_by_sql(sql, bind_values, preparable: true, &block)
+ rescue ::RangeError
+ nil
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 1b407f7702..6fecb06897 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.
@@ -9,14 +11,20 @@ module ActiveRecord
# of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's
# already built around just accessing attributes on the model.
#
+ # Every accessor comes with dirty tracking methods (+key_changed?+, +key_was+ and +key_change+) and
+ # methods to access the changes made during the last save (+saved_change_to_key?+, +saved_change_to_key+ and
+ # +key_before_last_save+).
+ #
+ # NOTE: There is no +key_will_change!+ method for accessors, use +store_will_change!+ instead.
+ #
# Make sure that you declare the database column used for the serialized store as a text, so there's
# plenty of room.
#
# You can set custom coder to encode/decode your serialized attributes to/from different formats.
# JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
#
- # NOTE: If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for
- # the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store].
+ # NOTE: If you are using structured database data types (eg. PostgreSQL +hstore+/+json+, or MySQL 5.7+
+ # +json+) there is no need for the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store].
# Simply use {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead to generate
# the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
# using a symbol.
@@ -29,24 +37,40 @@ 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
# u.settings[:country] # => 'Denmark'
# u.settings['country'] # => 'Denmark'
#
+ # # Dirty tracking
+ # u.color = 'green'
+ # u.color_changed? # => true
+ # u.color_was # => 'black'
+ # u.color_change # => ['black', 'red']
+ #
# # 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}[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
#
@@ -78,22 +102,79 @@ 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
+
+ define_method("#{accessor_key}_changed?") do
+ return false unless attribute_changed?(store_attribute)
+ prev_store, new_store = changes[store_attribute]
+ prev_store&.dig(key) != new_store&.dig(key)
+ end
+
+ define_method("#{accessor_key}_change") do
+ return unless attribute_changed?(store_attribute)
+ prev_store, new_store = changes[store_attribute]
+ [prev_store&.dig(key), new_store&.dig(key)]
+ end
+
+ define_method("#{accessor_key}_was") do
+ return unless attribute_changed?(store_attribute)
+ prev_store, _new_store = changes[store_attribute]
+ prev_store&.dig(key)
+ end
+
+ define_method("saved_change_to_#{accessor_key}?") do
+ return false unless saved_change_to_attribute?(store_attribute)
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
+ prev_store&.dig(key) != new_store&.dig(key)
+ end
+
+ define_method("saved_change_to_#{accessor_key}") do
+ return unless saved_change_to_attribute?(store_attribute)
+ prev_store, new_store = saved_change_to_attribute(store_attribute)
+ [prev_store&.dig(key), new_store&.dig(key)]
+ end
+
+ define_method("#{accessor_key}_before_last_save") do
+ return unless saved_change_to_attribute?(store_attribute)
+ prev_store, _new_store = saved_change_to_attribute(store_attribute)
+ prev_store&.dig(key)
+ end
end
end
@@ -114,27 +195,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:
@@ -177,34 +257,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 8ec4b48d31..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,10 +32,11 @@ 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
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
index 0faad48ce3..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,9 +11,7 @@ module ActiveRecord
end
def resolve_column_aliases(hash)
- # This method is a hot spot, so for now, use Hash[] to dup the hash.
- # https://bugs.ruby-lang.org/issues/7166
- new_hash = Hash[hash]
+ 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)
@@ -31,21 +30,26 @@ module ActiveRecord
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
@@ -61,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 8881986f1b..155d2b0b98 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/string/filters'
+# frozen_string_literal: true
+
+require "active_record/database_configurations"
module ActiveRecord
module Tasks # :nodoc:
@@ -8,7 +10,7 @@ module ActiveRecord
# 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
@@ -35,19 +37,29 @@ 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::Migrator.current_environment
- stored = ActiveRecord::Migrator.last_stored_environment
+ 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::Migrator.protected_environment?
+ if ActiveRecord::Base.connection.migration_context.protected_environment?
raise ActiveRecord::ProtectedEnvironmentError.new(stored)
end
@@ -63,24 +75,24 @@ module ActiveRecord
@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
@@ -91,35 +103,53 @@ 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
- $stderr.puts "Couldn't create database for #{configuration.inspect}"
+ $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)
+ 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(ActiveRecord::Base, old_pool.spec)
+ ActiveRecord::Base.connection_handler.establish_connection(old_pool.spec.to_hash)
+ end
+ end
+
+ def for_each
+ databases = Rails.application.config.load_database_yaml
+ 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
@@ -132,12 +162,13 @@ 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
- $stderr.puts "Couldn't drop #{configuration['database']}"
+ $stderr.puts "Couldn't drop database '#{configuration['database']}'"
raise
end
@@ -151,39 +182,85 @@ module ActiveRecord
}
end
+ def truncate_tables(configuration)
+ ActiveRecord::Base.connected_to(database: { truncation: configuration }) do
+ table_names = ActiveRecord::Base.connection.tables
+ table_names -= [
+ ActiveRecord::Base.schema_migrations_table_name,
+ ActiveRecord::Base.internal_metadata_table_name
+ ]
+
+ ActiveRecord::Base.connection.truncate_tables(*table_names)
+ end
+ end
+ private :truncate_tables
+
+ def truncate_all(environment = env)
+ ActiveRecord::Base.configurations.configs_for(env_name: environment).each do |db_config|
+ truncate_tables db_config.config
+ end
+ 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(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
@@ -202,61 +279,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] = ActiveRecord::Migrator.current_environment
+ 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 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 `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)
+ 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
@@ -265,47 +361,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]
- environments << 'test' if environment == 'development'
+ 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 af0c935342..1c1b29b5e1 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -1,8 +1,8 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Tasks # :nodoc:
class MySQLDatabaseTasks # :nodoc:
- ACCESS_DENIED_ERROR = 1045
-
delegate :connection, :establish_connection, to: ActiveRecord::Base
def initialize(configuration)
@@ -11,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.message
- establish_connection root_configuration_without_database
- connection.create_database configuration['database'], creation_options
- if configuration['username'] != 'root'
- connection.execute grant_statement.gsub(/\s+/, ' ').strip
- end
- establish_connection configuration
- else
- $stderr.puts error.inspect
- $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}"
- $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding']
- end
end
def drop
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
@@ -53,99 +39,75 @@ module ActiveRecord
connection.collation
end
- def structure_dump(filename)
+ def structure_dump(filename, extra_flags)
args = prepare_command_options
args.concat(["--result-file", "#{filename}"])
args.concat(["--no-data"])
args.concat(["--routines"])
+ args.concat(["--skip-comments"])
+
+ ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
+ if ignore_tables.any?
+ args += ignore_tables.map { |table| "--ignore-table=#{configuration['database']}.#{table}" }
+ end
+
args.concat(["#{configuration['database']}"])
+ args.unshift(*extra_flags) if extra_flags
- run_cmd('mysqldump', args, 'dumping')
+ run_cmd("mysqldump", args, "dumping")
end
- def structure_load(filename)
+ def structure_load(filename, extra_flags)
args = prepare_command_options
- args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}])
+ args.concat(["--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')
+ run_cmd("mysql", args, "loading")
end
private
- def configuration
- @configuration
- end
-
- def configuration_without_database
- configuration.merge('database' => nil)
- end
+ attr_reader :configuration
- def creation_options
- Hash.new.tap do |options|
- options[:charset] = configuration['encoding'] if configuration.include? 'encoding'
- options[:collation] = configuration['collation'] if configuration.include? 'collation'
+ 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
- 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 root_password
- $stdout.print "Please provide the root password for your MySQL installation\n>"
- $stdin.gets.strip
- end
- def prepare_command_options
- args = {
- 'host' => '--host',
- '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 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 run_cmd(cmd, args, action)
- fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args)
- 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: `#{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
+ 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 8b4874044c..8acb11f75f 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -1,7 +1,13 @@
+# 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
@@ -12,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
@@ -25,7 +31,7 @@ module ActiveRecord
def drop
establish_master_connection
- connection.drop_database configuration['database']
+ connection.drop_database configuration["database"]
end
def charset
@@ -42,69 +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 = ["-s", "-x", "-O", "-f", filename]
+ args.concat(Array(extra_flags)) if extra_flags
unless search_path.blank?
- args += search_path.split(',').map do |part|
+ args += search_path.split(",").map do |part|
"--schema=#{part.strip}"
end
end
- args << configuration['database']
- run_cmd('pg_dump', args, 'dumping')
+
+ 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
- args = [ '-q', '-f', filename, configuration['database'] ]
- run_cmd('psql', args, 'loading' )
+ 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(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 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 9ec3c8a94a..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,20 +10,20 @@ 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)
rescue Errno::ENOENT => error
- raise NoDatabaseError.new(error.message, error)
+ raise NoDatabaseError.new(error.message)
end
def purge
@@ -35,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..8c60d71669
--- /dev/null
+++ b/activerecord/lib/active_record/test_fixtures.rb
@@ -0,0 +1,224 @@
+# 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, _lazy: 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, _lazy: 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
+ setup_shared_connection_pool
+
+ ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
+ end
+
+ private
+
+ # Shares the writing connection pool with connections on
+ # other handlers.
+ #
+ # In an application with a primary and replica the test fixtures
+ # need to share a connection pool so that the reading connection
+ # can see data in the open transaction on the writing connection.
+ def setup_shared_connection_pool
+ writing_handler = ActiveRecord::Base.connection_handler
+
+ ActiveRecord::Base.connection_handlers.values.each do |handler|
+ if handler != writing_handler
+ handler.connection_pool_list.each do |pool|
+ name = pool.spec.name
+ writing_connection = writing_handler.retrieve_connection_pool(name)
+ handler.send(:owner_to_pool)[name] = writing_connection
+ end
+ end
+ end
+ end
+
+ 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 d9c18a5e38..04a1c03474 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
# = Active Record \Timestamp
#
@@ -42,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:
@@ -51,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
@@ -68,59 +101,51 @@ module ActiveRecord
super
end
- def _update_record(*args, touch: true, **options)
- if touch && should_record_timestamps?
+ def _update_record
+ if @_touch_record && should_record_timestamps?
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)
+
+ super
+ end
+
+ def create_or_update(touch: true, **)
+ @_touch_record = touch
+ super
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)
- timestamp_names
- .map { |attr| self[attr] }
+ def max_updated_column_timestamp
+ timestamp_attributes_for_update_in_model
+ .map { |attr| self[attr]&.to_time }
.compact
- .map(&:to_time)
.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 9a80a63e28..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
@@ -20,7 +27,7 @@ module ActiveRecord
# 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, r.foreign_key, r.name, touch, :touch_later)
+ ActiveRecord::Associations::Builder::BelongsTo.touch_record(self, changes_to_save, r.foreign_key, r.name, touch, :touch_later)
end
end
end
@@ -53,6 +60,5 @@ module ActiveRecord
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 77c2845d88..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
@@ -11,7 +13,6 @@ module ActiveRecord
:before_commit_without_transaction_enrollment,
:commit_without_transaction_enrollment,
:rollback_without_transaction_enrollment,
- terminator: deprecated_false_terminator,
scope: [:kind, :name]
end
@@ -124,7 +125,7 @@ module ActiveRecord
# # statement will cause a PostgreSQL error, even though the unique
# # constraint is no longer violated:
# Number.create(i: 1)
- # # => "PGError: ERROR: current transaction is aborted, commands
+ # # => "PG::Error: ERROR: current transaction is aborted, commands
# # ignored until end of transaction block"
# end
#
@@ -169,7 +170,7 @@ 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.7/en/savepoint.html
+ # https://dev.mysql.com/doc/refman/5.7/en/savepoint.html
# for more information about savepoints.
#
# === \Callbacks
@@ -189,8 +190,8 @@ module ActiveRecord
#
# === 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
@@ -274,35 +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, enforced_options = {})
- options = args.extract_options!.merge!(enforced_options)
- args << options
+ 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] << "transaction_include_any_action?(#{fire_on})"
+ 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.
@@ -315,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:
@@ -328,17 +317,6 @@ 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_before_commit_without_transaction_enrollment_callbacks
_run_before_commit_callbacks
@@ -349,11 +327,13 @@ module ActiveRecord
# 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?
+ 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
@@ -391,119 +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
- ensure
- if @transaction_state && @transaction_state.committed?
- clear_transaction_record_state
- end
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 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 +@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 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
+ # 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 4911d93dd9..03d00006b7 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -1,17 +1,22 @@
-require 'active_model/type'
+# frozen_string_literal: true
-require 'active_record/type/internal/abstract_json'
-require 'active_record/type/internal/timezone'
+require "active_model/type"
-require 'active_record/type/date'
-require 'active_record/type/date_time'
-require 'active_record/type/time'
+require "active_record/type/internal/timezone"
-require 'active_record/type/serialized'
-require 'active_record/type/adapter_specific_registry'
+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/type_map'
-require 'active_record/type/hash_lookup_type_map'
+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
@@ -37,24 +42,24 @@ module ActiveRecord
registry.lookup(*args, adapter: adapter, **kwargs)
end
+ def default_value # :nodoc:
+ @default_value ||= Value.new
+ end
+
private
- def current_adapter_name
- ActiveRecord::Base.connection.adapter_name.downcase.to_sym
- end
+ def current_adapter_name
+ ActiveRecord::Base.connection.adapter_name.downcase.to_sym
+ end
end
- Helpers = ActiveModel::Type::Helpers
BigInteger = ActiveModel::Type::BigInteger
Binary = ActiveModel::Type::Binary
Boolean = ActiveModel::Type::Boolean
Decimal = ActiveModel::Type::Decimal
- DecimalWithoutScale = ActiveModel::Type::DecimalWithoutScale
Float = ActiveModel::Type::Float
Integer = ActiveModel::Type::Integer
String = ActiveModel::Type::String
- Text = ActiveModel::Type::Text
- UnsignedInteger = ActiveModel::Type::UnsignedInteger
Value = ActiveModel::Type::Value
register(:big_integer, Type::BigInteger, override: false)
@@ -65,6 +70,7 @@ module ActiveRecord
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 d440eac619..b300fdfa05 100644
--- a/activerecord/lib/active_record/type/adapter_specific_registry.rb
+++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb
@@ -1,4 +1,6 @@
-require 'active_model/type/registry'
+# frozen_string_literal: true
+
+require "active_model/type/registry"
module ActiveRecord
# :stopdoc:
@@ -10,15 +12,15 @@ module ActiveRecord
private
- def registration_klass
- Registration
- end
+ def registration_klass
+ Registration
+ end
- def find_registration(symbol, *args)
- registrations
- .select { |registration| registration.matches?(symbol, *args) }
- .max
- end
+ def find_registration(symbol, *args)
+ registrations
+ .select { |registration| registration.matches?(symbol, *args) }
+ .max
+ end
end
class Registration
@@ -52,42 +54,42 @@ module ActiveRecord
protected
- attr_reader :name, :block, :adapter, :override
-
- def priority
- result = 0
- if adapter
- result |= 1
- end
- if override
- result |= 2
+ attr_reader :name, :block, :adapter, :override
+
+ def priority
+ result = 0
+ if adapter
+ result |= 1
+ end
+ if override
+ result |= 2
+ end
+ result
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
@@ -110,17 +112,14 @@ 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
diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb
index ccafed054e..8177074a20 100644
--- a/activerecord/lib/active_record/type/date.rb
+++ b/activerecord/lib/active_record/type/date.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Type
class Date < ActiveModel::Type::Date
diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb
index 1fb9380ecd..4acde6b9f8 100644
--- a/activerecord/lib/active_record/type/date_time.rb
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Type
class DateTime < ActiveModel::Type::DateTime
diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb
new file mode 100644
index 0000000000..a207940dc7
--- /dev/null
+++ b/activerecord/lib/active_record/type/decimal_without_scale.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Type
+ class DecimalWithoutScale < ActiveModel::Type::BigInteger # :nodoc:
+ def type
+ :decimal
+ end
+
+ def type_cast_for_schema(value)
+ value.to_s.inspect
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/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/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb
deleted file mode 100644
index 097d1bd363..0000000000
--- a/activerecord/lib/active_record/type/internal/abstract_json.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module ActiveRecord
- module Type
- module Internal # :nodoc:
- class AbstractJson < ActiveModel::Type::Value # :nodoc:
- include ActiveModel::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
diff --git a/activerecord/lib/active_record/type/internal/timezone.rb b/activerecord/lib/active_record/type/internal/timezone.rb
index 947e06158a..3059755752 100644
--- a/activerecord/lib/active_record/type/internal/timezone.rb
+++ b/activerecord/lib/active_record/type/internal/timezone.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Type
module Internal
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 4ff0740cfb..0a2f6cb9fb 100644
--- a/activerecord/lib/active_record/type/serialized.rb
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Type
class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc:
+ undef to_yaml if method_defined?(:to_yaml)
+
include ActiveModel::Type::Helpers::Mutable
attr_reader :subtype, :coder
@@ -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
@@ -43,15 +47,25 @@ module ActiveRecord
def assert_valid_value(value)
if coder.respond_to?(:assert_valid_value)
- coder.assert_valid_value(value)
+ coder.assert_valid_value(value, action: "serialize")
end
end
+ 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 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/text.rb b/activerecord/lib/active_record/type/text.rb
new file mode 100644
index 0000000000..6d19696671
--- /dev/null
+++ b/activerecord/lib/active_record/type/text.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Type
+ class Text < ActiveModel::Type::String # :nodoc:
+ def type
+ :text
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb
index 70988d84ff..f4da1ecf2c 100644
--- a/activerecord/lib/active_record/type/time.rb
+++ b/activerecord/lib/active_record/type/time.rb
@@ -1,8 +1,21 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Type
class Time < ActiveModel::Type::Time
include Internal::Timezone
+
+ class Value < DelegateClass(::Time) # :nodoc:
+ end
+
+ def serialize(value)
+ case value = super
+ when ::Time
+ Value.new(value)
+ else
+ value
+ end
+ end
end
end
end
-
diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb
index 850a7a4e09..fc40b460f0 100644
--- a/activerecord/lib/active_record/type/type_map.rb
+++ b/activerecord/lib/active_record/type/type_map.rb
@@ -1,4 +1,6 @@
-require 'concurrent/map'
+# frozen_string_literal: true
+
+require "concurrent/map"
module ActiveRecord
module Type
@@ -11,7 +13,7 @@ module ActiveRecord
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 ||= ActiveModel::Type::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
new file mode 100644
index 0000000000..4619528f81
--- /dev/null
+++ b/activerecord/lib/active_record/type/unsigned_integer.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Type
+ class UnsignedInteger < ActiveModel::Type::Integer # :nodoc:
+ private
+
+ def max_value
+ super * 2
+ end
+
+ def min_value
+ 0
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb
index accc339d00..2e5f45fa3d 100644
--- a/activerecord/lib/active_record/type_caster.rb
+++ b/activerecord/lib/active_record/type_caster.rb
@@ -1,5 +1,7 @@
-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 # :nodoc:
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
index 7ed8dcc313..7cf8181d8e 100644
--- a/activerecord/lib/active_record/type_caster/connection.rb
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module TypeCaster
class Connection # :nodoc:
@@ -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.data_source_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 3a367b3999..663cdadb03 100644
--- a/activerecord/lib/active_record/type_caster/map.rb
+++ b/activerecord/lib/active_record/type_caster/map.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module TypeCaster
class Map # :nodoc:
@@ -7,13 +9,12 @@ module ActiveRecord
def type_cast_for_database(attr_name, value)
return value if value.is_a?(Arel::Nodes::BindParam)
- type = types.type_for_attribute(attr_name.to_s)
+ type = types.type_for_attribute(attr_name)
type.serialize(value)
end
- 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 ecaf04e39e..ca27a3f0ab 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
# = Active Record \RecordInvalid
#
@@ -40,13 +42,13 @@ module ActiveRecord
# The validation process on save can be skipped by passing <tt>validate: false</tt>.
# 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={})
+ def save(options = {})
perform_validations(options) ? super : false
end
# 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={})
+ def save!(options = {})
perform_validations(options) ? super : raise_validation_error
end
@@ -68,7 +70,7 @@ module ActiveRecord
alias_method :validate, :valid?
- protected
+ private
def default_validation_context
new_record? ? :create : :update
@@ -78,7 +80,7 @@ module ActiveRecord
raise(RecordInvalid.new(self))
end
- def perform_validations(options={}) # :nodoc:
+ def perform_validations(options = {})
options[:validate] == false || valid?(options[:context])
end
end
diff --git a/activerecord/lib/active_record/validations/absence.rb b/activerecord/lib/active_record/validations/absence.rb
index 641d041f3d..6afb9eabd2 100644
--- a/activerecord/lib/active_record/validations/absence.rb
+++ b/activerecord/lib/active_record/validations/absence.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Validations
class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc:
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index b14db85167..3538aeec22 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Validations
class AssociatedValidator < ActiveModel::EachValidator #:nodoc:
@@ -37,7 +39,7 @@ module ActiveRecord
#
# * <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 0e0cebce4a..f47b14ae3a 100644
--- a/activerecord/lib/active_record/validations/length.rb
+++ b/activerecord/lib/active_record/validations/length.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Validations
class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc:
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index ad82ea66c4..75e97e1997 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveRecord
module Validations
class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc:
@@ -44,7 +46,7 @@ module ActiveRecord
# 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 ActiveModel::Validation#validates! for more information.
+ # See ActiveModel::Validations#validates! for more information.
def validates_presence_of(*attr_names)
validates_with PresenceValidator, _merge_attributes(attr_names)
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 4a80cda0b8..2c3a2fb797 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,24 +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
- super({ case_sensitive: true }.merge!(options))
+ 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
@klass = options[:class]
end
def validate_each(record, attribute, value)
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 = build_relation(finder_class, attribute, value)
if record.persisted?
if finder_class.primary_key
- relation = relation.where.not(finder_class.primary_key => record.id_was || record.id)
+ relation = relation.where.not(finder_class.primary_key => record.id_in_database)
else
- raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
+ raise UnknownPrimaryKey.new(finder_class, "Cannot validate uniqueness for persisted record without primary key.")
end
end
- relation = scope_relation(record, table, relation)
+ relation = scope_relation(record, relation)
relation = relation.merge(options[:conditions]) if options[:conditions]
if relation.exists?
@@ -34,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
@@ -50,50 +55,30 @@ module ActiveRecord
class_hierarchy.detect { |klass| !klass.abstract_class? }
end
- def build_relation(klass, table, attribute, value) #:nodoc:
- if reflection = klass._reflect_on_association(attribute)
- attribute = reflection.foreign_key
- value = value.attributes[reflection.klass.primary_key] unless value.nil?
- end
-
- # the attribute may be an aliased attribute
- if klass.attribute_alias?(attribute)
- attribute = klass.attribute_alias(attribute)
- end
-
- attribute_name = attribute.to_s
+ def build_relation(klass, attribute, value)
+ relation = klass.unscoped
+ comparison = relation.bind_attribute(attribute, value) do |attr, bind|
+ return relation.none! if bind.unboundable?
- 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]
+ if !options.key?(:case_sensitive) || bind.nil?
+ klass.connection.default_uniqueness_comparison(attr, bind, klass)
+ elsif options[:case_sensitive]
+ klass.connection.case_sensitive_comparison(attr, bind)
+ else
+ # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
+ klass.connection.case_insensitive_comparison(attr, bind)
+ end
end
- comparison = if !options[:case_sensitive] && !value.nil?
- # 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
- if value.nil?
- klass.unscoped.where(comparison)
- else
- bind = Relation::QueryAttribute.new(attribute_name, value, Type::Value.new)
- klass.unscoped.where(comparison, bind)
- end
- rescue RangeError
- klass.none
+ relation.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
@@ -208,9 +193,7 @@ module ActiveRecord
# | # Boom! We now have a duplicate
# | # title!
#
- # This could even happen if you use transactions with the 'serializable'
- # isolation level. The best way to work around this problem is to add a unique
- # index to the database table using
+ # The best way to work around this problem is to add a unique index to the database table using
# {connection.add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index].
# In the rare case that a race condition occurs, the database will guarantee
# the field's uniqueness.
@@ -222,7 +205,7 @@ module ActiveRecord
# 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
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..361cd915cc
--- /dev/null
+++ b/activerecord/lib/arel.rb
@@ -0,0 +1,51 @@
+# 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/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
+
+ def self.fetch_attribute(value)
+ case value
+ 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
+ yield value.left.is_a?(Arel::Attributes::Attribute) ? value.left : value.right
+ end
+ 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/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..cb31e3060b
--- /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)
+ end
+ self
+ end
+
+ def create_values(values)
+ Nodes::ValuesList.new([values])
+ 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..f994754620
--- /dev/null
+++ b/activerecord/lib/arel/nodes.rb
@@ -0,0 +1,68 @@
+# 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_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/comment"
+
+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..bf516db35f
--- /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::NodeExpression
+ 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..344e46479f
--- /dev/null
+++ b/activerecord/lib/arel/nodes/bind_param.rb
@@ -0,0 +1,36 @@
+# 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 infinite?
+ value.respond_to?(:infinite?) && value.infinite?
+ end
+
+ def unboundable?
+ value.respond_to?(:unboundable?) && value.unboundable?
+ 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..1c4b727bf6
--- /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::NodeExpression
+ 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..6e911b717d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/casted.rb
@@ -0,0 +1,50 @@
+# 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
+
+ def infinite?
+ value.respond_to?(:infinite?) && value.infinite?
+ 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/comment.rb b/activerecord/lib/arel/nodes/comment.rb
new file mode 100644
index 0000000000..237ff27e7e
--- /dev/null
+++ b/activerecord/lib/arel/nodes/comment.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Comment < Arel::Nodes::Node
+ attr_reader :values
+
+ def initialize(values)
+ super()
+ @values = values
+ end
+
+ def initialize_copy(other)
+ super
+ @values = @values.clone
+ end
+
+ def hash
+ [@values].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.values == other.values
+ end
+ alias :== :eql?
+ 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..11b4f39ece
--- /dev/null
+++ b/activerecord/lib/arel/nodes/select_core.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class SelectCore < Arel::Nodes::Node
+ attr_accessor :projections, :wheres, :groups, :windows, :comment
+ attr_accessor :havings, :source, :set_quantifier, :optimizer_hints
+
+ def initialize
+ super()
+ @source = JoinSource.new nil
+
+ # https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier
+ @set_quantifier = nil
+ @optimizer_hints = nil
+ @projections = []
+ @wheres = []
+ @groups = []
+ @havings = []
+ @windows = []
+ @comment = nil
+ 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, @optimizer_hints,
+ @wheres, @groups, @havings, @windows, @comment
+ ].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.source == other.source &&
+ self.set_quantifier == other.set_quantifier &&
+ self.optimizer_hints == other.optimizer_hints &&
+ self.projections == other.projections &&
+ self.wheres == other.wheres &&
+ self.groups == other.groups &&
+ self.havings == other.havings &&
+ self.windows == other.windows &&
+ self.comment == other.comment
+ 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..6d1ac36b0e
--- /dev/null
+++ b/activerecord/lib/arel/nodes/unary.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Unary < Arel::Nodes::NodeExpression
+ attr_accessor :expr
+ alias :value :expr
+
+ def initialize(expr)
+ super()
+ @expr = expr
+ end
+
+ def hash
+ @expr.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.expr == other.expr
+ end
+ alias :== :eql?
+ end
+
+ %w{
+ Bin
+ Cube
+ DistinctOn
+ Group
+ GroupingElement
+ GroupingSet
+ Lateral
+ Limit
+ Lock
+ Not
+ Offset
+ On
+ OptimizerHints
+ 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_list.rb b/activerecord/lib/arel/nodes/values_list.rb
new file mode 100644
index 0000000000..1a9d9ebf01
--- /dev/null
+++ b/activerecord/lib/arel/nodes/values_list.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class ValuesList < Unary
+ alias :rows :expr
+ 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..7dafde4952
--- /dev/null
+++ b/activerecord/lib/arel/predications.rb
@@ -0,0 +1,257 @@
+# 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 unboundable?(other.begin) == 1 || unboundable?(other.end) == -1
+ self.in([])
+ elsif open_ended?(other.begin)
+ if other.end.nil? || open_ended?(other.end)
+ not_in([])
+ elsif other.exclude_end?
+ lt(other.end)
+ else
+ lteq(other.end)
+ end
+ elsif other.end.nil? || open_ended?(other.end)
+ 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 unboundable?(other.begin) == 1 || unboundable?(other.end) == -1
+ not_in([])
+ elsif open_ended?(other.begin)
+ if other.end.nil? || open_ended?(other.end)
+ self.in([])
+ elsif other.exclude_end?
+ gteq(other.end)
+ else
+ gt(other.end)
+ end
+ elsif other.end.nil? || open_ended?(other.end)
+ 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 infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
+
+ def unboundable?(value)
+ value.respond_to?(:unboundable?) && value.unboundable?
+ end
+
+ def open_ended?(value)
+ infinity?(value) || unboundable?(value)
+ 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..ddc9e394dd
--- /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 optimizer_hints(*hints)
+ unless hints.empty?
+ @ctx.optimizer_hints = Arel::Nodes::OptimizerHints.new(hints)
+ end
+ self
+ 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
+
+ def comment(*values)
+ @ctx.comment = Nodes::Comment.new(values)
+ self
+ 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..d696edc507
--- /dev/null
+++ b/activerecord/lib/arel/visitors/depth_first.rb
@@ -0,0 +1,204 @@
+# 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
+ alias :visit_Arel_Nodes_OptimizerHints :unary
+ alias :visit_Arel_Nodes_ValuesList :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_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_Arel_Nodes_Comment(o)
+ visit o.values
+ 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..ecc386de07
--- /dev/null
+++ b/activerecord/lib/arel/visitors/dot.rb
@@ -0,0 +1,297 @@
+# 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_ValuesList(o)
+ visit_edge o, "rows"
+ 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_OptimizerHints :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_Arel_Nodes_Comment(o)
+ visit_edge(o, "values")
+ end
+
+ 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..5cf958f5f0
--- /dev/null
+++ b/activerecord/lib/arel/visitors/ibm_db.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class IBM_DB < Arel::Visitors::ToSql
+ private
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector = super
+ maybe_visit o.optimizer_hints, collector
+ end
+
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ hints = o.expr.map { |v| sanitize_as_sql_comment(v) }.join
+ collector << "/* <OPTGUIDELINES>#{hints}</OPTGUIDELINES> */"
+ end
+
+ 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
+
+ def collect_optimizer_hints(o, 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..1a4ad1c8d8
--- /dev/null
+++ b/activerecord/lib/arel/visitors/informix.rb
@@ -0,0 +1,62 @@
+# 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
+ 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_OptimizerHints(o, collector)
+ hints = o.expr.map { |v| sanitize_as_sql_comment(v) }.join(", ")
+ collector << "/*+ #{hints} */"
+ 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..8475139870
--- /dev/null
+++ b/activerecord/lib/arel/visitors/mssql.rb
@@ -0,0 +1,157 @@
+# 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 visit_Arel_Nodes_SelectCore(o, collector)
+ collector = super
+ maybe_visit o.optimizer_hints, collector
+ end
+
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ hints = o.expr.map { |v| sanitize_as_sql_comment(v) }.join(", ")
+ collector << "OPTION (#{hints})"
+ 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 collect_optimizer_hints(o, collector)
+ collector
+ 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..9a7fe4d626
--- /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
+ 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..8296f1cdc1
--- /dev/null
+++ b/activerecord/lib/arel/visitors/postgresql.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class PostgreSQL < Arel::Visitors::ToSql
+ 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 "
+ 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..277d553e6c
--- /dev/null
+++ b/activerecord/lib/arel/visitors/to_sql.rb
@@ -0,0 +1,860 @@
+# 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
+ 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).to_s
+ end
+ collector << ", " unless k == row_len
+ end
+ collector << ")"
+ collector << ", " unless i == len
+ }
+ collector
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ if o.with
+ collector = visit o.with, collector
+ collector << " "
+ 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 << ", " unless len == i
+ }
+ end
+
+ visit_Arel_Nodes_SelectOptions(o, collector)
+ end
+
+ def visit_Arel_Nodes_SelectOptions(o, collector)
+ collector = maybe_visit o.limit, collector
+ collector = maybe_visit o.offset, collector
+ maybe_visit o.lock, collector
+ end
+
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector << "SELECT"
+
+ collector = collect_optimizer_hints(o, collector)
+ collector = maybe_visit o.set_quantifier, collector
+
+ collect_nodes_for o.projections, collector, " "
+
+ 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 "
+
+ maybe_visit o.comment, collector
+ end
+
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
+ hints = o.expr.map { |v| sanitize_as_sql_comment(v) }.join(" ")
+ collector << "/*+ #{hints} */"
+ end
+
+ def visit_Arel_Nodes_Comment(o, collector)
+ collector << o.values.map { |v| "/* #{sanitize_as_sql_comment(v)} */" }.join(" ")
+ end
+
+ def collect_nodes_for(nodes, collector, spacer, connector = ", ")
+ 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, ", "
+ end
+
+ def visit_Arel_Nodes_WithRecursive(o, collector)
+ collector << "WITH RECURSIVE "
+ inject_join o.children, collector, ", "
+ 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 << " " if o.partitions.any?
+ collector << "ORDER BY "
+ collector = inject_join o.orders, collector, ", "
+ end
+
+ if o.framing
+ collector << " " 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 << " " if o.left
+ collector = inject_join o.right, collector, " "
+ 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 << " "
+ 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 << " "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_InnerJoin(o, collector)
+ collector << "INNER JOIN "
+ collector = visit o.left, collector
+ if o.right
+ collector << " "
+ 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.delete_if { |value| unboundable?(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.delete_if { |value| unboundable?(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
+
+ return collector << "1=0" if unboundable?(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
+
+ return collector << "1=1" if unboundable?(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 sanitize_as_sql_comment(value)
+ return value if Arel::Nodes::SqlLiteral === value
+ @connection.sanitize_as_sql_comment(value)
+ end
+
+ def collect_optimizer_hints(o, collector)
+ maybe_visit o.optimizer_hints, collector
+ 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 unboundable?(value)
+ value.respond_to?(:unboundable?) && value.unboundable?
+ 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/model/templates/application_record.rb b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt
index 60050e0bf8..60050e0bf8 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/application_record.rb
+++ b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt
diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb
index c2b2209638..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:
@@ -20,6 +22,27 @@ module ActiveRecord
key_type = options[:primary_key_type]
", id: :#{key_type}" if key_type
end
+
+ def db_migrate_path
+ if defined?(Rails.application) && Rails.application
+ 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 4e5872b585..cb2c74f1ca 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -1,58 +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
@@ -60,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 5f7201cfe1..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[<%= 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/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..562543f981
--- /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 %>
+<% elsif !attribute.virtual? -%>
+ 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 481c70201b..0000000000
--- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-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/migration/templates/migration.rb.tt b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
new file mode 100644
index 0000000000..c07380bec9
--- /dev/null
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
@@ -0,0 +1,48 @@
+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
+ <%- elsif !attribute.virtual? -%>
+ 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 %>
+ <%- elsif !attribute.virtual? -%>
+ <%= '# ' 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 -%>
+ <%- if !attribute.virtual? %>
+ remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
+ <%- end -%>
+ <%- 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 f191eff5bf..c71bbdcab8 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -1,9 +1,11 @@
-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
@@ -12,65 +14,35 @@ module ActiveRecord
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")
- generate_application_record
+ 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
- generate_application_record
+ 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
- # FIXME: Change this file to a symlink once RubyGems 2.5.0 is required.
- def generate_application_record
- if self.behavior == :invoke && !application_record_exist?
- template 'application_record.rb', application_record_file_name
- end
- end
-
# Used by the migration template to determine the parent name of the model
def parent_class_name
- options[:parent] || determine_default_parent_class
- end
-
- def application_record_exist?
- file_exist = nil
- in_root { file_exist = File.exist?(application_record_file_name) }
- file_exist
- end
-
- def application_record_file_name
- @application_record_file_name ||= if mountable_engine?
- "app/models/#{namespaced_path}/application_record.rb"
- else
- 'app/models/application_record.rb'
- end
- end
-
- def determine_default_parent_class
- if application_record_exist?
- "ApplicationRecord"
- else
- "ActiveRecord::Base"
- end
+ options[:parent] || "ApplicationRecord"
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
deleted file mode 100644
index 55dc65c8ad..0000000000
--- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-<% module_namespacing do -%>
-class <%= class_name %> < <%= parent_class_name.classify %>
-<% attributes.select(&:reference?).each do |attribute| -%>
- belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %>
-<% end -%>
-<% attributes.select(&:token?).each do |attribute| -%>
- has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %>
-<% end -%>
-<% if attributes.any?(&:password_digest?) -%>
- has_secure_password
-<% end -%>
-end
-<% end -%>
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
new file mode 100644
index 0000000000..c1c03e2762
--- /dev/null
+++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
@@ -0,0 +1,22 @@
+<% module_namespacing do -%>
+class <%= class_name %> < <%= parent_class_name.classify %>
+<% attributes.select(&:reference?).each do |attribute| -%>
+ belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %>
+<% end -%>
+<% attributes.select(&:rich_text?).each do |attribute| -%>
+ has_rich_text :<%= attribute.name %>
+<% end -%>
+<% attributes.select(&:attachment?).each do |attribute| -%>
+ has_one_attached :<%= attribute.name %>
+<% end -%>
+<% attributes.select(&:attachments?).each do |attribute| -%>
+ has_many_attached :<%= attribute.name %>
+<% end -%>
+<% attributes.select(&:token?).each do |attribute| -%>
+ has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %>
+<% end -%>
+<% if attributes.any?(&:password_digest?) -%>
+ has_secure_password
+<% end -%>
+end
+<% end -%>
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 43c817e057..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)
@@ -9,7 +11,7 @@ module ActiveRecord
class FakeAdapter < AbstractAdapter
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
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 4f389e9249..2b20d842e8 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -1,12 +1,17 @@
+# 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
##
@@ -17,7 +22,7 @@ module ActiveRecord
b = Book.create(name: "my \x00 book")
b.reload
assert_equal "my \x00 book", b.name
- b.update_attributes(name: "my other \x00 book")
+ b.update(name: "my other \x00 book")
b.reload
assert_equal "my other \x00 book", b.name
end
@@ -29,100 +34,115 @@ module ActiveRecord
assert_nothing_raised { Book.destroy(0) }
end
- def test_tables
- tables = nil
- ActiveSupport::Deprecation.silence { tables = @connection.tables }
- assert tables.include?("accounts")
- assert tables.include?("authors")
- assert tables.include?("tasks")
- assert tables.include?("topics")
+ def test_valid_column
+ @connection.native_database_types.each_key do |type|
+ assert @connection.valid_type?(type)
+ end
end
- def test_table_exists?
- ActiveSupport::Deprecation.silence do
- assert @connection.table_exists?("accounts")
- assert !@connection.table_exists?("nonexistingtable")
- assert !@connection.table_exists?(nil)
- end
+ def test_invalid_column
+ assert_not @connection.valid_type?(:foobar)
end
- def test_table_exists_checking_both_tables_and_views_is_deprecated
- assert_deprecated { @connection.table_exists?("accounts") }
+ def test_tables
+ tables = @connection.tables
+ 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?(: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 data_sources.include?("accounts")
- assert data_sources.include?("authors")
- assert data_sources.include?("tasks")
- assert data_sources.include?("topics")
+ 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?
+ 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 !indexes.first.unique
- assert_equal ["firm_id"], indexes.first.columns
- else
- warn "#{@connection.class} does not respond to #indexes"
- end
+ @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
+ 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 => idx_name) rescue nil
+ @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
+ def test_exec_query_returns_an_empty_result
+ result = @connection.exec_query "INSERT INTO subscribers(nick) VALUES('me')"
+ assert_instance_of(ActiveRecord::Result, result)
+ end
+
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))
-
- config = ARTest.connection_config
- ActiveRecord::Base.connection.execute(
- "SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \
- "FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses"
- )
- end
- ensure
- ActiveRecord::Base.establish_connection :arunit
+ assert_nothing_raised do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"].except(:database))
+
+ config = ARTest.connection_config
+ ActiveRecord::Base.connection.execute(
+ "SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \
+ "FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses"
+ )
end
+ ensure
+ ActiveRecord::Base.establish_connection :arunit
end
end
@@ -133,9 +153,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
@@ -143,23 +163,62 @@ 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_preventing_writes_predicate
+ assert_not_predicate @connection, :preventing_writes?
- 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
+ @connection.while_preventing_writes do
+ assert_predicate @connection, :preventing_writes?
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! }
+ assert_not_predicate @connection, :preventing_writes?
+ end
+
+ def test_errors_when_an_insert_query_is_called_while_preventing_writes
+ assert_no_queries do
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @connection.while_preventing_writes do
+ @connection.transaction do
+ @connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')", nil, false)
+ end
+ end
+ end
+ end
+ end
+
+ def test_errors_when_an_update_query_is_called_while_preventing_writes
+ @connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
+
+ assert_no_queries do
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @connection.while_preventing_writes do
+ @connection.transaction do
+ @connection.update("UPDATE subscribers SET nick = '9989' WHERE nick = '138853948594'")
+ end
+ end
+ end
+ end
+ end
+
+ def test_errors_when_a_delete_query_is_called_while_preventing_writes
+ @connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
+
+ assert_no_queries do
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @connection.while_preventing_writes do
+ @connection.transaction do
+ @connection.delete("DELETE FROM subscribers WHERE nick = '138853948594'")
+ end
+ end
+ end
+ end
+ end
+
+ def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes
+ @connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
+
+ @connection.while_preventing_writes do
+ result = @connection.select_all("SELECT subscribers.* FROM subscribers WHERE nick = '138853948594'")
+ assert_equal 1, result.length
end
end
@@ -172,51 +231,52 @@ module ActiveRecord
assert_not_nil error.cause
end
+ 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
- error = assert_raises(ActiveRecord::InvalidForeignKey) do
- # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
- if @connection.prefetch_primary_key?
- id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id"))
- @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)"
- else
- @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)"
- end
+ 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'
- end
-
- error = assert_raises(ActiveRecord::InvalidForeignKey) do
- has_fk = klass_has_fk.new
- has_fk.fk_id = 1231231231
- has_fk.save(validate: false)
+ def test_numeric_value_out_of_ranges_are_translated_to_specific_exception
+ error = assert_raises(ActiveRecord::RangeError) do
+ Book.connection.create("INSERT INTO books(author_id) VALUES (9223372036854775808)")
end
assert_not_nil error.cause
end
end
- 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
@@ -224,22 +284,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)
@@ -249,57 +348,203 @@ module ActiveRecord
assert_equal "special_db_type", @connection.type_to_sql(:special_db_type)
end
- unless current_adapter?(:PostgreSQLAdapter)
- def test_log_invalid_encoding
- error = assert_raise ActiveRecord::StatementInvalid do
- @connection.send :log, "SELECT 'Ñ‹' FROM DUAL" do
- raise 'Ñ‹'.force_encoding(Encoding::ASCII_8BIT)
- end
- end
+ def test_supports_foreign_keys_in_create_is_deprecated
+ assert_deprecated { @connection.supports_foreign_keys_in_create? }
+ end
- assert_not_nil error.cause
+ 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
- if current_adapter?(:Mysql2Adapter, :SQLite3Adapter)
- def test_tables_returning_both_tables_and_views_is_deprecated
- assert_deprecated { @connection.tables }
+ 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_passing_arguments_to_tables_is_deprecated
- assert_deprecated { @connection.tables(:books) }
+ 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
self.use_transactional_tests = false
- class Klass < ActiveRecord::Base
- end
+ fixtures :posts, :authors, :author_addresses
def setup
- Klass.establish_connection :arunit
- @connection = Klass.connection
- end
-
- teardown do
- Klass.remove_connection
+ @connection = ActiveRecord::Base.connection
end
unless in_memory_db?
+ test "reconnect after a disconnect" do
+ assert_predicate @connection, :active?
+ @connection.disconnect!
+ assert_not_predicate @connection, :active?
+ @connection.reconnect!
+ assert_predicate @connection, :active?
+ end
+
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?
+ ensure
+ @connection.reconnect!
+ end
+ end
+
+ def test_truncate
+ assert_operator @connection.query_value("SELECT COUNT(*) FROM posts"), :>, 0
+
+ @connection.truncate("posts")
+
+ assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM posts")
+ end
+
+ def test_truncate_tables
+ assert_operator @connection.query_value("SELECT COUNT(*) FROM posts"), :>, 0
+ assert_operator @connection.query_value("SELECT COUNT(*) FROM authors"), :>, 0
+ assert_operator @connection.query_value("SELECT COUNT(*) FROM author_addresses"), :>, 0
+
+ @connection.truncate_tables("author_addresses", "authors", "posts")
+
+ assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM posts")
+ assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM authors")
+ assert_equal 0, @connection.query_value("SELECT COUNT(*) FROM author_addresses")
+ 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/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index 99f97c7914..88c2ac5d0a 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -1,13 +1,16 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/connection_helper'
+require "support/connection_helper"
class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
include ConnectionHelper
def setup
+ ActiveRecord::Base.connection.send(:default_row_format)
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
@@ -21,60 +24,63 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
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).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 = /\ACREATE 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
+ assert_match expected, actual
end
- expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)) ) ENGINE=InnoDB"
+ expected = /\ACREATE 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
- assert_equal expected, actual
+ assert_match expected, actual
end
def test_index_in_bulk_change
@@ -89,8 +95,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
@@ -101,14 +107,20 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
end
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})
+ if row_format_dynamic_by_default?
+ assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8mb4`", create_database(:matt)
+ else
+ error = assert_raises(RuntimeError) { create_database(:matt) }
+ expected = "Configure a supported :charset and ensure innodb_large_prefix is enabled to support indexes on varchar(255) string columns."
+ assert_equal expected, error.message
+ end
+ 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'})
+ create_database(:luca, charset: "latin1")
+ assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, charset: "latin1")
end
def test_add_column
@@ -116,51 +128,51 @@ 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
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
+ ActiveRecord::Base.connection.create_table :delete_me
+ ActiveRecord::Base.connection.add_timestamps :delete_me, null: true
+ assert column_exists?("delete_me", "updated_at", "datetime")
+ assert column_exists?("delete_me", "created_at", "datetime")
+ ensure
+ ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
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
+ ActiveRecord::Base.connection.create_table :delete_me do |t|
+ t.timestamps null: true
end
+ ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true
+ assert_not column_exists?("delete_me", "updated_at", "datetime")
+ assert_not column_exists?("delete_me", "created_at", "datetime")
+ ensure
+ ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
end
def test_indexes_in_create
- ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false)
- ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
+ assert_called_with(
+ ActiveRecord::Base.connection,
+ :data_source_exists?,
+ [:temp],
+ returns: false
+ ) do
+ expected = /\ACREATE TEMPORARY TABLE `temp` \( INDEX `index_temp_on_zip` \(`zip`\)\)(?: ROW_FORMAT=DYNAMIC)? 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_match expected, actual
end
-
- assert_equal expected, actual
end
private
@@ -182,9 +194,4 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
def method_missing(method_symbol, *arguments)
ActiveRecord::Base.connection.send(method_symbol, *arguments)
end
-
- def column_present?(table_name, column_name, type)
- results = ActiveRecord::Base.connection.select_all("SHOW FIELDS FROM #{table_name} LIKE '#{column_name}'")
- results.first && results.first['Type'] == type
- end
end
diff --git a/activerecord/test/cases/adapters/mysql2/annotate_test.rb b/activerecord/test/cases/adapters/mysql2/annotate_test.rb
new file mode 100644
index 0000000000..b512540073
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/annotate_test.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+class Mysql2AnnotateTest < ActiveRecord::Mysql2TestCase
+ fixtures :posts
+
+ def test_annotate_wraps_content_in_an_inline_comment
+ assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do
+ posts = Post.select(:id).annotate("foo")
+ assert posts.first
+ end
+ end
+
+ def test_annotate_is_sanitized
+ assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do
+ posts = Post.select(:id).annotate("*/foo/*")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do
+ posts = Post.select(:id).annotate("**//foo//**")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/ /\* bar \*/}) do
+ posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* \+ MAX_EXECUTION_TIME\(1\) \*/}) do
+ posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)")
+ assert posts.first
+ end
+ 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 9cb05119a2..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,46 +9,46 @@ 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)
@@ -54,8 +56,8 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase
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')
+ 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)
diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb
index 668c07dacb..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+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 c4715393b3..9c6566106a 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,52 +22,79 @@ 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
- def test_truncate
- rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
- count = rows.first.values.first
- assert_operator count, :>, 0
-
- ActiveRecord::Base.connection.truncate("comments")
- rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
- count = rows.first.values.first
- assert_equal 0, count
- 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
def test_mysql_default_in_strict_mode
@@ -92,29 +121,29 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
def test_mysql_sql_mode_variable_overrides_strict_mode
run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
- result = ActiveRecord::Base.connection.select_value('SELECT @@SESSION.sql_mode')
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { "sql_mode" => "ansi" }))
+ result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.sql_mode")
assert_no_match %r(STRICT_ALL_TABLES), result
end
end
- def test_passing_arbitary_flags_to_adapter
+ 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]
+ 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'] }))
+ 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 => {:default_week_format => 3}}))
+ 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
@@ -122,7 +151,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
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
@@ -130,21 +159,23 @@ 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_name"
+ 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"
@@ -155,19 +186,19 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
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'
+ 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_name"
+ 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'
+ "expected release_advisory_lock to return false when there was no lock to release"
end
- protected
+ private
- def test_lock_free(lock_name)
- @connection.select_value("SELECT IS_FREE_LOCK('#{lock_name}');") == 1
- end
+ 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 35dbc76d1b..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,22 +7,17 @@ class Mysql2EnumTest < ActiveRecord::Mysql2TestCase
end
def test_enum_limit
- column = EnumTest.columns_hash['enum_column']
+ column = EnumTest.columns_hash["enum_column"]
assert_equal 8, column.limit
end
- def test_should_not_be_blob_or_text_column
- column = EnumTest.columns_hash['enum_column']
- assert_not column.blob_or_text_column?
- end
-
def test_should_not_be_unsigned
- column = EnumTest.columns_hash['enum_column']
- assert_not column.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 column.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 b783b5fcd9..b8e778f0b0 100644
--- a/activerecord/test/cases/adapters/mysql2/explain_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb
@@ -1,21 +1,23 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/developer'
-require 'models/computer'
+require "models/author"
+require "models/post"
class Mysql2ExplainTest < ActiveRecord::Mysql2TestCase
- fixtures :developers
+ fixtures :authors
def test_explain_for_one_query
- explain = Developer.where(id: 1).explain
- assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
- assert_match %r(developers |.* const), explain
+ explain = Author.where(id: 1).explain
+ assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain
+ assert_match %r(authors |.* const), explain
end
def test_explain_with_eager_loading
- explain = Developer.where(id: 1).includes(:audit_logs).explain
- assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
- assert_match %r(developers |.* const), explain
- assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain
- assert_match %r(audit_logs |.* ALL), explain
+ explain = Author.where(id: 1).includes(:posts).explain
+ assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain
+ assert_match %r(authors |.* const), explain
+ assert_match %(EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`author_id` = 1), explain
+ assert_match %r(posts |.* ALL), explain
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb
index c8c933af5e..de78ba91f5 100644
--- a/activerecord/test/cases/adapters/mysql2/json_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/json_test.rb
@@ -1,172 +1,24 @@
-require 'cases/helper'
-require 'support/schema_dumping_helper'
+# frozen_string_literal: true
-if ActiveRecord::Base.connection.supports_json?
-class Mysql2JSONTest < ActiveRecord::Mysql2TestCase
- include SchemaDumpingHelper
- self.use_transactional_tests = false
-
- class JsonDataType < ActiveRecord::Base
- self.table_name = 'json_data_type'
-
- store_accessor :settings, :resolution
- end
+require "cases/helper"
+require "cases/json_shared_test_cases"
- def setup
- @connection = ActiveRecord::Base.connection
- begin
- @connection.create_table('json_data_type') do |t|
- t.json 'payload'
- t.json 'settings'
+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
- 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 :json, column.type
- assert_equal 'json', column.sql_type
-
- type = JsonDataType.type_for_attribute("payload")
- assert_not type.binary?
- end
-
- def test_change_table_supports_json
- @connection.change_table('json_data_type') do |t|
- t.json 'users'
- end
- JsonDataType.reset_column_information
- column = JsonDataType.columns_hash['users']
- assert_equal :json, column.type
- end
-
- def test_schema_dumping
- output = dump_table_schema("json_data_type")
- assert_match(/t\.json\s+"settings"/, 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
-
- 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)
- 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
-
- 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
+ private
+ def column_type
+ :json
+ end
end
end
-end
diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
index 00d23740b6..6ade2eec24 100644
--- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "support/ddl_helper"
@@ -11,8 +13,8 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
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')
+ @conn.exec_query("INSERT INTO ex (number) VALUES (1)")
+ @conn.exec_query("DELETE FROM ex WHERE number = 1")
end
end
end
@@ -23,25 +25,25 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
end
def test_columns_for_distinct_one_order
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id", ["posts.created_at desc"])
end
def test_columns_for_distinct_few_orders
- assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
+ assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id",
@conn.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
end
def test_columns_for_distinct_with_case
assert_equal(
- 'posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0',
- @conn.columns_for_distinct('posts.id',
+ "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id",
+ @conn.columns_for_distinct("posts.id",
["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"])
)
end
def test_columns_for_distinct_blank_not_nil_orders
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "])
end
@@ -50,13 +52,169 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
def order.to_sql
"posts.created_at desc"
end
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id", [order])
end
- private
+ def test_errors_for_bigint_fks_on_integer_pk_table_in_alter_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_includes error.message, <<~MSG.squish
+ Column `old_car_id` on table `engines` does not match column `id` on `old_cars`,
+ which has type `int(11)`. To resolve this issue, change the type of the `old_car_id`
+ column on `engines` to be :integer. (For example `t.integer :old_car_id`).
+ MSG
+ assert_not_nil error.cause
+ ensure
+ @conn.execute("ALTER TABLE engines DROP COLUMN old_car_id") rescue nil
+ end
+
+ def test_errors_for_bigint_fks_on_integer_pk_table_in_create_table
+ # table old_cars has primary key of integer
+
+ error = assert_raises(ActiveRecord::MismatchedForeignKey) do
+ @conn.execute(<<~SQL)
+ CREATE TABLE activerecord_unittest.foos (
+ id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ old_car_id bigint,
+ INDEX index_foos_on_old_car_id (old_car_id),
+ CONSTRAINT fk_rails_ff771f3c96 FOREIGN KEY (old_car_id) REFERENCES old_cars (id)
+ )
+ SQL
+ end
+
+ assert_includes error.message, <<~MSG.squish
+ Column `old_car_id` on table `foos` does not match column `id` on `old_cars`,
+ which has type `int(11)`. To resolve this issue, change the type of the `old_car_id`
+ column on `foos` to be :integer. (For example `t.integer :old_car_id`).
+ MSG
+ assert_not_nil error.cause
+ ensure
+ @conn.drop_table :foos, if_exists: true
+ end
+
+ def test_errors_for_integer_fks_on_bigint_pk_table_in_create_table
+ # table old_cars has primary key of bigint
+
+ error = assert_raises(ActiveRecord::MismatchedForeignKey) do
+ @conn.execute(<<~SQL)
+ CREATE TABLE activerecord_unittest.foos (
+ id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ car_id int,
+ INDEX index_foos_on_car_id (car_id),
+ CONSTRAINT fk_rails_ff771f3c96 FOREIGN KEY (car_id) REFERENCES cars (id)
+ )
+ SQL
+ end
+
+ assert_includes error.message, <<~MSG.squish
+ Column `car_id` on table `foos` does not match column `id` on `cars`,
+ which has type `bigint(20)`. To resolve this issue, change the type of the `car_id`
+ column on `foos` to be :bigint. (For example `t.bigint :car_id`).
+ MSG
+ assert_not_nil error.cause
+ ensure
+ @conn.drop_table :foos, if_exists: true
+ end
+
+ def test_errors_for_bigint_fks_on_string_pk_table_in_create_table
+ # table old_cars has primary key of string
+
+ error = assert_raises(ActiveRecord::MismatchedForeignKey) do
+ @conn.execute(<<~SQL)
+ CREATE TABLE activerecord_unittest.foos (
+ id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ subscriber_id bigint,
+ INDEX index_foos_on_subscriber_id (subscriber_id),
+ CONSTRAINT fk_rails_ff771f3c96 FOREIGN KEY (subscriber_id) REFERENCES subscribers (nick)
+ )
+ SQL
+ end
+
+ assert_includes error.message, <<~MSG.squish
+ Column `subscriber_id` on table `foos` does not match column `nick` on `subscribers`,
+ which has type `varchar(255)`. To resolve this issue, change the type of the `subscriber_id`
+ column on `foos` to be :string. (For example `t.string :subscriber_id`).
+ MSG
+ assert_not_nil error.cause
+ ensure
+ @conn.drop_table :foos, if_exists: true
+ end
+
+ def test_errors_when_an_insert_query_is_called_while_preventing_writes
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @conn.while_preventing_writes do
+ @conn.insert("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')")
+ end
+ end
+ end
+
+ def test_errors_when_an_update_query_is_called_while_preventing_writes
+ @conn.insert("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')")
+
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @conn.while_preventing_writes do
+ @conn.update("UPDATE `engines` SET `engines`.`car_id` = '9989' WHERE `engines`.`car_id` = '138853948594'")
+ end
+ end
+ end
+
+ def test_errors_when_a_delete_query_is_called_while_preventing_writes
+ @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')")
+
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @conn.while_preventing_writes do
+ @conn.execute("DELETE FROM `engines` where `engines`.`car_id` = '138853948594'")
+ end
+ end
+ end
+
+ def test_errors_when_a_replace_query_is_called_while_preventing_writes
+ @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')")
+
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @conn.while_preventing_writes do
+ @conn.execute("REPLACE INTO `engines` SET `engines`.`car_id` = '249823948'")
+ end
+ end
+ end
+
+ def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes
+ @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')")
+
+ @conn.while_preventing_writes do
+ assert_equal 1, @conn.execute("SELECT `engines`.* FROM `engines` WHERE `engines`.`car_id` = '138853948594'").entries.count
+ end
+ end
+
+ def test_doesnt_error_when_a_show_query_is_called_while_preventing_writes
+ @conn.while_preventing_writes do
+ assert_equal 2, @conn.execute("SHOW FULL FIELDS FROM `engines`").entries.count
+ end
+ end
+
+ def test_doesnt_error_when_a_set_query_is_called_while_preventing_writes
+ @conn.while_preventing_writes do
+ assert_nil @conn.execute("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci")
+ end
+ end
+
+ def test_doesnt_error_when_a_read_query_with_leading_chars_is_called_while_preventing_writes
+ @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')")
- def with_example_table(definition = 'id int auto_increment primary key, number int, data varchar(255)', &block)
- super(@conn, 'ex', definition, &block)
+ @conn.while_preventing_writes do
+ assert_equal 1, @conn.execute("(\n( SELECT `engines`.* FROM `engines` WHERE `engines`.`car_id` = '138853948594' ) )").entries.count
+ end
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/optimizer_hints_test.rb b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb
new file mode 100644
index 0000000000..b9794c5710
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+if supports_optimizer_hints?
+ class Mysql2OptimzerHintsTest < ActiveRecord::Mysql2TestCase
+ fixtures :posts
+
+ def test_optimizer_hints
+ assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
+ posts = Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |"
+ end
+ end
+
+ def test_optimizer_hints_is_sanitized
+ assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
+ posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |"
+ end
+
+ assert_sql(%r{\ASELECT /\*\+ `posts`\.\*, \*/}) do
+ posts = Post.optimizer_hints("**// `posts`.*, //**")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_equal({ "id" => 1 }, posts.first.as_json)
+ end
+ end
+
+ def test_optimizer_hints_with_unscope
+ assert_sql(%r{\ASELECT `posts`\.`id`}) do
+ posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ posts.unscope(:optimizer_hints).load
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/quoting_test.rb b/activerecord/test/cases/adapters/mysql2/quoting_test.rb
deleted file mode 100644
index 2de7e1b526..0000000000
--- a/activerecord/test/cases/adapters/mysql2/quoting_test.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require "cases/helper"
-
-class Mysql2QuotingTest < ActiveRecord::Mysql2TestCase
- setup do
- @connection = ActiveRecord::Base.connection
- end
-
- test 'quoted date precision for gte 5.6.4' do
- @connection.stubs(:full_version).returns('5.6.4')
- @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
- t = Time.now.change(usec: 1)
- assert_match(/\.000001\z/, @connection.quoted_date(t))
- end
-
- test 'quoted date precision for lt 5.6.4' do
- @connection.stubs(:full_version).returns('5.6.3')
- @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
- t = Time.now.change(usec: 1)
- assert_no_match(/\.000001\z/, @connection.quoted_date(t))
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql2/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 7c89fda582..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"
@@ -16,9 +20,9 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase
table_name = ActiveRecord::SchemaMigration.table_name
connection.drop_table table_name, if_exists: true
- connection.initialize_schema_migrations_table
+ ActiveRecord::SchemaMigration.create_table
- assert connection.column_exists?(table_name, :version, :string, collation: 'utf8_general_ci')
+ assert connection.column_exists?(table_name, :version, :string)
end
end
@@ -27,33 +31,35 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase
table_name = ActiveRecord::InternalMetadata.table_name
connection.drop_table table_name, if_exists: true
- connection.initialize_internal_metadata_table
+ ActiveRecord::InternalMetadata.create_table
- assert connection.column_exists?(table_name, :key, :string, collation: 'utf8_general_ci')
+ assert connection.column_exists?(table_name, :key, :string)
end
+ ensure
+ ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment
end
private
- def with_encoding_utf8mb4
- database_name = connection.current_database
- database_info = connection.select_one("SELECT * FROM information_schema.schemata WHERE schema_name = '#{database_name}'")
+ 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"]
+ original_charset = database_info["DEFAULT_CHARACTER_SET_NAME"]
+ original_collation = database_info["DEFAULT_COLLATION_NAME"]
- execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET utf8mb4")
+ 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
+ yield
+ ensure
+ execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}")
+ end
- def connection
- @connection ||= ActiveRecord::Base.connection
- end
+ def connection
+ @connection ||= ActiveRecord::Base.connection
+ end
- def execute(sql)
- connection.execute(sql)
- 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 43957791b1..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
@@ -16,7 +18,7 @@ module ActiveRecord
@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
@@ -31,13 +33,13 @@ module ActiveRecord
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_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' }
+ 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
@@ -56,7 +58,7 @@ module ActiveRecord
end
def test_primary_key
- assert_equal 'id', @omgpost.primary_key
+ assert_equal "id", @omgpost.primary_key
end
def test_data_source_exists?
@@ -65,22 +67,22 @@ module ActiveRecord
end
def test_data_source_exists_wrong_schema
- assert(!@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist")
+ 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
@@ -103,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
index 4197ba45f1..7b6dce71e9 100644
--- a/activerecord/test/cases/adapters/mysql2/sp_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/sp_test.rb
@@ -1,13 +1,15 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
-require 'models/reply'
+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'
+ unless ActiveRecord::Base.connection.version >= "5.6.0"
skip("no stored procedure support")
end
end
@@ -15,21 +17,21 @@ class Mysql2StoredProcedureTest < ActiveRecord::Mysql2TestCase
# Test that MySQL allows multiple results for stored procedures
#
# In MySQL 5.6, CLIENT_MULTI_RESULTS is enabled by default.
- # http://dev.mysql.com/doc/refman/5.6/en/call.html
+ # https://dev.mysql.com/doc/refman/5.6/en/call.html
def test_multi_results
- rows = @connection.select_rows('CALL ten();')
+ 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']
+ 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);'
+ topics = Topic.find_by_sql "CALL topics(3);"
assert_equal 3, topics.size
assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select'"
end
diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
index 4926bc2267..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', 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 0a9703263e..97da96003d 100644
--- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "support/schema_dumping_helper"
@@ -15,6 +17,7 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
t.bigint :unsigned_bigint, unsigned: true
t.float :unsigned_float, unsigned: true
t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2
+ t.column :unsigned_zerofill, "int unsigned zerofill"
end
end
@@ -28,16 +31,16 @@ 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(RangeError) do
+ assert_raise(ActiveModel::RangeError) do
UnsignedType.create(unsigned_bigint: -10)
end
- assert_raise(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::RangeError) do
UnsignedType.create(unsigned_float: -10.0)
end
- assert_raise(ActiveRecord::StatementInvalid) do
+ assert_raise(ActiveRecord::RangeError) do
UnsignedType.create(unsigned_decimal: -10.0)
end
end
@@ -50,16 +53,16 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
t.unsigned_decimal :unsigned_decimal_t, precision: 10, scale: 2
end
- @connection.columns("unsigned_types").select { |c| /^unsigned_/ === c.name }.each do |column|
- assert column.unsigned?
+ @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+limit: 24,\s+unsigned: true$}, schema
- assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema
+ 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 ed44bf7362..62efaf3bfe 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,48 +19,68 @@ 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.send(:define_method, :index_name_exists?) { |*| false }
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.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
- 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_exists?
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.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'
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.define_method(:index_name_for_remove) do |*|
+ "index_people_on_last_name"
end
expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name")
@@ -66,7 +90,18 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
add_index(:people, :last_name, algorithm: :copy)
end
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_for_remove
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.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/annotate_test.rb b/activerecord/test/cases/adapters/postgresql/annotate_test.rb
new file mode 100644
index 0000000000..42a2861511
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/annotate_test.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+class PostgresqlAnnotateTest < ActiveRecord::PostgreSQLTestCase
+ fixtures :posts
+
+ def test_annotate_wraps_content_in_an_inline_comment
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
+ posts = Post.select(:id).annotate("foo")
+ assert posts.first
+ end
+ end
+
+ def test_annotate_is_sanitized
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
+ posts = Post.select(:id).annotate("*/foo/*")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
+ posts = Post.select(:id).annotate("**//foo//**")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/ /\* bar \*/}) do
+ posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* \+ MAX_EXECUTION_TIME\(1\) \*/}) do
+ posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)")
+ assert posts.first
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 380a90d765..2e7a4b498f 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", force: true 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,23 @@ 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_from_non_array_to_array
+ @connection.add_column :pg_arrays, :snippets, :string
+ @connection.change_column :pg_arrays, :snippets, :text, array: true, default: [], using: "string_to_array(\"snippets\", ',')"
+
+ PgArray.reset_column_information
+ column = PgArray.columns_hash["snippets"]
+
+ assert_equal :text, column.type
+ assert_equal [], PgArray.column_defaults["snippets"]
+ assert_predicate column, :array?
end
def test_change_column_cant_make_non_array_column_to_array
@@ -88,17 +137,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 +159,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 +187,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 +213,39 @@ 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_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 +261,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 +283,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 +341,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 +357,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 cec6081aec..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
@@ -65,10 +67,11 @@ class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase
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 7adc070430..3988c2adca 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -1,29 +1,31 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_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
@@ -32,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
@@ -42,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
@@ -66,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
@@ -88,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
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..79e9efcf06
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class PostgresqlCaseInsensitiveTest < ActiveRecord::PostgreSQLTestCase
+ class Default < ActiveRecord::Base; end
+
+ def test_case_insensitiveness
+ connection = ActiveRecord::Base.connection
+
+ attr = Default.arel_attribute(:char1)
+ comparison = connection.case_insensitive_comparison(attr, nil)
+ assert_match(/lower/i, comparison.to_sql)
+
+ attr = Default.arel_attribute(:char2)
+ comparison = connection.case_insensitive_comparison(attr, nil)
+ assert_match(/lower/i, comparison.to_sql)
+
+ attr = Default.arel_attribute(:char3)
+ comparison = connection.case_insensitive_comparison(attr, nil)
+ assert_match(/lower/i, comparison.to_sql)
+
+ attr = Default.arel_attribute(:multiline_default)
+ comparison = connection.case_insensitive_comparison(attr, 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..683066cdb3 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
@@ -13,14 +15,14 @@ module PostgresqlCompositeBehavior
@connection = ActiveRecord::Base.connection
@connection.transaction do
- @connection.execute <<-SQL
- CREATE TYPE full_address AS
- (
- city VARCHAR(90),
- street VARCHAR(90)
- );
- SQL
- @connection.create_table('postgresql_composites') do |t|
+ @connection.execute <<~SQL
+ CREATE TYPE full_address AS
+ (
+ city VARCHAR(90),
+ street VARCHAR(90)
+ );
+ SQL
+ @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 f8403bfe1a..210758f462 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
@@ -22,24 +25,22 @@ module ActiveRecord
super
end
- def test_truncate
- 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
- 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,137 +55,118 @@ 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
- ActiveSupport::Deprecation.silence { @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
- ActiveSupport::Deprecation.silence { @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
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)
+ @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
+ 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
- # true for this test to run.
- #
- # 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, ...
- # 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?
-
- 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 ' +
- 'server with the "-m fast" option) and then press enter.'
- $stdin.gets
- else
- # We're not capable of terminating the backend ourselves, and
- # we're not allowed to seek assistance; bail out without
- # actually testing anything.
- return
- end
+ assert_predicate @connection, :active?
+
+ 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)
@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
@@ -192,7 +174,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
@@ -201,20 +183,27 @@ 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
+ list_advisory_locks = <<~SQL
SELECT locktype,
(classid::bigint << 32) | objid::bigint AS lock_id
FROM pg_locks
@@ -224,14 +213,14 @@ module ActiveRecord
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}
+ 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}
+ 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
@@ -241,17 +230,21 @@ module ActiveRecord
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'
+ "expected release_advisory_lock to return false when there was no lock to release"
end
end
- protected
-
- def with_warning_suppression
- log_level = @connection.client_min_messages
- @connection.client_min_messages = 'error'
- yield
- @connection.client_min_messages = log_level
+ def test_supports_ranges_is_deprecated
+ assert_deprecated { @connection.supports_ranges? }
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..416a2b141b 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
@@ -11,18 +13,18 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase
def setup
@connection = ActiveRecord::Base.connection
@connection.transaction do
- @connection.execute <<-SQL
+ @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 29bf2c15ea..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 %r(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 %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = \$?1), explain
- assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain
+ 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 b56c226763..df97ab11e7 100644
--- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase
@@ -20,10 +22,6 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase
@connection = ActiveRecord::Base.connection
- unless @connection.supports_extensions?
- return skip("no extension support")
- end
-
@old_schema_migration_table_name = ActiveRecord::SchemaMigration.table_name
@old_table_name_prefix = ActiveRecord::Base.table_name_prefix
@old_table_name_suffix = ActiveRecord::Base.table_name_suffix
diff --git a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb
new file mode 100644
index 0000000000..69339c8a31
--- /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 9e250c2b7c..8c6f046553 100644
--- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/geometric_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 PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
include ConnectionHelper
@@ -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,7 +157,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
p.reload
assert_equal [10.0, 25.0], p.legacy_x
- assert_not p.changed?
+ assert_not_predicate p, :changed?
end
end
@@ -183,51 +178,51 @@ class PostgresqlGeometricTest < ActiveRecord::PostgreSQLTestCase
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)
@@ -263,28 +258,28 @@ class PostgreSQLGeometricLineTest < ActiveRecord::PostgreSQLTestCase
teardown do
if defined?(@connection)
- @connection.drop_table 'postgresql_lines', if_exists: true
+ @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}'
+ a_line: "{2.0, 3, 5.5}"
)
g.save!
h = PostgresqlLine.find(g.id)
- assert_equal '{2,3,5.5}', h.a_line
+ 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)'
+ 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
+ assert_equal "{1.5,-1,0}", h.a_line
end
def test_schema_dumping_for_line_type
@@ -367,12 +362,12 @@ class PostgreSQLGeometricTypesTest < ActiveRecord::PostgreSQLTestCase
private
- def assert_column_exists(column_name)
- assert connection.column_exists?(table_name, column_name)
- end
+ 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
+ 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 27cc65a643..671d8211a7 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -1,327 +1,364 @@
-require "cases/helper"
-require 'support/schema_dumping_helper'
+# frozen_string_literal: true
-if ActiveRecord::Base.connection.supports_extensions?
- class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase
- include SchemaDumpingHelper
- class Hstore < ActiveRecord::Base
- self.table_name = 'hstores'
+require "cases/helper"
+require "support/schema_dumping_helper"
+require "support/stubs/strong_parameters"
- store_accessor :settings, :language, :timezone
- end
+class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+ class Hstore < ActiveRecord::Base
+ self.table_name = "hstores"
- def setup
- @connection = ActiveRecord::Base.connection
+ store_accessor :settings, :language, :timezone
+ 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::Current) do
- def change
- change_table("hstores") do |t|
- t.hstore :keys
- end
+ def test_hstore_migration
+ hstore_migration = Class.new(ActiveRecord::Migration::Current) do
+ def change
+ change_table("hstores") do |t|
+ t.hstore :keys
end
end
-
- hstore_migration.new.suppress_messages do
- hstore_migration.migrate(:up)
- assert_includes @connection.columns(:hstores).map(&:name), "keys"
- hstore_migration.migrate(:down)
- assert_not_includes @connection.columns(:hstores).map(&:name), "keys"
- end
end
- def test_cast_value_on_write
- x = Hstore.new tags: {"bool" => true, "number" => 5}
- assert_equal({"bool" => true, "number" => 5}, x.tags_before_type_cast)
- assert_equal({"bool" => "true", "number" => "5"}, x.tags)
- x.save
- assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags)
+ hstore_migration.new.suppress_messages do
+ hstore_migration.migrate(:up)
+ assert_includes @connection.columns(:hstores).map(&:name), "keys"
+ hstore_migration.migrate(:down)
+ assert_not_includes @connection.columns(:hstores).map(&:name), "keys"
end
+ end
- def test_type_cast_hstore
- assert_equal({'1' => '2'}, @type.deserialize("\"1\"=>\"2\""))
- assert_equal({}, @type.deserialize(""))
- assert_equal({'key'=>nil}, @type.deserialize('key => NULL'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b")))
- end
+ def test_cast_value_on_write
+ x = Hstore.new tags: { "bool" => true, "number" => 5 }
+ assert_equal({ "bool" => true, "number" => 5 }, x.tags_before_type_cast)
+ assert_equal({ "bool" => "true", "number" => "5" }, x.tags)
+ x.save
+ assert_equal({ "bool" => "true", "number" => "5" }, x.reload.tags)
+ end
- def test_with_store_accessors
- x = Hstore.new(language: "fr", timezone: "GMT")
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ def test_type_cast_hstore
+ assert_equal({ "1" => "2" }, @type.deserialize("\"1\"=>\"2\""))
+ assert_equal({}, @type.deserialize(""))
+ assert_equal({ "key" => nil }, @type.deserialize("key => NULL"))
+ assert_equal({ "c" => "}", '"a"' => 'b "a b' }, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b")))
+ end
- x.save!
- x = Hstore.first
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ def test_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- x.language = "de"
- x.save!
+ x.save!
+ x = Hstore.first
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- x = Hstore.first
- assert_equal "de", x.language
- assert_equal "GMT", x.timezone
- end
+ x.language = "de"
+ x.save!
- def test_duplication_with_store_accessors
- x = Hstore.new(language: "fr", timezone: "GMT")
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ x = Hstore.first
+ assert_equal "de", x.language
+ assert_equal "GMT", x.timezone
+ end
- y = x.dup
- assert_equal "fr", y.language
- assert_equal "GMT", y.timezone
- end
+ def test_duplication_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_yaml_round_trip_with_store_accessors
- x = Hstore.new(language: "fr", timezone: "GMT")
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ y = x.dup
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
- y = YAML.load(YAML.dump(x))
- assert_equal "fr", y.language
- assert_equal "GMT", y.timezone
- end
+ def test_yaml_round_trip_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_changes_in_place
- hstore = Hstore.create!(settings: { 'one' => 'two' })
- hstore.settings['three'] = 'four'
- hstore.save!
- hstore.reload
+ y = YAML.load(YAML.dump(x))
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
- assert_equal 'four', hstore.settings['three']
- assert_not hstore.changed?
- end
+ def test_changes_with_store_accessors
+ x = Hstore.new(language: "de")
+ assert x.language_changed?
+ assert_nil x.language_was
+ assert_equal [nil, "de"], x.language_change
+ x.save!
- def test_gen1
- assert_equal(%q(" "=>""), @type.serialize({' '=>''}))
- end
+ assert_not x.language_changed?
+ x.reload
- def test_gen2
- assert_equal(%q(","=>""), @type.serialize({','=>''}))
- end
+ x.settings = nil
+ assert x.language_changed?
+ assert_equal "de", x.language_was
+ assert_equal ["de", nil], x.language_change
+ end
- def test_gen3
- assert_equal(%q("="=>""), @type.serialize({'='=>''}))
- end
+ def test_changes_in_place
+ hstore = Hstore.create!(settings: { "one" => "two" })
+ hstore.settings["three"] = "four"
+ hstore.save!
+ hstore.reload
- def test_gen4
- assert_equal(%q(">"=>""), @type.serialize({'>'=>''}))
- end
+ assert_equal "four", hstore.settings["three"]
+ assert_not_predicate hstore, :changed?
+ 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_dirty_from_user_equal
+ settings = { "alongkey" => "anything", "key" => "value" }
+ hstore = Hstore.create!(settings: settings)
- def test_parse2
- assert_equal({" " => " "}, @type.deserialize("\\ =>\\ "))
- end
+ hstore.settings = { "key" => "value", "alongkey" => "anything" }
+ assert_equal settings, hstore.settings
+ assert_not_predicate hstore, :changed?
+ end
- def test_parse3
- assert_equal({"=" => ">"}, @type.deserialize("==>>"))
- end
+ def test_hstore_dirty_from_database_equal
+ settings = { "alongkey" => "anything", "key" => "value" }
+ hstore = Hstore.create!(settings: settings)
+ hstore.reload
- def test_parse4
- assert_equal({"=a"=>"q=w"}, @type.deserialize('\=a=>q=w'))
- end
+ assert_equal settings, hstore.settings
+ hstore.settings = settings
+ assert_not_predicate hstore, :changed?
+ end
- def test_parse5
- assert_equal({"=a"=>"q=w"}, @type.deserialize('"=a"=>q\=w'))
- end
+ def test_gen1
+ assert_equal('" "=>""', @type.serialize(" " => ""))
+ end
- def test_parse6
- assert_equal({"\"a"=>"q>w"}, @type.deserialize('"\"a"=>q>w'))
- end
+ def test_gen2
+ assert_equal('","=>""', @type.serialize("," => ""))
+ end
- def test_parse7
- assert_equal({"\"a"=>"q\"w"}, @type.deserialize('\"a=>q"w'))
- end
+ def test_gen3
+ assert_equal('"="=>""', @type.serialize("=" => ""))
+ 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_gen4
+ assert_equal('">"=>""', @type.serialize(">" => ""))
+ 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_parse1
+ assert_equal({ "a" => nil, "b" => nil, "c" => "NuLl", "null" => "c" }, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
+ end
- def test_array_cycle
- assert_array_cycle([{"AA" => "BB", "CC" => "DD"}, {"AA" => nil}])
- end
+ def test_parse2
+ assert_equal({ " " => " " }, @type.deserialize("\\ =>\\ "))
+ end
- def test_array_strings_with_quotes
- assert_array_cycle([{'this has' => 'some "s that need to be escaped"'}])
- end
+ def test_parse3
+ assert_equal({ "=" => ">" }, @type.deserialize("==>>"))
+ end
- def test_array_strings_with_commas
- assert_array_cycle([{'this,has' => 'many,values'}])
- end
+ def test_parse4
+ assert_equal({ "=a" => "q=w" }, @type.deserialize('\=a=>q=w'))
+ end
- def test_array_strings_with_array_delimiters
- assert_array_cycle(['{' => '}'])
- end
+ def test_parse5
+ assert_equal({ "=a" => "q=w" }, @type.deserialize('"=a"=>q\=w'))
+ end
- def test_array_strings_with_null_strings
- assert_array_cycle([{'NULL' => 'NULL'}])
- end
+ def test_parse6
+ assert_equal({ "\"a" => "q>w" }, @type.deserialize('"\"a"=>q>w'))
+ end
- def test_contains_nils
- assert_array_cycle([{'NULL' => nil}])
- end
+ def test_parse7
+ assert_equal({ "\"a" => "q\"w" }, @type.deserialize('\"a=>q"w'))
+ 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_rewrite
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.first
+ x.tags = { '"a\'' => "b" }
+ assert x.save!
+ end
- def test_create
- assert_cycle('a' => 'b', '1' => '2')
- 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_nil
- assert_cycle('a' => nil)
- end
+ def test_array_cycle
+ assert_array_cycle([{ "AA" => "BB", "CC" => "DD" }, { "AA" => nil }])
+ end
- def test_quotes
- assert_cycle('a' => 'b"ar', '1"foo' => '2')
- end
+ def test_array_strings_with_quotes
+ assert_array_cycle([{ "this has" => 'some "s that need to be escaped"' }])
+ end
- def test_whitespace
- assert_cycle('a b' => 'b ar', '1"foo' => '2')
- end
+ def test_array_strings_with_commas
+ assert_array_cycle([{ "this,has" => "many,values" }])
+ end
- def test_backslash
- assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2')
- end
+ def test_array_strings_with_array_delimiters
+ assert_array_cycle(["{" => "}"])
+ end
- def test_comma
- assert_cycle('a, b' => 'bar', '1"foo' => '2')
- end
+ def test_array_strings_with_null_strings
+ assert_array_cycle([{ "NULL" => "NULL" }])
+ end
- def test_arrow
- assert_cycle('a=>b' => 'bar', '1"foo' => '2')
- end
+ def test_contains_nils
+ assert_array_cycle([{ "NULL" => nil }])
+ end
- def test_quoting_special_characters
- assert_cycle('ca' => 'cà', 'ac' => 'àc')
- 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_multiline
- assert_cycle("a\nb" => "c\nd")
- end
+ def test_create
+ assert_cycle("a" => "b", "1" => "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_nil
+ assert_cycle("a" => nil)
+ end
- class HstoreWithSerialize < Hstore
- serialize :tags, TagCollection
- end
+ def test_quotes
+ assert_cycle("a" => 'b"ar', '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_whitespace
+ assert_cycle("a b" => "b ar", '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_backslash
+ assert_cycle('a\\b' => 'b\\ar', '1"foo' => "2")
+ end
- def test_schema_dump_with_shorthand
- output = dump_table_schema("hstores")
- assert_match %r[t\.hstore "tags",\s+default: {}], output
- end
+ def test_comma
+ assert_cycle("a, b" => "bar", '1"foo' => "2")
+ end
+
+ def test_arrow
+ assert_cycle("a=>b" => "bar", '1"foo' => "2")
+ end
+
+ def test_quoting_special_characters
+ assert_cycle("ca" => "cà", "ac" => "àc")
+ end
- private
+ 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
+
+ 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(ProtectedParams.new("hi" => "hi"))
+ end
+
+ private
def assert_array_cycle(array)
# test creation
x = Hstore.create!(payload: array)
@@ -338,16 +375,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..b1bf06d9e9 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)
@@ -54,16 +71,38 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
end
test "assigning 'infinity' on a datetime column with TZ aware attributes" do
- begin
- in_time_zone "Pacific Time (US & Canada)" do
- record = PostgresqlInfinity.create!(datetime: "infinity")
- assert_equal Float::INFINITY, record.datetime
- assert_equal record.datetime, record.reload.datetime
- end
- ensure
- # setting time_zone_aware_attributes causes the types to change.
- # There is no way to do this automatically since it can be set on a superclass
- PostgresqlInfinity.reset_column_information
+ in_time_zone "Pacific Time (US & Canada)" do
+ record = PostgresqlInfinity.create!(datetime: "infinity")
+ assert_equal Float::INFINITY, record.datetime
+ assert_equal record.datetime, record.reload.datetime
end
+ ensure
+ # setting time_zone_aware_attributes causes the types to change.
+ # There is no way to do this automatically since it can be set on a superclass
+ PostgresqlInfinity.reset_column_information
+ 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 b3b121b4fb..ee08841eb3 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -1,189 +1,37 @@
+# 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/optimizer_hints_test.rb b/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb
new file mode 100644
index 0000000000..5e4bf232e1
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+if supports_optimizer_hints?
+ class PostgresqlOptimzerHintsTest < ActiveRecord::PostgreSQLTestCase
+ fixtures :posts
+
+ def setup
+ enable_extension!("pg_hint_plan", ActiveRecord::Base.connection)
+ end
+
+ def test_optimizer_hints
+ assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
+ posts = Post.optimizer_hints("SeqScan(posts)")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "Seq Scan on posts"
+ end
+ end
+
+ def test_optimizer_hints_is_sanitized
+ assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
+ posts = Post.optimizer_hints("/*+ SeqScan(posts) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "Seq Scan on posts"
+ end
+
+ assert_sql(%r{\ASELECT /\*\+ "posts"\.\*, \*/}) do
+ posts = Post.optimizer_hints("**// \"posts\".*, //**")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_equal({ "id" => 1 }, posts.first.as_json)
+ end
+ end
+
+ def test_optimizer_hints_with_unscope
+ assert_sql(%r{\ASELECT "posts"\."id"}) do
+ posts = Post.optimizer_hints("/*+ SeqScan(posts) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ posts.unscope(:optimizer_hints).load
+ end
+ end
+ end
+end
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 8b08ebc3c4..fbd3cbf90f 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -1,6 +1,8 @@
+# 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
@@ -15,163 +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_primary_key_raises_error_if_table_not_found
- assert_raises(ActiveRecord::StatementInvalid) do
- @connection.primary_key('unobtainium')
+ with_example_table "id integer" do
+ assert_nil @connection.primary_key("ex")
end
end
def test_exec_insert_with_returning_disabled
connection = connection_without_insert_returning
- result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq')
- 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", "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_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 (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_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 DEFAULT VALUES", 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_exec_insert_default_values_quoted_schema_with_returning_disabled_and_no_sequence_name_given
connection = connection_without_insert_returning
- 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
+ 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_sql_for_insert_with_returning_disabled
- connection = connection_without_insert_returning
- sql, binds = connection.sql_for_insert('sql', nil, nil, nil, 'binds')
- assert_equal ['sql', 'binds'], [sql, binds]
- 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'"
]
@@ -185,15 +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
+ @connection.drop_table "ex", if_exists: true
+ @connection.drop_table "ex2", if_exists: true
end
def test_table_alias_length
@@ -204,86 +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
if ActiveRecord::Base.connection.prepared_statements
def test_exec_with_binds
with_example_table do
- string = @connection.quote('foo')
+ string = @connection.quote("foo")
@connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
bind = Relation::QueryAttribute.new("id", 1, Type::Value.new)
- result = @connection.exec_query('SELECT id, data FROM ex WHERE id = $1', nil, [bind])
+ 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
def test_exec_typecasts_bind_vals
with_example_table do
- string = @connection.quote('foo')
+ string = @connection.quote("foo")
@connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
bind = Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)
- result = @connection.exec_query('SELECT id, data FROM ex WHERE id = $1', nil, [bind])
+ 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_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
@@ -292,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
@@ -328,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
@@ -358,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
@@ -372,15 +376,81 @@ module ActiveRecord
end
end
- private
+ def test_errors_when_an_insert_query_is_called_while_preventing_writes
+ with_example_table do
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @connection.while_preventing_writes do
+ @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')")
+ end
+ end
+ end
+ end
- def with_example_table(definition = 'id serial primary key, number integer, data character varying(255)', &block)
- super(@connection, 'ex', definition, &block)
+ def test_errors_when_an_update_query_is_called_while_preventing_writes
+ with_example_table do
+ @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')")
+
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @connection.while_preventing_writes do
+ @connection.execute("UPDATE ex SET data = '9989' WHERE data = '138853948594'")
+ end
+ end
+ end
+ end
+
+ def test_errors_when_a_delete_query_is_called_while_preventing_writes
+ with_example_table do
+ @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')")
+
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @connection.while_preventing_writes do
+ @connection.execute("DELETE FROM ex where data = '138853948594'")
+ end
+ end
+ end
+ end
+
+ def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes
+ with_example_table do
+ @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')")
+
+ @connection.while_preventing_writes do
+ assert_equal 1, @connection.execute("SELECT * FROM ex WHERE data = '138853948594'").entries.count
+ end
+ end
end
- def connection_without_insert_returning
- ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false))
+ def test_doesnt_error_when_a_show_query_is_called_while_preventing_writes
+ @connection.while_preventing_writes do
+ assert_equal 1, @connection.execute("SHOW TIME ZONE").entries.count
+ end
+ end
+
+ def test_doesnt_error_when_a_set_query_is_called_while_preventing_writes
+ @connection.while_preventing_writes do
+ assert_equal [], @connection.execute("SET standard_conforming_strings = on").entries
+ end
+ end
+
+ def test_doesnt_error_when_a_read_query_with_leading_chars_is_called_while_preventing_writes
+ with_example_table do
+ @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')")
+
+ @connection.while_preventing_writes do
+ assert_equal 1, @connection.execute("(\n( SELECT * FROM ex WHERE data = '138853948594' ) )").entries.count
+ end
+ end
end
+
+ private
+
+ 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
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/prepared_statements_test.rb b/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb
deleted file mode 100644
index f1519db48b..0000000000
--- a/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-require "cases/helper"
-require "models/developer"
-
-class PreparedStatementsTest < ActiveRecord::PostgreSQLTestCase
- fixtures :developers
-
- def setup
- @default_prepared_statements = Developer.connection_config[:prepared_statements]
- Developer.connection_config[:prepared_statements] = false
- end
-
- def teardown
- Developer.connection_config[:prepared_statements] = @default_prepared_statements
- end
-
- def nothing_raised_with_falsy_prepared_statements
- assert_nothing_raised do
- Developer.where(id: 1)
- end
- end
-
-end
diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
index 5e6f4dbbb8..d571355a9c 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,12 @@ 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
+
+ def test_quote_table_name_with_spaces
+ value = "user posts"
+ assert_equal "\"user posts\"", @conn.quote_table_name(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 0edfa4ed9d..068f1e8bea 100644
--- a/activerecord/test/cases/adapters/postgresql/range_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/range_test.rb
@@ -1,343 +1,434 @@
+# frozen_string_literal: true
+
require "cases/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
- begin
- @connection.transaction do
- @connection.execute <<_SQL
- CREATE TYPE floatrange AS RANGE (
- subtype = float8,
- subtype_diff = float8mi
- );
-_SQL
-
- @connection.create_table('postgresql_ranges') do |t|
- t.daterange :date_range
- t.numrange :num_range
- t.tsrange :ts_range
- t.tstzrange :tstz_range
- t.int4range :int4_range
- t.int8range :int8_range
- end
-
- @connection.add_column 'postgresql_ranges', 'float_range', 'floatrange'
+require "support/connection_helper"
+
+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
+ begin
+ @connection.transaction do
+ @connection.execute <<~SQL
+ CREATE TYPE floatrange AS RANGE (
+ subtype = float8,
+ subtype_diff = float8mi
+ );
+ SQL
+
+ @connection.create_table("postgresql_ranges") do |t|
+ t.daterange :date_range
+ t.numrange :num_range
+ t.tsrange :ts_range
+ t.tstzrange :tstz_range
+ t.int4range :int4_range
+ t.int8range :int8_range
end
- PostgresqlRange.reset_column_information
- rescue ActiveRecord::StatementInvalid
- skip "do not test on PG without range"
- end
- insert_range(id: 101,
- date_range: "[''2012-01-02'', ''2012-01-04'']",
- num_range: "[0.1, 0.2]",
- ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']",
- tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']",
- int4_range: "[1, 10]",
- int8_range: "[10, 100]",
- float_range: "[0.5, 0.7]")
-
- insert_range(id: 102,
- date_range: "[''2012-01-02'', ''2012-01-04'')",
- num_range: "[0.1, 0.2)",
- ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')",
- tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')",
- int4_range: "[1, 10)",
- int8_range: "[10, 100)",
- float_range: "[0.5, 0.7)")
-
- insert_range(id: 103,
- date_range: "[''2012-01-02'',]",
- num_range: "[0.1,]",
- ts_range: "[''2010-01-01 14:30'',]",
- tstz_range: "[''2010-01-01 14:30:00+05'',]",
- int4_range: "[1,]",
- int8_range: "[10,]",
- float_range: "[0.5,]")
-
- insert_range(id: 104,
- date_range: "[,]",
- num_range: "[,]",
- ts_range: "[,]",
- tstz_range: "[,]",
- int4_range: "[,]",
- int8_range: "[,]",
- float_range: "[,]")
-
- insert_range(id: 105,
- date_range: "[''2012-01-02'', ''2012-01-02'')",
- num_range: "[0.1, 0.1)",
- ts_range: "[''2010-01-01 14:30'', ''2010-01-01 14:30'')",
- tstz_range: "[''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')",
- int4_range: "[1, 1)",
- int8_range: "[10, 10)",
- float_range: "[0.5, 0.5)")
-
- @new_range = PostgresqlRange.new
- @first_range = PostgresqlRange.find(101)
- @second_range = PostgresqlRange.find(102)
- @third_range = PostgresqlRange.find(103)
- @fourth_range = PostgresqlRange.find(104)
- @empty_range = PostgresqlRange.find(105)
+ @connection.add_column "postgresql_ranges", "float_range", "floatrange"
+ end
+ PostgresqlRange.reset_column_information
+ rescue ActiveRecord::StatementInvalid
+ skip "do not test on PG without range"
end
- teardown do
- @connection.drop_table 'postgresql_ranges', if_exists: true
- @connection.execute 'DROP TYPE IF EXISTS floatrange'
- reset_connection
- end
+ insert_range(id: 101,
+ date_range: "[''2012-01-02'', ''2012-01-04'']",
+ num_range: "[0.1, 0.2]",
+ ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']",
+ tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']",
+ int4_range: "[1, 10]",
+ int8_range: "[10, 100]",
+ float_range: "[0.5, 0.7]")
+
+ insert_range(id: 102,
+ date_range: "[''2012-01-02'', ''2012-01-04'')",
+ num_range: "[0.1, 0.2)",
+ ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')",
+ tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')",
+ int4_range: "[1, 10)",
+ int8_range: "[10, 100)",
+ float_range: "[0.5, 0.7)")
+
+ insert_range(id: 103,
+ date_range: "[''2012-01-02'',]",
+ num_range: "[0.1,]",
+ ts_range: "[''2010-01-01 14:30'',]",
+ tstz_range: "[''2010-01-01 14:30:00+05'',]",
+ int4_range: "[1,]",
+ int8_range: "[10,]",
+ float_range: "[0.5,]")
+
+ insert_range(id: 104,
+ date_range: "[,]",
+ num_range: "[,]",
+ ts_range: "[,]",
+ tstz_range: "[,]",
+ int4_range: "[,]",
+ int8_range: "[,]",
+ float_range: "[,]")
+
+ insert_range(id: 105,
+ date_range: "[''2012-01-02'', ''2012-01-02'')",
+ num_range: "[0.1, 0.1)",
+ ts_range: "[''2010-01-01 14:30'', ''2010-01-01 14:30'')",
+ tstz_range: "[''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')",
+ int4_range: "[1, 1)",
+ int8_range: "[10, 10)",
+ float_range: "[0.5, 0.5)")
+
+ @new_range = PostgresqlRange.new
+ @first_range = PostgresqlRange.find(101)
+ @second_range = PostgresqlRange.find(102)
+ @third_range = PostgresqlRange.find(103)
+ @fourth_range = PostgresqlRange.find(104)
+ @empty_range = PostgresqlRange.find(105)
+ end
- def test_data_type_of_range_types
- assert_equal :daterange, @first_range.column_for_attribute(:date_range).type
- assert_equal :numrange, @first_range.column_for_attribute(:num_range).type
- assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type
- assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type
- assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type
- assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type
- end
+ teardown do
+ @connection.drop_table "postgresql_ranges", if_exists: true
+ @connection.execute "DROP TYPE IF EXISTS floatrange"
+ reset_connection
+ end
- def test_int4range_values
- assert_equal 1...11, @first_range.int4_range
- assert_equal 1...10, @second_range.int4_range
- assert_equal 1...Float::INFINITY, @third_range.int4_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range)
- assert_nil @empty_range.int4_range
- end
+ def test_data_type_of_range_types
+ assert_equal :daterange, @first_range.column_for_attribute(:date_range).type
+ assert_equal :numrange, @first_range.column_for_attribute(:num_range).type
+ assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type
+ assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type
+ assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type
+ assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type
+ end
- def test_int8range_values
- assert_equal 10...101, @first_range.int8_range
- assert_equal 10...100, @second_range.int8_range
- assert_equal 10...Float::INFINITY, @third_range.int8_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range)
- assert_nil @empty_range.int8_range
- end
+ def test_int4range_values
+ assert_equal 1...11, @first_range.int4_range
+ assert_equal 1...10, @second_range.int4_range
+ assert_equal 1...Float::INFINITY, @third_range.int4_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range)
+ assert_nil @empty_range.int4_range
+ end
- def test_daterange_values
- assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range
- assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 4), @second_range.date_range
- assert_equal Date.new(2012, 1, 2)...Float::INFINITY, @third_range.date_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range)
- assert_nil @empty_range.date_range
- end
+ def test_int8range_values
+ assert_equal 10...101, @first_range.int8_range
+ assert_equal 10...100, @second_range.int8_range
+ assert_equal 10...Float::INFINITY, @third_range.int8_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range)
+ assert_nil @empty_range.int8_range
+ 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_nil @empty_range.num_range
- end
+ def test_daterange_values
+ assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range
+ assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 4), @second_range.date_range
+ assert_equal Date.new(2012, 1, 2)...Float::INFINITY, @third_range.date_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range)
+ assert_nil @empty_range.date_range
+ end
- def test_tsrange_values
- tz = ::ActiveRecord::Base.default_timezone
- assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range
- assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range)
- assert_nil @empty_range.ts_range
- end
+ def test_numrange_values
+ 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
- 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(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range)
- assert_nil @empty_range.tstz_range
- end
+ def test_tsrange_values
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range
+ assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range)
+ assert_nil @empty_range.ts_range
+ end
- def test_custom_range_values
- assert_equal 0.5..0.7, @first_range.float_range
- assert_equal 0.5...0.7, @second_range.float_range
- assert_equal 0.5...Float::INFINITY, @third_range.float_range
- assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range)
- assert_nil @empty_range.float_range
- 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(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range)
+ assert_nil @empty_range.tstz_range
+ end
- def test_timezone_awareness_tzrange
- tz = "Pacific Time (US & Canada)"
+ def test_custom_range_values
+ assert_equal 0.5..0.7, @first_range.float_range
+ assert_equal 0.5...0.7, @second_range.float_range
+ assert_equal 0.5...Float::INFINITY, @third_range.float_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range)
+ assert_nil @empty_range.float_range
+ end
- in_time_zone tz do
- PostgresqlRange.reset_column_information
- time_string = Time.current.to_s
- time = Time.zone.parse(time_string)
+ def test_timezone_awareness_tzrange
+ tz = "Pacific Time (US & Canada)"
- 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
+ in_time_zone tz do
+ PostgresqlRange.reset_column_information
+ time_string = Time.current.to_s
+ time = Time.zone.parse(time_string)
- record.save!
- record.reload
+ 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
- assert_equal time..time, record.tstz_range
- assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone
- end
- end
+ record.save!
+ record.reload
- def test_create_tstzrange
- 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 time..time, record.tstz_range
+ assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone
end
+ 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'))
- 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'))
- end
+ def test_create_tstzrange
+ 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")
+ end
- def test_create_tsrange
- tz = ::ActiveRecord::Base.default_timezone
- assert_equal_round_trip(@new_range, :ts_range,
- Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0))
- 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"))
+ 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"))
+ end
- def test_update_tsrange
- tz = ::ActiveRecord::Base.default_timezone
- assert_equal_round_trip(@first_range, :ts_range,
- Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0))
- assert_nil_round_trip(@first_range, :ts_range,
- Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0))
- end
+ def test_create_tsrange
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal_round_trip(@new_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0))
+ end
- def test_timezone_awareness_tsrange
- tz = "Pacific Time (US & Canada)"
+ def test_update_tsrange
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal_round_trip(@first_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0))
+ assert_nil_round_trip(@first_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0))
+ end
- in_time_zone tz do
- PostgresqlRange.reset_column_information
- time_string = Time.current.to_s
- time = Time.zone.parse(time_string)
+ def test_timezone_awareness_tsrange
+ tz = "Pacific Time (US & Canada)"
- 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
+ in_time_zone tz do
+ PostgresqlRange.reset_column_information
+ time_string = Time.current.to_s
+ time = Time.zone.parse(time_string)
- record.save!
- record.reload
+ 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..time, record.ts_range
- assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone
- end
- end
+ record.save!
+ record.reload
- def test_create_numrange
- assert_equal_round_trip(@new_range, :num_range,
- BigDecimal.new('0.5')...BigDecimal.new('1'))
+ assert_equal time..time, record.ts_range
+ assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone
end
+ end
- def test_update_numrange
- assert_equal_round_trip(@first_range, :num_range,
- BigDecimal.new('0.5')...BigDecimal.new('1'))
- assert_nil_round_trip(@first_range, :num_range,
- BigDecimal.new('0.5')...BigDecimal.new('0.5'))
- 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_create_daterange
- assert_equal_round_trip(@new_range, :date_range,
- Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true))
- 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_update_daterange
- assert_equal_round_trip(@first_range, :date_range,
- Date.new(2012, 2, 3)...Date.new(2012, 2, 10))
- assert_nil_round_trip(@first_range, :date_range,
- Date.new(2012, 2, 3)...Date.new(2012, 2, 3))
- 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_create_int4range
- assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true))
- 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_update_int4range
- assert_equal_round_trip(@first_range, :int4_range, 6...10)
- assert_nil_round_trip(@first_range, :int4_range, 3...3)
- end
+ def test_timezone_awareness_tsrange_preserve_usec
+ tz = "Pacific Time (US & Canada)"
- def test_create_int8range
- assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true))
- end
+ 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
- def test_update_int8range
- assert_equal_round_trip(@first_range, :int8_range, 60000...10000000)
- assert_nil_round_trip(@first_range, :int8_range, 39999...39999)
- end
+ 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
- def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported
- assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") }
- assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") }
- assert_raises(ArgumentError) { PostgresqlRange.create!(int4_range: "(1, 10]") }
- assert_raises(ArgumentError) { PostgresqlRange.create!(int8_range: "(10, 100]") }
- assert_raises(ArgumentError) { PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']") }
- assert_raises(ArgumentError) { PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']") }
- assert_raises(ArgumentError) { PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") }
+ 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_update_all_with_ranges
- PostgresqlRange.create!
+ def test_create_numrange
+ assert_equal_round_trip(@new_range, :num_range,
+ BigDecimal("0.5")...BigDecimal("1"))
+ end
- PostgresqlRange.update_all(int8_range: 1..100)
+ def test_update_numrange
+ assert_equal_round_trip(@first_range, :num_range,
+ BigDecimal("0.5")...BigDecimal("1"))
+ assert_nil_round_trip(@first_range, :num_range,
+ BigDecimal("0.5")...BigDecimal("0.5"))
+ end
- assert_equal 1...101, PostgresqlRange.first.int8_range
- end
+ def test_create_daterange
+ assert_equal_round_trip(@new_range, :date_range,
+ Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true))
+ end
- def test_ranges_correctly_escape_input
- range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a"
- PostgresqlRange.update_all(int8_range: range)
+ def test_update_daterange
+ assert_equal_round_trip(@first_range, :date_range,
+ Date.new(2012, 2, 3)...Date.new(2012, 2, 10))
+ assert_nil_round_trip(@first_range, :date_range,
+ Date.new(2012, 2, 3)...Date.new(2012, 2, 3))
+ end
- assert_nothing_raised do
- PostgresqlRange.first
- end
+ def test_create_int4range
+ assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true))
+ end
+
+ def test_update_int4range
+ assert_equal_round_trip(@first_range, :int4_range, 6...10)
+ assert_nil_round_trip(@first_range, :int4_range, 3...3)
+ end
+
+ def test_create_int8range
+ assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true))
+ end
+
+ def test_update_int8range
+ assert_equal_round_trip(@first_range, :int8_range, 60000...10000000)
+ assert_nil_round_trip(@first_range, :int8_range, 39999...39999)
+ end
+
+ def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported
+ assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") }
+ assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") }
+ assert_raises(ArgumentError) { PostgresqlRange.create!(int4_range: "(1, 10]") }
+ assert_raises(ArgumentError) { PostgresqlRange.create!(int8_range: "(10, 100]") }
+ assert_raises(ArgumentError) { PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']") }
+ assert_raises(ArgumentError) { PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']") }
+ 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!
+
+ PostgresqlRange.update_all(int8_range: 1..100)
+
+ assert_equal 1...101, PostgresqlRange.first.int8_range
+ end
+
+ def test_ranges_correctly_escape_input
+ range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a"
+ PostgresqlRange.update_all(int8_range: range)
+
+ assert_nothing_raised do
+ PostgresqlRange.first
end
+ end
- private
- def assert_equal_round_trip(range, attribute, value)
- round_trip(range, attribute, value)
- assert_equal value, range.public_send(attribute)
- end
+ def test_infinity_values
+ PostgresqlRange.create!(int4_range: 1..Float::INFINITY,
+ int8_range: -Float::INFINITY..0,
+ float_range: -Float::INFINITY..Float::INFINITY)
- def assert_nil_round_trip(range, attribute, value)
- round_trip(range, attribute, value)
- assert_nil range.public_send(attribute)
- end
+ record = PostgresqlRange.first
- def round_trip(range, attribute, value)
- range.public_send "#{attribute}=", value
- assert range.save
- assert range.reload
- end
+ 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
- def insert_range(values)
- @connection.execute <<-SQL
- INSERT INTO postgresql_ranges (
- id,
- date_range,
- num_range,
- ts_range,
- tstz_range,
- int4_range,
- int8_range,
- float_range
- ) VALUES (
- #{values[:id]},
- '#{values[:date_range]}',
- '#{values[:num_range]}',
- '#{values[:ts_range]}',
- '#{values[:tstz_range]}',
- '#{values[:int4_range]}',
- '#{values[:int8_range]}',
- '#{values[:float_range]}'
- )
- SQL
- end
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0")
+ def test_endless_range_values
+ record = PostgresqlRange.create!(
+ int4_range: eval("1.."),
+ int8_range: eval("10.."),
+ float_range: eval("0.5..")
+ )
+
+ record = PostgresqlRange.find(record.id)
+
+ assert_equal 1...Float::INFINITY, record.int4_range
+ assert_equal 10...Float::INFINITY, record.int8_range
+ assert_equal 0.5...Float::INFINITY, record.float_range
+ end
end
+
+ private
+ def assert_equal_round_trip(range, attribute, value)
+ round_trip(range, attribute, value)
+ assert_equal value, range.public_send(attribute)
+ end
+
+ def assert_nil_round_trip(range, attribute, value)
+ round_trip(range, attribute, value)
+ assert_nil range.public_send(attribute)
+ end
+
+ def round_trip(range, attribute, value)
+ range.public_send "#{attribute}=", value
+ assert range.save
+ assert range.reload
+ end
+
+ def insert_range(values)
+ @connection.execute <<~SQL
+ INSERT INTO postgresql_ranges (
+ id,
+ date_range,
+ num_range,
+ ts_range,
+ tstz_range,
+ int4_range,
+ int8_range,
+ float_range
+ ) VALUES (
+ #{values[:id]},
+ '#{values[:date_range]}',
+ '#{values[:num_range]}',
+ '#{values[:ts_range]}',
+ '#{values[:tstz_range]}',
+ '#{values[:int4_range]}',
+ '#{values[:int8_range]}',
+ '#{values[:float_range]}'
+ )
+ SQL
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
index 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..7eccaf4aa2 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 285a92f60e..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
@@ -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
@@ -68,31 +70,20 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase
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)])
+ 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
- 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")
- set_session_auth
- end
- end
- end
-
def test_sequence_schema_caching
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
@@ -100,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 00ebabc9c5..336cec30ca 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -1,6 +1,8 @@
+# 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)
@@ -17,32 +19,32 @@ 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"
@@ -61,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
@@ -91,6 +93,7 @@ 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
@@ -105,23 +108,19 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
end
def test_create_schema
- begin
- @connection.create_schema "test_schema3"
- assert @connection.schema_names.include? "test_schema3"
- ensure
- @connection.drop_schema "test_schema3"
- end
+ @connection.create_schema "test_schema3"
+ assert @connection.schema_names.include? "test_schema3"
+ ensure
+ @connection.drop_schema "test_schema3"
end
def test_raise_create_schema_with_existing_schema
- begin
+ @connection.create_schema "test_schema3"
+ assert_raises(ActiveRecord::StatementInvalid) do
@connection.create_schema "test_schema3"
- assert_raises(ActiveRecord::StatementInvalid) do
- @connection.create_schema "test_schema3"
- end
- ensure
- @connection.drop_schema "test_schema3"
end
+ ensure
+ @connection.drop_schema "test_schema3"
end
def test_drop_schema
@@ -130,7 +129,7 @@ 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
@@ -143,7 +142,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
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
+ ActiveRecord::Base.connection.execute <<~SQL
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);
@@ -168,21 +167,21 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
def test_raise_wrapped_exception_on_bad_prepare
assert_raises(ActiveRecord::StatementInvalid) do
- @connection.exec_query "select * from developers where id = ?", 'sql', [bind_param(1)]
+ @connection.exec_query "select * from developers where id = ?", "sql", [bind_param(1)]
end
end
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', []
+ @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)]
+ @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
+ @connection.exec_query("alter table developers drop column zomg", "sql", []) if altered
end
end
@@ -200,13 +199,13 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
end
def test_data_source_exists_when_not_on_schema_search_path
- with_schema_search_path('PUBLIC') do
- assert(!@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found")
+ 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_data_source_exists_wrong_schema
- assert(!@connection.data_source_exists?("foo.things"), "data_source should not exist")
+ assert_not(@connection.data_source_exists?("foo.things"), "data_source should not exist")
end
def test_data_source_exists_quoted_names
@@ -246,9 +245,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"')
@@ -260,25 +259,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
@@ -287,7 +286,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
@@ -301,13 +300,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
@@ -325,14 +324,14 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
def test_dump_indexes_for_table_with_scheme_specified_in_name
indexes = @connection.indexes("#{SCHEMA_NAME}.#{TABLE_NAME}")
- assert_equal 4, indexes.size
+ assert_equal 5, indexes.size
end
def test_with_uppercase_index_name
@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", name: "things_Index"}
+ assert_nothing_raised { @connection.remove_index "things", name: "things_Index" }
end
end
@@ -356,21 +355,13 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
%(#{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
@@ -381,19 +372,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
@@ -401,7 +392,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
@@ -414,11 +405,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
@@ -442,25 +433,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 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
@@ -501,6 +496,78 @@ class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase
end
end
+class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "trains" do |t|
+ t.string :name
+ t.string :position
+ 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
+
+ def test_opclass_class_parsing_on_non_reserved_and_cannot_be_function_or_type_keyword
+ @connection.enable_extension("pg_trgm")
+ @connection.execute "CREATE INDEX trains_position ON trains USING gin(position gin_trgm_ops)"
+ @connection.execute "CREATE INDEX trains_name_and_position ON trains USING btree(name, position text_pattern_ops)"
+
+ output = dump_table_schema "trains"
+
+ assert_match(/opclass: :gin_trgm_ops/, output)
+ assert_match(/opclass: \{ position: :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
@@ -535,7 +602,7 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCa
end
def test_decimal_defaults_in_new_schema_when_overriding_domain
- assert_equal BigDecimal.new("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed"
+ assert_equal BigDecimal("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed"
end
def test_bpchar_defaults_in_new_schema_when_overriding_domain
diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb
index 8abe064bf1..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
@@ -22,14 +24,14 @@ class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlSerial.columns_hash["seq"]
assert_equal :integer, column.type
assert_equal "integer", column.sql_type
- assert column.serial?
+ assert_predicate column, :serial?
end
def test_not_serial_column
column = PostgresqlSerial.columns_hash["serials_id"]
assert_equal :integer, column.type
assert_equal "integer", column.sql_type
- assert_not column.serial?
+ assert_not_predicate column, :serial?
end
def test_schema_dump_with_shorthand
@@ -64,14 +66,14 @@ class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlBigSerial.columns_hash["seq"]
assert_equal :integer, column.type
assert_equal "bigint", column.sql_type
- assert column.serial?
+ assert_predicate column, :serial?
end
def test_not_bigserial_column
column = PostgresqlBigSerial.columns_hash["serials_id"]
assert_equal :integer, column.type
assert_equal "bigint", column.sql_type
- assert_not column.serial?
+ assert_not_predicate column, :serial?
end
def test_schema_dump_with_shorthand
@@ -84,3 +86,71 @@ class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase
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..919ff3d158
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
@@ -0,0 +1,189 @@
+# 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
+ 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 7628075ad2..d2d8ea8042 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,10 +30,11 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
end
setup do
- enable_extension!('uuid-ossp', connection)
+ 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
@@ -31,72 +42,124 @@ 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_invalid_uuid_dont_match_to_nil
+ UUIDType.create!
+ assert_empty UUIDType.where(guid: "")
+ assert_empty UUIDType.where(guid: "foobar")
+ end
+
+ class DuckUUID
+ def initialize(uuid)
+ @uuid = uuid
+ end
+
+ def to_s
+ @uuid
+ end
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}",
+ # Support Object-Oriented UUIDs which respond to #to_s
+ DuckUUID.new("A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"),
].each do |valid_uuid|
uuid = UUIDType.new guid: valid_uuid
- assert_not_nil uuid.guid
+ assert_instance_of String, 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
@@ -133,7 +196,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
@@ -142,71 +205,102 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
class UUID < ActiveRecord::Base
- self.table_name = 'pg_uuids'
+ self.table_name = "pg_uuids"
end
setup do
- 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() $$
- LANGUAGE SQL VOLATILE;
+ connection.execute <<~SQL
+ CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid
+ 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();'
+ 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)
+ 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_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
@@ -214,9 +308,9 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
setup do
- 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
@@ -224,19 +318,36 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase
drop_table "pg_uuids"
end
- if ActiveRecord::Base.connection.supports_extensions?
- def test_id_allows_default_override_via_nil
- col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
- FROM pg_attribute a
- LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
- WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first
- assert_nil col_desc["default"]
- end
+ def test_id_allows_default_override_via_nil
+ col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
+ FROM pg_attribute a
+ LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
+ WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first
+ assert_nil col_desc["default"]
+ end
- def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil
- schema = dump_table_schema "pg_uuids"
- assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema)
- end
+ def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil
+ schema = dump_table_schema "pg_uuids"
+ assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema)
+ end
+
+ def test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+
+ migration = Class.new(ActiveRecord::Migration[5.0]) do
+ def version; 101 end
+ def migrate(x)
+ create_table("pg_uuids_4", id: :uuid, default: nil)
+ end
+ end.new
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ schema = dump_table_schema "pg_uuids_4"
+ assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema)
+ ensure
+ drop_table "pg_uuids_4"
+ ActiveRecord::Migration.verbose = @verbose_was
end
end
@@ -244,51 +355,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
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"
+ 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/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/annotate_test.rb b/activerecord/test/cases/adapters/sqlite3/annotate_test.rb
new file mode 100644
index 0000000000..6567a5eca3
--- /dev/null
+++ b/activerecord/test/cases/adapters/sqlite3/annotate_test.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+class SQLite3AnnotateTest < ActiveRecord::SQLite3TestCase
+ fixtures :posts
+
+ def test_annotate_wraps_content_in_an_inline_comment
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
+ posts = Post.select(:id).annotate("foo")
+ assert posts.first
+ end
+ end
+
+ def test_annotate_is_sanitized
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
+ posts = Post.select(:id).annotate("*/foo/*")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
+ posts = Post.select(:id).annotate("**//foo//**")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/ /\* bar \*/}) do
+ posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* \+ MAX_EXECUTION_TIME\(1\) \*/}) do
+ posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)")
+ assert posts.first
+ end
+ end
+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 a1a6e5f16a..b6d2ccdb53 100644
--- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
@@ -1,21 +1,23 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/developer'
-require 'models/computer'
+require "models/author"
+require "models/post"
class SQLite3ExplainTest < ActiveRecord::SQLite3TestCase
- fixtures :developers
+ fixtures :authors
def test_explain_for_one_query
- explain = Developer.where(id: 1).explain
- assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\?|1)), explain
- assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
+ explain = Author.where(id: 1).explain
+ assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\? \[\["id", 1\]\]|1)), explain
+ assert_match(/(SEARCH )?TABLE authors USING (INTEGER )?PRIMARY KEY/, explain)
end
def test_explain_with_eager_loading
- explain = Developer.where(id: 1).includes(:audit_logs).explain
- assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\?|1)), explain
- assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
- assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain
- assert_match(/(SCAN )?TABLE audit_logs/, explain)
+ 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..9d26f32102 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -1,94 +1,78 @@
+# 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
+ super
+ @conn = ActiveRecord::Base.connection
+ 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
+ assert_equal 1, @conn.type_cast(true)
+ end
+
+ def test_type_cast_false
+ 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 bbc9f978bf..806cfbfc00 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 ', '
- result = Owner.connection.exec_query <<-esql
+ select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ", "
+ result = Owner.connection.exec_query <<~SQL
SELECT #{select}
FROM #{Owner.table_name}
WHERE #{Owner.primary_key} = #{owner.id}
- esql
+ SQL
- 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,103 +80,103 @@ 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
+ 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
def test_quote_binary_column_escapes_it
- DualEncoding.connection.execute(<<-eosql)
+ DualEncoding.connection.execute(<<~SQL)
CREATE TABLE IF NOT EXISTS dual_encodings (
id integer PRIMARY KEY AUTOINCREMENT,
name varchar(255),
data binary
)
- eosql
- str = "\x80".force_encoding("ASCII-8BIT")
- binary = DualEncoding.new name: 'ã„ãŸã ãã¾ã™ï¼', data: str
+ SQL
+ 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
@@ -204,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
@@ -226,7 +212,7 @@ module ActiveRecord
def test_insert_id_value_returned
with_example_table do
sql = "INSERT INTO ex (number) VALUES (10)"
- idval = 'vuvuzela'
+ idval = "vuvuzela"
id = @conn.insert(sql, nil, nil, idval)
assert_equal idval, id
end
@@ -237,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
@@ -254,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)"
@@ -267,50 +253,36 @@ module ActiveRecord
def test_tables
with_example_table do
- ActiveSupport::Deprecation.silence { assert_equal %w{ ex }, @conn.tables }
- with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer', 'people' do
- ActiveSupport::Deprecation.silence { assert_equal %w{ ex people }.sort, @conn.tables.sort }
+ assert_equal %w{ ex }, @conn.tables
+ with_example_table "id integer PRIMARY KEY AUTOINCREMENT, number integer", "people" do
+ assert_equal %w{ ex people }.sort, @conn.tables.sort
end
end
end
def test_tables_logs_name
- sql = <<-SQL
- SELECT name FROM sqlite_master
- WHERE type IN ('table','view') AND name <> 'sqlite_sequence'
+ sql = <<~SQL
+ SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND type IN ('table')
SQL
- assert_logged [[sql.squish, 'SCHEMA', []]] do
- ActiveSupport::Deprecation.silence do
- @conn.tables('hello')
- end
- 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 IN ('table','view') AND name <> 'sqlite_sequence' AND name = 'ex'
+ sql = <<~SQL
+ SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND name = 'ex' AND type IN ('table')
SQL
- assert_logged [[sql.squish, 'SCHEMA', []]] do
- ActiveSupport::Deprecation.silence do
- assert @conn.table_exists?('ex')
- end
+ assert_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)
@@ -319,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
@@ -337,80 +309,226 @@ 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
+ 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.stub(:step, ->{ raise ::SQLite3::BusyException.new('busy') }) do
+ "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'
+ @conn.exec_query "select * from statement_test"
end
end
end
@@ -418,24 +536,123 @@ module ActiveRecord
end
end
- private
+ def test_db_is_not_readonly_when_readonly_option_is_false
+ conn = Base.sqlite3_connection database: ":memory:",
+ adapter: "sqlite3",
+ readonly: false
- 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)
+ 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
+
+ def test_errors_when_an_insert_query_is_called_while_preventing_writes
+ with_example_table "id int, data string" do
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @conn.while_preventing_writes do
+ @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')")
+ end
+ end
+ end
+ end
+
+ def test_errors_when_an_update_query_is_called_while_preventing_writes
+ with_example_table "id int, data string" do
+ @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')")
+
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @conn.while_preventing_writes do
+ @conn.execute("UPDATE ex SET data = '9989' WHERE data = '138853948594'")
+ end
+ end
+ end
end
+
+ def test_errors_when_a_delete_query_is_called_while_preventing_writes
+ with_example_table "id int, data string" do
+ @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')")
+
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @conn.while_preventing_writes do
+ @conn.execute("DELETE FROM ex where data = '138853948594'")
+ end
+ end
+ end
+ end
+
+ def test_errors_when_a_replace_query_is_called_while_preventing_writes
+ with_example_table "id int, data string" do
+ @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')")
+
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @conn.while_preventing_writes do
+ @conn.execute("REPLACE INTO ex (data) VALUES ('249823948')")
+ end
+ end
+ end
+ end
+
+ def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes
+ with_example_table "id int, data string" do
+ @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')")
+
+ @conn.while_preventing_writes do
+ assert_equal 1, @conn.execute("SELECT data from ex WHERE data = '138853948594'").count
+ end
+ end
+ end
+
+ def test_doesnt_error_when_a_read_query_with_leading_chars_is_called_while_preventing_writes
+ with_example_table "id int, data string" do
+ @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')")
+
+ @conn.while_preventing_writes do
+ assert_equal 1, @conn.execute(" SELECT data from ex WHERE data = '138853948594'").count
+ end
+ 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 9b675b804b..cfc9853aba 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
@@ -1,22 +1,22 @@
+# 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|
- begin
- dir = Pathname.new(dir)
- @conn = Base.sqlite3_connection :database => dir.join("db/foo.sqlite3"),
- :adapter => 'sqlite3',
- :timeout => 100
+ 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')
- ensure
- @conn.disconnect! if @conn
- end
+ assert Dir.exist? dir.join("db")
+ assert File.exist? dir.join("db/foo.sqlite3")
+ ensure
+ @conn.disconnect! if @conn
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 559b951109..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(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..d270175af4 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(FrozenError) { 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 9c99689c1e..9d88b14dab 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -1,110 +1,176 @@
+# frozen_string_literal: true
+
require "cases/helper"
-if ActiveRecord::Base.connection.supports_migrations?
+class ActiveRecordSchemaTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ setup do
+ @original_verbose = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+ @connection = ActiveRecord::Base.connection
+ ActiveRecord::SchemaMigration.drop_table
+ end
+
+ 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
- class ActiveRecordSchemaTest < ActiveRecord::TestCase
- self.use_transactional_tests = false
+ 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
- setup do
- @original_verbose = ActiveRecord::Migration.verbose
- ActiveRecord::Migration.verbose = false
- @connection = ActiveRecord::Base.connection
- ActiveRecord::SchemaMigration.drop_table
+ 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
- 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
+ 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
- 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
+ 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
- ActiveRecord::SchemaMigration.create_table
- assert_difference "ActiveRecord::SchemaMigration.count", 1 do
- ActiveRecord::SchemaMigration.create version: 12
+ 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
- ensure
- ActiveRecord::SchemaMigration.drop_table
- ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type
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
- 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
+ end
+ end
- assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
- assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" }
- assert_equal 7, ActiveRecord::Migrator::current_version
+ 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_schema_define_w_table_name_prefix
- table_name = ActiveRecord::SchemaMigration.table_name
- old_table_name_prefix = ActiveRecord::Base.table_name_prefix
- ActiveRecord::Base.table_name_prefix = "nep_"
- ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}"
- ActiveRecord::Schema.define(:version => 7) do
- create_table :fruits do |t|
- t.column :color, :string
- t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
- t.column :texture, :string
- t.column :flavor, :string
- end
+ def test_normalize_version
+ assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118")
+ assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2")
+ assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017")
+ assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947")
+ end
+
+ def test_schema_load_with_multiple_indexes_for_column_of_different_names
+ ActiveRecord::Schema.define do
+ create_table :multiple_indexes do |t|
+ t.string "foo"
+ t.index ["foo"], name: "multiple_indexes_foo_1"
+ t.index ["foo"], name: "multiple_indexes_foo_2"
end
- assert_equal 7, ActiveRecord::Migrator::current_version
- ensure
- ActiveRecord::Base.table_name_prefix = old_table_name_prefix
- ActiveRecord::SchemaMigration.table_name = table_name
end
- def test_schema_raises_an_error_for_invalid_column_type
- assert_raise NoMethodError do
- ActiveRecord::Schema.define(:version => 8) do
- create_table :vegetables do |t|
- t.unknown :color
- end
- end
+ indexes = @connection.indexes("multiple_indexes")
+
+ assert_equal 2, indexes.length
+ assert_equal ["multiple_indexes_foo_1", "multiple_indexes_foo_2"], indexes.collect(&:name).sort
+ end
+
+ 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 @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
+ end
+
+ def test_timestamps_without_null_set_null_to_false_on_change_table
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps do |t|
+ t.timestamps default: Time.now
end
end
- def test_schema_subclass
- Class.new(ActiveRecord::Schema).define(:version => 9) do
- create_table :fruits
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_without_null_set_null_to_false_on_change_table_with_bulk
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps, bulk: true do |t|
+ t.timestamps default: Time.now
+ end
end
- assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
end
+ 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")
+ 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
- def test_timestamps_without_null_set_null_to_false_on_create_table
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
+ end
+
+ if subsecond_precision_supported?
+ def test_timestamps_sets_presicion_on_create_table
ActiveRecord::Schema.define do
create_table :has_timestamps do |t|
t.timestamps
end
end
- assert !@connection.columns(:has_timestamps).find { |c| c.name == 'created_at' }.null
- assert !@connection.columns(:has_timestamps).find { |c| c.name == 'updated_at' }.null
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
end
- def test_timestamps_without_null_set_null_to_false_on_change_table
+ def test_timestamps_sets_presicion_on_change_table
ActiveRecord::Schema.define do
create_table :has_timestamps
@@ -113,18 +179,33 @@ if ActiveRecord::Base.connection.supports_migrations?
end
end
- assert !@connection.columns(:has_timestamps).find { |c| c.name == 'created_at' }.null
- assert !@connection.columns(:has_timestamps).find { |c| c.name == 'updated_at' }.null
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_sets_presicion_on_change_table_with_bulk
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps, bulk: true do |t|
+ t.timestamps default: Time.now
+ end
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
end
- def test_timestamps_without_null_set_null_to_false_on_add_timestamps
+ def test_timestamps_sets_presicion_on_add_timestamps
ActiveRecord::Schema.define do
create_table :has_timestamps
add_timestamps :has_timestamps, default: Time.now
end
- assert !@connection.columns(:has_timestamps).find { |c| c.name == 'created_at' }.null
- assert !@connection.columns(:has_timestamps).find { |c| c.name == 'updated_at' }.null
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
end
end
end
diff --git a/activerecord/test/cases/arel/attributes/attribute_test.rb b/activerecord/test/cases/arel/attributes/attribute_test.rb
new file mode 100644
index 0000000000..c7bd0a053b
--- /dev/null
+++ b/activerecord/test/cases/arel/attributes/attribute_test.rb
@@ -0,0 +1,1080 @@
+# 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 "#between" 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
+
+ if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION)
+ it "can be constructed with a range implicitly ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(eval("0..")) # Use eval for compatibility with Ruby < 2.6 parser
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+ 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
+ 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 "#not_between" 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 a quoted range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(quoted_range(-::Float::INFINITY, 3, false))
+
+ node.must_equal Nodes::GreaterThan.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.not_between(-::Float::INFINITY...3)
+
+ node.must_equal Nodes::GreaterThanOrEqual.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.not_between(quoted_range(-::Float::INFINITY, 3, true))
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Quoted.new(3)
+ )
+ 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 quoted infinite range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(quoted_range(-::Float::INFINITY, ::Float::INFINITY, false))
+
+ 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
+
+ if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION)
+ it "can be constructed with a range implicitly ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(eval("0..")) # Use eval for compatibility with Ruby < 2.6 parser
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+ end
+
+ it "can be constructed with a quoted range ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(quoted_range(0, ::Float::INFINITY, false))
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Quoted.new(0)
+ )
+ 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
+
+ private
+ 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
+ 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..79b85742ee
--- /dev/null
+++ b/activerecord/test/cases/arel/insert_manager_test.rb
@@ -0,0 +1,241 @@
+# 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 ValuesList node" do
+ manager = Arel::InsertManager.new
+ values = manager.create_values_list([%w{ a b }, %w{ c d }])
+
+ assert_kind_of Arel::Nodes::ValuesList, values
+ assert_equal [%w{ a b }, %w{ c d }], values.rows
+ end
+
+ it "allows sql literals" do
+ manager = Arel::InsertManager.new
+ manager.into Table.new(:users)
+ manager.values = manager.create_values([Arel.sql("*")])
+ 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::ValuesList.new([[1], [2]])
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" VALUES (1), (2)
+ }
+ 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::ValuesList.new([[1, "aaron"], [2, "david"]])
+ manager.columns << table[:id]
+ manager.columns << table[:name]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") VALUES (1, 'aaron'), (2, 'david')
+ }
+ 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..d123ca9fd0
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/and_test.rb
@@ -0,0 +1,30 @@
+# 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
+
+ describe "functions as node expression" do
+ it "allows aliasing" do
+ aliased = And.new(["foo", "bar"]).as("baz")
+
+ assert_kind_of As, aliased
+ assert_kind_of SqlLiteral, aliased.right
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/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..946c2b0453
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/case_test.rb
@@ -0,0 +1,96 @@
+# 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
+
+ describe "#as" do
+ it "allows aliasing" do
+ node = Case.new "foo"
+ as = node.as("bar")
+
+ assert_equal node, as.left
+ assert_kind_of Arel::Nodes::SqlLiteral, as.right
+ 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/comment_test.rb b/activerecord/test/cases/arel/nodes/comment_test.rb
new file mode 100644
index 0000000000..bf5eaf4c5a
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/comment_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "yaml"
+
+module Arel
+ module Nodes
+ class CommentTest < Arel::Spec
+ describe "equality" do
+ it "is equal with equal contents" do
+ array = [Comment.new(["foo"]), Comment.new(["foo"])]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different contents" do
+ array = [Comment.new(["foo"]), Comment.new(["bar"])]
+ assert_equal 2, array.uniq.size
+ 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..6860f2a395
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/select_core_test.rb
@@ -0,0 +1,79 @@
+# 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]
+ core1.comment = Arel::Nodes::Comment.new(["comment"])
+ 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]
+ core2.comment = Arel::Nodes::Comment.new(["comment"])
+ 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]
+ core1.comment = Arel::Nodes::Comment.new(["comment"])
+ 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]
+ core2.comment = Arel::Nodes::Comment.new(["comment"])
+ array = [core1, core2]
+ assert_equal 2, array.uniq.size
+ core2.havings = %w[p q r]
+ core2.comment = Arel::Nodes::Comment.new(["other"])
+ 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..e6c49cd429
--- /dev/null
+++ b/activerecord/test/cases/arel/select_manager_test.rb
@@ -0,0 +1,1248 @@
+# 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
+
+ describe "comment" do
+ it "chains" do
+ manager = Arel::SelectManager.new
+ manager.comment("selecting").must_equal manager
+ end
+
+ it "appends a comment to the generated query" do
+ manager = Arel::SelectManager.new
+ table = Table.new :users
+ manager.from(table).project(table["id"])
+
+ manager.comment("selecting")
+ manager.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" /* selecting */
+ }
+
+ manager.comment("selecting", "with", "comment")
+ manager.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" /* selecting */ /* with */ /* comment */
+ }
+ 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..18e6c10c9d
--- /dev/null
+++ b/activerecord/test/cases/arel/support/fake_record.rb
@@ -0,0 +1,133 @@
+# 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 sanitize_as_sql_comment(comment)
+ comment
+ 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..106be2311d
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/depth_first_test.rb
@@ -0,0 +1,276 @@
+# 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::ValuesList,
+ 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
+
+ def test_comment
+ comment = Nodes::Comment.new ["foo"]
+ @visitor.accept comment
+ assert_equal ["foo", ["foo"], comment], @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::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..ade53c358e
--- /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::ValuesList,
+ 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::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..625e37f1c0
--- /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 ValuesList" do
+ bp = Nodes::BindParam.new(1)
+ values = Nodes::ValuesList.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 a3046d526e..3525fa2ab8 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -1,28 +1,30 @@
-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'
+# 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,
@@ -30,9 +32,27 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
:posts, :tags, :taggings, :comments, :sponsors, :members
def test_belongs_to
- firm = Client.find(3).firm
- assert_not_nil firm
- assert_equal companies(:first_firm).name, firm.name
+ client = Client.find(3)
+ first_firm = companies(:first_firm)
+ assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do
+ assert_equal first_firm, client.firm
+ assert_equal first_firm.name, client.firm.name
+ end
+ end
+
+ def test_assigning_belongs_to_on_destroyed_object
+ client = Client.create!(name: "Client")
+ client.destroy!
+ assert_raise(FrozenError) { client.firm = nil }
+ assert_raise(FrozenError) { 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
@@ -43,11 +63,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
ActiveRecord::SQLCounter.clear_log
Client.find(3).firm
ensure
- assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, 'ORDER BY was used in the query'
+ sql_log = ActiveRecord::SQLCounter.log
+ assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}"
end
def test_belongs_to_with_primary_key
- 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
@@ -77,7 +98,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
@@ -93,8 +114,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
account = model.new
- assert_not 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
@@ -110,31 +131,69 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
account = model.new
- assert_not 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
@@ -166,9 +225,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")
@@ -177,6 +236,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
@@ -203,15 +264,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
@@ -223,8 +284,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
@@ -247,48 +317,82 @@ 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
@@ -301,7 +405,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
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
client.association(:firm_with_primary_key).reload
@@ -325,19 +429,18 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
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)
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
@@ -345,17 +448,22 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_with_select
- assert_equal 1, Company.find(2).firm_with_select.attributes.size
- assert_equal 1, Company.all.merge!(:includes => :firm_with_select ).find(2).firm_with_select.attributes.size
+ assert_equal 1, Post.find(2).author_with_select.attributes.size
+ assert_equal 1, Post.includes(:author_with_select).find(2).author_with_select.attributes.size
+ end
+
+ def test_custom_attribute_with_select
+ assert_equal 2, Company.find(2).firm_with_select.attributes.size
+ assert_equal 2, Company.includes(:firm_with_select).find(2).firm_with_select.attributes.size
end
def test_belongs_to_without_counter_cache_option
# Ship has a conventionally named `treasures_count` column, but the counter_cache
# option is not given on the association.
- ship = Ship.create(name: 'Countless')
+ 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 = Treasure.new(name: "Gold", ship: ship)
treasure.save
end
@@ -377,31 +485,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 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.size
- comment.post = nil
+ reply.topic = nil
+ reply.save!
- assert_equal 1, Post.find(post.id).comments.size
+ 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 topic.id, reply.parent_id
+ assert_equal 1, topic.reload.replies_count
+
+ topic2 = reply.build_topic(title: "debate2")
+ reply.save!
+
+ 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
@@ -428,11 +575,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
@@ -461,14 +610,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])
@@ -527,7 +727,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
@@ -564,8 +764,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")
@@ -576,10 +776,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
final_cut.firm = firm
- assert !final_cut.persisted?
+ assert_not_predicate final_cut, :persisted?
assert final_cut.save
- assert final_cut.persisted?
- assert firm.persisted?
+ assert_predicate final_cut, :persisted?
+ assert_predicate firm, :persisted?
assert_equal firm, final_cut.firm
final_cut.association(:firm).reload
assert_equal firm, final_cut.firm
@@ -589,10 +789,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
final_cut.firm_with_primary_key = firm
- assert !final_cut.persisted?
+ assert_not_predicate final_cut, :persisted?
assert final_cut.save
- assert final_cut.persisted?
- assert firm.persisted?
+ assert_predicate final_cut, :persisted?
+ assert_predicate firm, :persisted?
assert_equal firm, final_cut.firm_with_primary_key
final_cut.association(:firm_with_primary_key).reload
assert_equal firm, final_cut.firm_with_primary_key
@@ -600,8 +800,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
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
@@ -623,7 +822,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
@@ -632,11 +837,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
@@ -646,10 +852,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]
@@ -666,10 +872,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]
@@ -687,11 +893,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
@@ -700,10 +907,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
@@ -723,7 +941,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_cant_save_readonly_association
assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_client).readonly_firm.save! }
- assert companies(:first_client).readonly_firm.readonly?
+ assert_predicate companies(:first_client).readonly_firm, :readonly?
end
def test_polymorphic_assignment_foreign_key_type_string
@@ -756,7 +974,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
@@ -781,7 +999,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
@@ -793,7 +1011,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
@@ -809,7 +1027,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
@@ -831,14 +1049,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
@@ -859,18 +1077,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
@@ -882,15 +1124,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
@@ -901,12 +1143,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
@@ -916,12 +1158,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
@@ -943,9 +1185,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
@@ -991,46 +1244,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
@@ -1045,9 +1298,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(Author) do
+ def self.name; "Temp"; end
+ validates :author_address, presence: true
+ end
+
+ author = model.new
+ author.author_address_id = 9223372036854775808 # out of range in the bigint
+
+ assert_nil author.author_address
+ assert_not_predicate author, :valid?
+ assert_equal [{ error: :blank }], author.errors.details[:author_address]
+ 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
@@ -1061,12 +1328,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
@@ -1075,8 +1342,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
@@ -1085,16 +1352,21 @@ 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_association_force_reload_with_only_true_is_deprecated
- client = Client.find(3)
+ def test_multiple_counter_cache_with_after_create_update
+ post = posts(:welcome)
+ parent = comments(:greetings)
- assert_deprecated { client.firm(true) }
+ 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
diff --git a/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb
index 2b867965ba..88221b012e 100644
--- a/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb
+++ b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb
@@ -1,5 +1,7 @@
-require 'cases/helper'
-require 'models/content'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/content"
class BidirectionalDestroyDependenciesTest < ActiveRecord::TestCase
fixtures :content, :content_positions
diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb
index a4298a25a6..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.unchangeable_posts.include?(@authorless)
+ assert_not_includes @david.unchangeable_posts, @authorless
begin
@david.unchangeable_posts << @authorless
rescue Exception
end
- assert @david.post_log.empty?
- assert !@david.unchangeable_posts.include?(@authorless)
+ assert_empty @david.post_log
+ assert_not_includes @david.unchangeable_posts, @authorless
@david.reload
- assert !@david.unchangeable_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..49f754be63 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.includes(posts: :comments).order(:id).to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
- 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.includes({ posts: :comments }, :categorizations).order(:id).to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
- 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 }).order(:id).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,26 +117,25 @@ 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
- assert_no_queries do
+ topics = Topic.all.merge!(includes: :replies, order: ["topics.id", "replies_topics.id"]).to_a
+ assert_queries(0) do
assert_equal 2, topics[0].replies.size
assert_equal 0, topics[1].replies.size
end
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..673d5f1dcf 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 = post.create_tagging!
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 7f2a2229ee..cd9c8a5285 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -1,75 +1,173 @@
+# 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/mentor'
-require 'models/contract'
+require "models/post"
+require "models/tagging"
+require "models/tag"
+require "models/rating"
+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,
- :companies, :accounts, :tags, :taggings, :people, :readers, :categorizations,
+ :companies, :accounts, :tags, :taggings, :ratings, :people, :readers, :categorizations,
:owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books,
: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_polymorphic_association_with_mixed_table_conditions
+ rating = Rating.first
+ assert_equal [taggings(:normal_comment_rating)], rating.taggings_without_tag
+
+ rating = Rating.preload(:taggings_without_tag).first
+ assert_equal [taggings(:normal_comment_rating)], rating.taggings_without_tag
+
+ rating = Rating.eager_load(:taggings_without_tag).first
+ assert_equal [taggings(:normal_comment_rating)], rating.taggings_without_tag
+ 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|
@@ -90,49 +188,53 @@ 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
assert_called(Comment.connection, :in_clause_length, returns: 5) do
- posts = Post.all.merge!(:includes=>:comments).to_a
+ 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
assert_called(Comment.connection, :in_clause_length, returns: nil) do
- posts = Post.all.merge!(:includes=>:comments).to_a
+ 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
assert_called(Comment.connection, :in_clause_length, times: 2, returns: 5) do
- posts = Post.all.merge!(:includes=>:categories).to_a
+ 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
assert_called(Comment.connection, :in_clause_length, times: 2, returns: nil) do
- posts = Post.all.merge!(:includes=>:categories).to_a
+ posts = Post.all.merge!(includes: :categories).to_a
assert_equal 11, posts.size
end
end
@@ -141,7 +243,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
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
+ Post.includes(:comments).where(id: post.id).to_a
end
end
end
@@ -150,7 +252,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
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
+ Post.includes(:comments).where(id: [post1.id, post2.id]).to_a
end
end
end
@@ -159,50 +261,50 @@ class EagerAssociationTest < ActiveRecord::TestCase
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
+ 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!
@@ -213,7 +315,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
@@ -224,7 +326,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
@@ -234,53 +336,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
@@ -288,117 +387,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
@@ -406,61 +505,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
@@ -471,32 +570,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 }
@@ -504,35 +603,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
@@ -540,7 +647,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
@@ -551,67 +658,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
@@ -621,13 +728,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
@@ -687,32 +794,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
@@ -721,8 +828,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
@@ -735,38 +842,45 @@ 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!")
+ 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)
+ Tagging.create!(taggable_type: "Post", taggable_id: post1.id, tag: tag)
+ Tagging.create!(taggable_type: "Post", taggable_id: post2.id, tag: tag)
tag_with_includes = OrderedTag.includes(:tagged_posts).find(tag.id)
- assert_equal(tag_with_includes.taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title))
+ 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')
+ 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!")
+ 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)
@@ -782,7 +896,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
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)
@@ -790,7 +904,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)
@@ -807,7 +921,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)
@@ -815,7 +929,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)
@@ -824,8 +938,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)
@@ -833,30 +947,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
@@ -865,15 +975,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
@@ -882,25 +992,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
@@ -929,7 +1039,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
@@ -939,28 +1053,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
@@ -1002,13 +1122,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
@@ -1016,7 +1136,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
@@ -1038,61 +1158,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
@@ -1103,39 +1217,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
@@ -1157,12 +1271,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_include_has_many_using_primary_key
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)
- else
- firm = Firm.all.merge!(:includes => :clients_using_primary_key, :order => 'clients_using_primary_keys_companies.name').find(1)
- end
+ firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies.name").find(1)
assert_no_queries do
assert_equal expected, firm.clients_using_primary_key
end
@@ -1170,7 +1279,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
@@ -1178,35 +1287,37 @@ 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
@@ -1234,31 +1345,31 @@ class EagerAssociationTest < ActiveRecord::TestCase
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 do
- Post.includes(:comments).order("").where(:comments => {:body => "Thank you for the welcome"}).first
+ 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 }
@@ -1274,20 +1385,25 @@ class EagerAssociationTest < ActiveRecord::TestCase
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
@@ -1312,10 +1428,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
@@ -1341,6 +1457,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
@@ -1355,7 +1473,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 {
@@ -1389,7 +1507,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
exception = assert_raises(ArgumentError) do
Author.preload(10).to_a
end
- assert_equal('10 was not recognized for preload', exception.message)
+ assert_equal("10 was not recognized for preload", exception.message)
end
test "associations with extensions are not instance dependent" do
@@ -1408,48 +1526,107 @@ class EagerAssociationTest < ActiveRecord::TestCase
test "preloading readonly association" do
# has-one
firm = Firm.where(id: "1").preload(:readonly_account).first!
- assert firm.readonly_account.readonly?
+ assert_predicate firm.readonly_account, :readonly?
# has_and_belongs_to_many
project = Project.where(id: "2").preload(:readonly_developers).first!
- assert project.readonly_developers.first.readonly?
+ assert_predicate project.readonly_developers.first, :readonly?
# has-many :through
david = Author.where(id: "1").preload(:readonly_comments).first!
- assert david.readonly_comments.first.readonly?
+ assert_predicate david.readonly_comments.first, :readonly?
+ end
+
+ test "eager-loading non-readonly association" do
+ # has_one
+ firm = Firm.where(id: "1").eager_load(:account).first!
+ assert_not_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
- ActiveRecord::Associations::HasManyAssociation.any_instance.expects(:reader).never
- Author.preload(:readonly_comments).first!
+ 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 1bbca84bb2..fe8bdd03ba 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,84 +1,89 @@
+# frozen_string_literal: true
+
require "cases/helper"
-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/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
@@ -88,7 +93,7 @@ end
class DeveloperWithExtendOption < Developer
module NamedExtension
def category
- 'sns'
+ "sns"
end
end
@@ -96,27 +101,42 @@ class DeveloperWithExtendOption < Developer
end
class ProjectUnscopingDavidDefaultScope < ActiveRecord::Base
- self.table_name = 'projects'
- has_and_belongs_to_many :developers, -> { unscope(where: 'name') },
+ 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
@@ -130,45 +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 = 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"
warning = capture(:stderr) do
country.treaties << treaty
end
- assert_no_match(/WARNING: Rails does not support composite primary key\./, warning)
+ 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
@@ -244,12 +264,12 @@ 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.reload.size
end
@@ -257,7 +277,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
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]
@@ -282,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
@@ -337,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
@@ -379,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
@@ -437,10 +451,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_removing_associations_on_destroy
david = DeveloperWithBeforeDestroyRaise.find(1)
- assert !david.projects.empty?
+ assert_not_empty david.projects
david.destroy
- assert david.projects.empty?
- assert DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1").empty?
+ assert_empty david.projects
+ assert_empty DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1")
end
def test_destroying
@@ -455,7 +469,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id} AND project_id = #{project.id}")
- assert join_records.empty?
+ assert_empty join_records
assert_equal 1, david.reload.projects.size
assert_equal 1, david.projects.reload.size
@@ -471,7 +485,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}")
- assert join_records.empty?
+ assert_empty join_records
assert_equal 0, david.reload.projects.size
assert_equal 0, david.projects.reload.size
@@ -480,23 +494,23 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_destroy_all
david = Developer.find(1)
david.projects.reload
- assert !david.projects.empty?
+ assert_not_empty david.projects
assert_no_difference "Project.count" do
david.projects.destroy_all
end
join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}")
- assert join_records.empty?
+ assert_empty join_records
- assert david.projects.empty?
- assert david.projects.reload.empty?
+ assert_empty david.projects
+ assert_empty david.projects.reload
end
def test_destroy_associations_destroys_multiple_associations
george = parrots(:george)
- assert !george.pirates.empty?
- assert !george.treasures.empty?
+ assert_not_empty george.pirates
+ assert_not_empty george.treasures
assert_no_difference "Pirate.count" do
assert_no_difference "Treasure.count" do
@@ -505,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.reload.empty?
+ assert_empty join_records
+ assert_empty george.pirates.reload
join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}")
- assert join_records.empty?
- assert george.treasures.reload.empty?
+ assert_empty join_records
+ assert_empty george.treasures.reload
end
def test_associations_with_conditions
@@ -542,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
@@ -553,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
@@ -576,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
@@ -612,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
@@ -628,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
@@ -646,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
@@ -658,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
@@ -689,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}"
@@ -719,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
@@ -739,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
@@ -751,7 +751,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_loaded_associations
developer = developers(:david)
developer.projects.reload
- assert_queries(0) do
+ assert_no_queries do
developer.project_ids
developer.project_ids
end
@@ -759,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
@@ -775,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")
@@ -791,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!
@@ -809,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
@@ -841,12 +851,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_attributes_are_being_set_when_initialized_from_habtm_association_with_where_clause
- new_developer = projects(:action_controller).developers.where(:name => "Marcelo").build
+ 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_habtm_association_with_multiple_where_clauses
- new_developer = projects(:action_controller).developers.where(:name => "Marcelo").where(:salary => 90_000).build
+ 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
@@ -854,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
@@ -869,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
@@ -885,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
@@ -898,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
@@ -926,29 +936,24 @@ 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 do
- DeveloperWithSymbolClassName.new
+ developer = DeveloperWithSymbolClassName.new
+ developer.projects
end
end
- def test_association_force_reload_with_only_true_is_deprecated
- developer = Developer.find(1)
-
- assert_deprecated { developer.projects(true) }
- end
-
def test_alternate_database
professor = Professor.create(name: "Plum")
course = Course.create(name: "Forensics")
@@ -995,4 +1000,25 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
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
+ 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
+
+ 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 e975f4fbdd..6a7efe2121 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -1,96 +1,109 @@
+# 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/treasure'
-require 'models/parrot'
-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/job"
+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
@@ -100,8 +113,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
@@ -116,14 +129,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)
@@ -135,20 +148,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
@@ -159,15 +172,15 @@ 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
travel(1.second) do
@@ -181,38 +194,38 @@ 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')
+ car = Car.create(name: "honda")
- bulb = car.bulbs.build(name: 'exotic')
- assert_equal 'exotic', bulb.name
+ 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
@@ -225,36 +238,44 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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_nothing_raised { car.reload.funky_bulbs.delete_all }
- assert_equal 0, Bulb.count, "bulbs should have been deleted using :delete_all strategy"
+ assert_equal 1, car.funky_bulbs.count
+ assert_equal 1, car.reload.funky_bulbs.delete_all
+ 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 }
@@ -263,17 +284,25 @@ 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 }
assert_equal(expected_sql, loaded_sql)
end
+ def test_delete_all_on_association_clears_scope
+ author = Author.create!(name: "Gannon")
+ posts = author.posts
+ posts.create!(title: "test", body: "body")
+ posts.delete_all
+ assert_nil posts.first
+ end
+
def test_building_the_associated_object_with_implicit_sti_base_class
firm = DependentFirm.new
company = firm.companies.build
@@ -282,29 +311,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
@@ -313,24 +342,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
@@ -340,22 +369,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
@@ -373,64 +423,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()
- bulbs.third_to_last({})
end
assert_no_queries do
bulbs.second_to_last()
- 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
@@ -438,25 +530,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
@@ -466,11 +569,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
@@ -480,13 +583,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
@@ -506,16 +609,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
@@ -534,46 +649,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
@@ -582,7 +692,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 }
@@ -601,9 +711,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
@@ -614,7 +738,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
@@ -622,67 +746,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
@@ -692,8 +892,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
@@ -706,7 +906,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
@@ -715,19 +915,28 @@ 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
@@ -738,7 +947,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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
@@ -746,7 +955,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
@@ -755,7 +964,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
@@ -763,7 +972,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
@@ -784,26 +993,33 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_adding_a_collection
force_signal37_to_load_all_clients_of_firm
- companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
+
+ assert_predicate companies(:first_firm).clients_of_firm, :loaded?
+
+ result = companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
assert_equal 4, companies(:first_firm).clients_of_firm.size
assert_equal 4, companies(:first_firm).clients_of_firm.reload.size
+ assert_equal companies(:first_firm).clients_of_firm, result
end
def test_transactions_when_adding_to_persisted
- 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.reload.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
@@ -817,21 +1033,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
@@ -840,13 +1064,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
@@ -863,24 +1108,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
@@ -889,18 +1140,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
@@ -911,15 +1170,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
@@ -927,25 +1184,31 @@ 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.reload.last
end
def test_create_many
- companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}])
+ 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.reload.size
@@ -962,15 +1225,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_has_many_without_counter_cache_option
# Ship has a conventionally named `treasures_count` column, but the counter_cache
# option is not given on the association.
- ship = Ship.create(name: 'Countless', treasures_count: 10)
+ ship = Ship.create!(name: "Countless", treasures_count: 10)
- assert_not Ship.reflect_on_association(:treasures).has_cached_counter?
+ assert_not_predicate Ship.reflect_on_association(:treasures), :has_cached_counter?
# Count should come from sql count() of treasures rather than treasures_count attribute
assert_equal ship.treasures.size, 0
assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do
- ship.treasures.create(name: 'Gold')
+ ship.treasures.create(name: "Gold")
end
assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do
@@ -1017,6 +1280,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!
@@ -1054,8 +1349,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
@@ -1067,20 +1362,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
@@ -1089,17 +1384,20 @@ 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]])
@@ -1109,17 +1407,23 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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
assert_difference "Client.count", -(clients.count) do
- companies(:first_firm).dependent_clients_of_firm.delete_all
+ assert_equal clients.count, companies(:first_firm).dependent_clients_of_firm.delete_all
end
end
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
@@ -1129,8 +1433,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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]
@@ -1143,8 +1447,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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)
@@ -1171,7 +1478,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
@@ -1180,7 +1487,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
@@ -1205,10 +1512,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_delete_all_with_option_delete_all
firm = companies(:first_firm)
client_id = firm.dependent_clients_of_firm.first.id
- firm.dependent_clients_of_firm.delete_all(:delete_all)
+ count = firm.dependent_clients_of_firm.count
+ assert_equal count, firm.dependent_clients_of_firm.delete_all(:delete_all)
assert_nil Client.find_by_id(client_id)
end
+ def test_delete_all_with_option_nullify
+ firm = companies(:first_firm)
+ client_id = firm.dependent_clients_of_firm.first.id
+ count = firm.dependent_clients_of_firm.count
+ assert_equal firm, Client.find(client_id).firm
+ assert_equal count, firm.dependent_clients_of_firm.delete_all(:nullify)
+ assert_nil Client.find(client_id).firm
+ end
+
def test_delete_all_accepts_limited_parameters
firm = companies(:first_firm)
assert_raise(ArgumentError) do
@@ -1238,8 +1555,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
@@ -1250,8 +1567,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
@@ -1262,11 +1579,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
@@ -1288,13 +1605,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
@@ -1308,17 +1625,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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.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
@@ -1328,8 +1648,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
@@ -1344,6 +1664,8 @@ 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
@@ -1352,9 +1674,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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
@@ -1366,6 +1690,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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
@@ -1376,6 +1702,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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
@@ -1389,8 +1718,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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"
@@ -1398,21 +1730,52 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert companies(:first_firm).clients_of_firm.reload.empty?, "37signals has no clients after destroy all and refresh"
end
+ def test_destroy_all_on_association_clears_scope
+ author = Author.create!(name: "Gannon")
+ posts = author.posts
+ posts.create!(title: "test", body: "body")
+ posts.destroy_all
+ assert_nil posts.first
+ end
+
+ def test_destroy_all_on_desynced_counter_cache_association
+ category = categories(:general)
+ assert_operator category.categorizations.count, :>, 0
+
+ category.categorizations.destroy_all
+ assert_equal 0, category.categorizations.count
+ end
+
+ def test_destroy_on_association_clears_scope
+ author = Author.create!(name: "Gannon")
+ posts = author.posts
+ post = posts.create!(title: "test", body: "body")
+ posts.destroy(post)
+ assert_nil posts.first
+ end
+
+ def test_delete_on_association_clears_scope
+ author = Author.create!(name: "Gannon")
+ posts = author.posts
+ post = posts.create!(title: "test", body: "body")
+ posts.delete(post)
+ assert_nil posts.first
+ 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
@@ -1439,7 +1802,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
@@ -1462,66 +1825,62 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal num_accounts, Account.count
end
- def test_restrict_with_exception
- firm = RestrictedWithExceptionFirm.create!(:name => 'restrict')
- firm.companies.create(:name => 'child')
-
- assert !firm.companies.empty?
- assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
- assert RestrictedWithExceptionFirm.exists?(:name => 'restrict')
- assert firm.companies.exists?(:name => 'child')
- end
-
- def test_restrict_with_error_is_deprecated_using_key_many
- I18n.backend = I18n::Backend::Simple.new
- I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { many: 'message for deprecated key' } } } }
+ def test_depends_and_nullify_on_polymorphic_assoc
+ author = PersonWithPolymorphicDependentNullifyComments.create!(first_name: "Laertis")
+ comment = posts(:welcome).comments.first
+ comment.author = author
+ comment.save!
- firm = RestrictedWithErrorFirm.create!(name: 'restrict')
- firm.companies.create(name: 'child')
+ assert_equal comment.author_id, author.id
+ assert_equal comment.author_type, author.class.name
- assert !firm.companies.empty?
+ author.destroy
+ comment.reload
- assert_deprecated { firm.destroy }
+ assert_nil comment.author_id
+ assert_nil comment.author_type
+ end
- assert !firm.errors.empty?
+ def test_restrict_with_exception
+ firm = RestrictedWithExceptionFirm.create!(name: "restrict")
+ firm.companies.create(name: "child")
- assert_equal 'message for deprecated key', firm.errors[:base].first
- assert RestrictedWithErrorFirm.exists?(name: 'restrict')
- assert firm.companies.exists?(name: 'child')
- ensure
- I18n.backend.reload!
+ assert_not_empty firm.companies
+ assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
+ 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')
+ 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 !firm.companies.empty?
+ assert_not_empty firm.companies
firm.destroy
- assert !firm.errors.empty?
+ assert_not_empty firm.errors
assert_equal "Cannot delete record because dependent client companies exist", firm.errors[:base].first
- assert RestrictedWithErrorFirm.exists?(name: 'restrict')
- assert firm.companies.exists?(name: 'child')
+ assert RestrictedWithErrorFirm.exists?(name: "restrict")
+ assert firm.companies.exists?(name: "child")
ensure
I18n.backend.reload!
end
@@ -1531,10 +1890,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
@@ -1542,7 +1901,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
@@ -1556,7 +1915,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
@@ -1569,8 +1928,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
@@ -1585,16 +1944,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]
@@ -1607,8 +1966,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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
@@ -1620,7 +1982,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_loaded_associations
company = companies(:first_firm)
company.clients.reload
- assert_queries(0) do
+ assert_no_queries do
company.client_ids
company.client_ids
end
@@ -1628,9 +1990,29 @@ 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 0, car.engines.size
+ end
+
+ def test_ids_reader_cache_not_used_for_size_when_association_is_dirty
+ firm = Firm.create!(name: "Startup")
+ assert_equal 0, firm.client_ids.size
+ firm.clients.build
+ assert_equal 1, firm.clients.size
+ end
+
+ def test_ids_reader_cache_should_be_cleared_when_collection_is_deleted
+ firm = companies(:first_firm)
+ assert_equal [2, 3, 11], firm.client_ids
+ client = firm.clients.first
+ firm.clients.delete(client)
+ assert_equal [3, 11], firm.client_ids
end
def test_get_ids_ignores_include_option
@@ -1642,11 +2024,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
@@ -1657,7 +2039,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
@@ -1669,8 +2051,8 @@ 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.reload.size
@@ -1685,19 +2067,28 @@ 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
+ original = authors(:david)
+ expected = [12, 10, 9, 8, 7, 6, 5, 3, 2, 1]
+ assert_equal expected, original.comments_desc.map(&:id)
+ preloaded = Author.includes(:comments_desc).find(original.id)
+ assert_equal expected, preloaded.comments_desc.map(&:id)
+ assert_equal original.posts_sorted_by_id.first.comments.map(&:id), preloaded.posts_sorted_by_id.first.comments.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
- assert_equal authors(:david).hello_posts, authors(:david).hello_posts_with_hash_conditions
- assert_equal authors(:david).hello_post_comments, authors(:david).hello_post_comments_with_hash_conditions
+ assert_equal authors(:david).hello_posts.sort_by(&:id), authors(:david).hello_posts_with_hash_conditions.sort_by(&:id)
+ assert_equal authors(:david).hello_post_comments.sort_by(&:id), authors(:david).hello_post_comments_with_hash_conditions.sort_by(&:id)
end
def test_include_uses_array_include_after_loaded
@@ -1707,7 +2098,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
@@ -1717,18 +2108,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
@@ -1737,15 +2128,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
@@ -1755,8 +2146,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
@@ -1764,13 +2155,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
@@ -1778,7 +2169,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
@@ -1793,15 +2184,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
@@ -1809,37 +2200,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
@@ -1848,33 +2240,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
@@ -1883,39 +2276,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
@@ -1923,13 +2317,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
@@ -1937,9 +2331,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
@@ -1948,43 +2343,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
@@ -1998,7 +2395,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!
@@ -2025,7 +2422,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!
@@ -2035,13 +2432,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
@@ -2051,10 +2448,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
@@ -2066,30 +2470,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
@@ -2100,7 +2504,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
@@ -2114,18 +2518,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
@@ -2134,7 +2551,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
@@ -2145,9 +2562,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
@@ -2156,7 +2573,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
@@ -2168,14 +2585,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
@@ -2188,8 +2613,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
@@ -2205,7 +2630,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
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.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
@@ -2215,7 +2640,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
test "can rewhere the default scope of the associated model" do
- Car.has_many :old_bulbs, -> { rewhere(name: 'old') }, class_name: "Bulb"
+ 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
@@ -2224,12 +2649,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [bulb2], car.old_bulbs
end
- test 'unscopes the default scope of associated model when used with include' do
+ 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
@@ -2244,7 +2670,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
@@ -2252,8 +2678,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
@@ -2262,16 +2688,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))
@@ -2281,7 +2707,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [], authors(:david).posts_with_signature.map(&:title)
end
- test 'associations autosaves when object is already persisted' do
+ test "associations autosaves when object is already persisted" do
bulb = Bulb.create!
tyre = Tyre.create!
@@ -2294,7 +2720,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])
@@ -2305,7 +2731,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])
@@ -2316,7 +2742,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
@@ -2337,7 +2763,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])
@@ -2347,7 +2773,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])
@@ -2358,16 +2793,99 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [first_bulb, second_bulb], car.bulbs
end
- test 'double insertion of new object to association when same association used in the after create callback of a new object' do
+ 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!
- car.bulbs << TrickyBulb.new
- assert_equal 1, car.bulbs.size
+ 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
- def test_association_force_reload_with_only_true_is_deprecated
- company = Company.find(1)
+ 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_deprecated { company.clients_of_firm(true) }
+ 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
@@ -2401,10 +2919,62 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_ids_reader_memoization
- car = Car.create!(name: 'TofaÅŸ')
+ 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
+
+ def test_has_many_with_out_of_range_value
+ reference = Reference.create!(id: 2147483648) # out of range in the integer
+ assert_equal [], reference.ideal_jobs
+ 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 bb8c9fa19c..fa540c9dab 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,26 @@ 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_has_many_through_create_record
+ assert books(:awdr).subscribers.create!(nick: "bob")
+ end
+
+ def test_marshal_dump
+ preloaded = Post.includes(:first_blue_tags).first
+ assert_equal preloaded, Marshal.load(Marshal.dump(preloaded))
+ end
+
+ def test_preload_with_nested_association
+ posts = Post.preload(:author, :author_favorites_with_scope).to_a
+
+ assert_no_queries do
+ posts.each(&:author)
+ posts.each(&:author_favorites_with_scope)
+ end
end
def test_preload_sti_rhs_class
@@ -47,9 +72,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 +84,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 +113,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 +134,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 +143,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 +167,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 +177,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 +189,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,19 +201,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
assert_queries(1) do
- assert post.people.include?(person)
+ assert_includes post.people, person
end
- assert post.reload.people.reload.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
- person.reload.jobs_with_dependent_destroy.delete_all
+ assert_no_difference "Job.count" do
+ assert_difference "Reference.count", -1 do
+ assert_equal 1, person.reload.jobs_with_dependent_destroy.delete_all
end
end
end
@@ -206,9 +222,9 @@ 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
- person.reload.jobs_with_dependent_nullify.delete_all
+ assert_no_difference "Job.count" do
+ assert_no_difference "Reference.count" do
+ assert_equal 1, person.reload.jobs_with_dependent_nullify.delete_all
end
end
end
@@ -217,26 +233,35 @@ 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
- person.reload.jobs_with_dependent_delete_all.delete_all
+ assert_no_difference "Job.count" do
+ assert_difference "Reference.count", -1 do
+ assert_equal 1, person.reload.jobs_with_dependent_delete_all.delete_all
end
end
end
+ def test_delete_all_on_association_clears_scope
+ post = Post.create!(title: "Rails 6", body: "")
+ people = post.people
+ people.create!(first_name: "Jeb")
+ people.delete_all
+ assert_nil people.first
+ end
+
def test_concat
person = people(:david)
post = posts(:thinking)
- post.people.concat [person]
+ result = post.people.concat [person]
assert_equal 1, post.people.size
assert_equal 1, post.people.reload.size
+ assert_equal post.people, result
end
def test_associate_existing_record_twice_should_add_to_target_twice
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 +271,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 +284,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 +310,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.reload.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.reload.collect(&:first_name).include?("Bob")
- assert posts(:thinking).reload.people.reload.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 +388,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.reload.empty?
+ assert_empty posts(:welcome).reload.people.reload
end
def test_destroy_association
@@ -366,8 +408,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
- assert posts(:welcome).reload.people.empty?
- assert posts(:welcome).people.reload.empty?
+ assert_empty posts(:welcome).reload.people
+ assert_empty posts(:welcome).people.reload
end
def test_destroy_all
@@ -377,8 +419,32 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
- assert posts(:welcome).reload.people.empty?
- assert posts(:welcome).people.reload.empty?
+ assert_empty posts(:welcome).reload.people
+ assert_empty posts(:welcome).people.reload
+ end
+
+ def test_destroy_all_on_association_clears_scope
+ post = Post.create!(title: "Rails 6", body: "")
+ people = post.people
+ people.create!(first_name: "Jeb")
+ people.destroy_all
+ assert_nil people.first
+ end
+
+ def test_destroy_on_association_clears_scope
+ post = Post.create!(title: "Rails 6", body: "")
+ people = post.people
+ person = people.create!(first_name: "Jeb")
+ people.destroy(person)
+ assert_nil people.first
+ end
+
+ def test_delete_on_association_clears_scope
+ post = Post.create!(title: "Rails 6", body: "")
+ people = post.people
+ person = people.create!(first_name: "Jeb")
+ people.delete(person)
+ assert_nil people.first
end
def test_should_raise_exception_for_destroying_mismatching_records
@@ -392,15 +458,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 +480,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 +501,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 +519,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 +532,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 +544,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 +586,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 +597,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.reload}
+ 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 +623,45 @@ 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_includes posts(:welcome).reload.people.reload, people(:david)
+ assert_not_includes posts(:welcome).reload.people.reload, people(:michael)
+ end
+
+ def test_replace_association_with_duplicates
+ post = posts(:thinking)
+ person = people(:david)
- assert posts(:welcome).reload.people.reload.include?(people(:david))
- assert !posts(:welcome).reload.people.reload.include?(people(:michael))
+ assert_difference "post.people.count", 2 do
+ post.people = [person]
+ post.people = [person, person]
+ end
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 +670,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.reload.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 +689,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.reload }
+ 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.reload.empty?
+ assert_empty posts(:welcome).reload.people.reload
end
def test_association_callback_ordering
@@ -692,7 +778,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 +786,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 +808,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,7 +841,7 @@ 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))
+ 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
@@ -752,7 +850,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_loaded_associations
person = people(:michael)
person.posts.reload
- assert_queries(0) do
+ assert_no_queries do
person.post_ids
person.post_ids
end
@@ -760,9 +858,9 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_unloaded_associations_does_not_load_them
person = people(:michael)
- assert !person.posts.loaded?
+ assert_not_predicate person.posts, :loaded?
assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort
- assert !person.posts.loaded?
+ assert_not_predicate person.posts, :loaded?
end
def test_association_proxy_transaction_method_starts_transaction_in_association_class
@@ -774,16 +872,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
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
@@ -807,37 +905,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.reload.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.reload.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
@@ -849,10 +947,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.reload.empty?
+ assert_not Categorization.exists?(author_id: author.id, named_category_name: category.name)
+ assert_empty author.named_categories.reload
end
def test_collection_singular_ids_getter_with_string_primary_keys
@@ -869,6 +967,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [dev], company.developers
end
+ def test_collection_singular_ids_setter_with_required_type_cast
+ company = companies(:rails_core)
+ dev = Developer.first
+
+ company.developer_ids = [dev.id.to_s]
+ assert_equal [dev], company.developers
+ end
+
def test_collection_singular_ids_setter_with_string_primary_keys
assert_nothing_raised do
book = books(:awdr)
@@ -878,26 +984,35 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
book.subscriber_ids = []
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::AssociationTypeMismatch) {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
@@ -906,19 +1021,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
@@ -927,10 +1042,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
@@ -942,7 +1064,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
@@ -954,30 +1076,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
@@ -1000,12 +1122,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
@@ -1020,15 +1142,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
@@ -1037,29 +1159,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
@@ -1068,41 +1190,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
@@ -1134,9 +1294,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
@@ -1146,8 +1306,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
@@ -1168,12 +1328,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_nil Club.new.special_favourites.distinct_value
end
- def test_association_force_reload_with_only_true_is_deprecated
- post = Post.find(1)
-
- assert_deprecated { post.people(true) }
- end
-
def test_has_many_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes
member = Member.create!
club = Club.create!
@@ -1201,4 +1355,159 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
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
+
+ def test_child_is_visible_to_join_model_in_add_association_callbacks
+ [:before_add, :after_add].each do |callback_name|
+ sentient_treasure = Class.new(Treasure) do
+ def self.name; "SentientTreasure"; end
+
+ has_many :pet_treasures, foreign_key: :treasure_id, callback_name => :check_pet!
+ has_many :pets, through: :pet_treasures
+
+ def check_pet!(added)
+ raise "No pet!" if added.pet.nil?
+ end
+ end
+
+ treasure = sentient_treasure.new
+ assert_nothing_raised { treasure.pets << pets(:mochi) }
+ end
+ 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 c9d9e29f09..7bb629466d 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -1,48 +1,58 @@
+# 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"
+require "models/drink_designer"
+require "models/chef"
+require "models/department"
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
end
def test_has_one
- assert_equal companies(:first_firm).account, Account.find(1)
- assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit
+ firm = companies(:first_firm)
+ first_account = Account.find(1)
+ assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do
+ assert_equal first_account, firm.account
+ assert_equal first_account.credit_limit, firm.account.credit_limit
+ end
end
def test_has_one_does_not_use_order_by
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'
+ sql_log = ActiveRecord::SQLCounter.log
+ assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}"
end
def test_has_one_cache_nils
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,17 +112,32 @@ 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_nullify_on_polymorphic_association
+ department = Department.create!
+ designer = DrinkDesignerWithPolymorphicDependentNullifyChef.create!
+ chef = department.chefs.create!(employable: designer)
+
+ assert_equal chef.employable_id, designer.id
+ assert_equal chef.employable_type, designer.class.name
+
+ designer.destroy!
+ chef.reload
+
+ assert_nil chef.employable_id
+ assert_nil chef.employable_type
+ end
+
def test_nullification_on_destroyed_association
developer = Developer.create!(name: "Someone")
ship = Ship.create!(name: "Planet Caravan", developer: developer)
ship.destroy
- assert !ship.persisted?
- assert !developer.persisted?
+ assert_not_predicate ship, :persisted?
+ assert_not_predicate developer, :persisted?
end
def test_natural_assignment_to_nil_after_destroy
@@ -125,12 +150,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
@@ -170,69 +195,50 @@ 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)
-
- assert_not_nil firm.account
-
- assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
- assert RestrictedWithExceptionFirm.exists?(:name => 'restrict')
- assert firm.account.present?
- end
-
- def test_restrict_with_error_is_deprecated_using_key_one
- I18n.backend = I18n::Backend::Simple.new
- I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { one: 'message for deprecated key' } } } }
-
- firm = RestrictedWithErrorFirm.create!(name: 'restrict')
+ firm = RestrictedWithExceptionFirm.create!(name: "restrict")
firm.create_account(credit_limit: 10)
assert_not_nil firm.account
- assert_deprecated { firm.destroy }
-
- assert !firm.errors.empty?
- assert_equal 'message for deprecated key', firm.errors[:base].first
- assert RestrictedWithErrorFirm.exists?(name: 'restrict')
- assert firm.account.present?
- ensure
- I18n.backend.reload!
+ assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
+ 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')
+ 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 !firm.errors.empty?
+ assert_not_empty firm.errors
assert_equal "Cannot delete record because a dependent firm account exists", firm.errors[:base].first
- assert RestrictedWithErrorFirm.exists?(name: 'restrict')
- assert firm.account.present?
+ assert RestrictedWithErrorFirm.exists?(name: "restrict")
+ assert_predicate firm.account, :present?
ensure
I18n.backend.reload!
end
@@ -247,9 +253,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
@@ -260,24 +270,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
@@ -295,19 +305,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
@@ -319,13 +329,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
@@ -365,7 +417,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
@@ -374,7 +426,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_assignment_before_child_saved
firm = Firm.find(1)
firm.account = a = Account.new("credit_limit" => 1000)
- assert a.persisted?
+ assert_predicate a, :persisted?
assert_equal a, firm.account
assert_equal a, firm.account
firm.association(:account).reload
@@ -382,7 +434,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
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
@@ -392,7 +444,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
@@ -411,14 +463,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
@@ -430,12 +482,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
@@ -447,9 +499,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
@@ -457,8 +509,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
@@ -478,14 +530,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
@@ -505,63 +557,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
@@ -581,36 +633,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
@@ -621,7 +673,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
@@ -635,7 +687,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
@@ -644,8 +696,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
@@ -654,9 +706,99 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
end
- def test_association_force_reload_with_only_true_is_deprecated
- firm = Firm.find(1)
+ 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_deprecated { firm.account(true) }
+ 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 b2b46812b9..69b4872519 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -1,29 +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/customer'
-require 'models/carrier'
-require 'models/shop_account'
-require 'models/customer_carrier'
+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)
@@ -33,15 +35,34 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_equal clubs(:boring_club), @member.club
end
+ def test_has_one_through_executes_limited_query
+ boring_club = clubs(:boring_club)
+ assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do
+ assert_equal boring_club, @member.general_club
+ end
+ 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
@@ -50,9 +71,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
@@ -65,7 +104,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
@@ -73,7 +112,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
@@ -82,40 +121,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
@@ -123,31 +169,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 }
@@ -159,8 +205,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
@@ -176,37 +222,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
@@ -217,10 +263,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
@@ -230,19 +276,19 @@ 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!
@@ -261,7 +307,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_value_is_properly_quoted
- minivan = Minivan.find('m1')
+ minivan = Minivan.find("m1")
assert_nothing_raised do
minivan.dashboard
end
@@ -270,7 +316,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
@@ -282,12 +328,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
@@ -308,12 +354,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
@@ -325,7 +371,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
minivan.speedometer_id = speedometers(:second).id
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal dashboards(:second), minivan.dashboard
end
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index 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 c9743e80d3..da3a42e2b5 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -1,21 +1,25 @@
+# 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/developer'
-require 'models/company'
-require 'models/project'
+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"
class AutomaticInverseFindingTests < ActiveRecord::TestCase
fixtures :ratings, :comments, :cars
@@ -24,11 +28,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
@@ -37,7 +39,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
@@ -46,11 +47,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
@@ -59,11 +58,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)
@@ -107,77 +119,53 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
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 do
- Class.new(ActiveRecord::Base).has_many(:wheels, :inverse_of => :car)
+ Class.new(ActiveRecord::Base).has_many(:wheels, inverse_of: :car)
end
assert_nothing_raised do
- Class.new(ActiveRecord::Base).has_one(:engine, :inverse_of => :car)
+ Class.new(ActiveRecord::Base).has_one(:engine, inverse_of: :car)
end
assert_nothing_raised do
- Class.new(ActiveRecord::Base).belongs_to(:car, :inverse_of => :driver)
+ 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
@@ -202,10 +190,20 @@ class InverseAssociationTests < ActiveRecord::TestCase
assert_nil belongs_to_ref.inverse_of
end
+ def test_polymorphic_associations_dont_attempt_to_find_inverse_of
+ belongs_to_ref = Sponsor.reflect_on_association(:sponsor)
+ assert_raise(ArgumentError) { belongs_to_ref.klass }
+ assert_nil belongs_to_ref.inverse_of
+
+ belongs_to_ref = Face.reflect_on_association(:human)
+ assert_raise(ArgumentError) { belongs_to_ref.klass }
+ assert_nil belongs_to_ref.inverse_of
+ end
+
def test_this_inverse_stuff
- firm = Firm.create!(name: 'Adequate Holdings')
- Project.create!(name: 'Project 1', firm: firm)
- Developer.create!(name: 'Gorbypuff', firm: firm)
+ 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"
@@ -220,73 +218,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
@@ -296,74 +293,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
@@ -385,25 +403,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
@@ -444,7 +462,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"
@@ -456,7 +474,7 @@ class InverseHasManyTests < ActiveRecord::TestCase
interest = Interest.create!(man: man)
man.interests.find(interest.id)
- assert_not man.interests.loaded?
+ assert_not_predicate man.interests, :loaded?
end
def test_raise_record_not_found_error_when_invalid_ids_are_passed
@@ -476,7 +494,10 @@ class InverseHasManyTests < ActiveRecord::TestCase
def test_raise_record_not_found_error_when_no_ids_are_passed
man = Man.create!
- assert_raise(ActiveRecord::RecordNotFound) { man.interests.find() }
+ exception = assert_raise(ActiveRecord::RecordNotFound) { man.interests.load.find() }
+
+ assert_equal exception.model, "Interest"
+ assert_equal exception.primary_key, "id"
end
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
@@ -485,7 +506,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
@@ -493,7 +514,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
@@ -504,49 +552,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
@@ -554,24 +602,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
@@ -584,30 +632,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
@@ -619,23 +667,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
@@ -651,16 +685,26 @@ 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
@@ -698,8 +742,8 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase
def test_that_we_can_create_associations_that_have_the_same_reciprocal_name_from_different_models
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 c7bd9d2119..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
@@ -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,19 +209,19 @@ 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_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
@@ -235,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 }
@@ -267,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 }
@@ -276,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 }
@@ -302,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
@@ -324,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]
@@ -354,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
@@ -365,13 +367,13 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
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'
+ Tag.has_many :null_tagged_posts, through: :null_taggings, source: "taggable", source_type: "Post"
assert_equal [], tags(:general).null_tagged_posts
- refute_equal [], tags(:general).tagged_posts
+ assert_not_equal [], tags(:general).tagged_posts
end
def test_eager_has_many_polymorphic_with_source_type
- 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
@@ -381,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
@@ -402,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
@@ -413,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
@@ -434,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
@@ -445,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
@@ -462,58 +464,57 @@ 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.reload.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.reload.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.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.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.reload.size)
@@ -528,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
@@ -550,7 +551,7 @@ 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.reload.size)
@@ -564,7 +565,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
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.reload.size)
@@ -581,9 +582,9 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
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.reload.size)
@@ -598,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
@@ -611,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
@@ -642,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 }
@@ -669,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 }
@@ -696,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
@@ -709,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
@@ -719,47 +720,47 @@ 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)
+ 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)
+ 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)
+ 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
@@ -770,8 +771,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
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
index 4af791b758..0e54e8c1b0 100644
--- a/activerecord/test/cases/associations/left_outer_join_association_test.rb
+++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb
@@ -1,13 +1,16 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/post'
-require 'models/comment'
-require 'models/author'
-require 'models/essay'
-require 'models/categorization'
-require 'models/person'
+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, :essays, :posts, :comments, :categorizations, :people
+ 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
@@ -17,56 +20,65 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
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
+ 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 =~ sql }
+ assert queries.any? { |sql| /agents_people_4/i.match?(sql) }
end
end
- def test_construct_finder_sql_executes_a_left_outer_join
- assert_not_equal Author.count, Author.joins(:posts).count
- assert_equal Author.count, Author.left_outer_joins(:posts).count
+ 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_join_by_left_joins
- assert_not_equal Author.count, Author.joins(:posts).count
- assert_equal Author.count, Author.left_joins(:posts).count
+ 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({}) }
- assert queries.none? { |sql| /LEFT OUTER JOIN/i =~ sql }
+ 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([]) }
- assert queries.none? { |sql| /LEFT OUTER JOIN/i =~ sql }
+ 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 }
+ 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)/i =~ sql }
- assert queries.none? { |sql| /WHERE/i =~ sql }
+ 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)
+ scope = Post.left_outer_joins(:special_comments).where(id: posts(:sti_comments).id)
# The join should match SpecialComment and its subclasses only
- assert scope.where("comments.type" => "Comment").empty?
- assert !scope.where("comments.type" => "SpecialComment").empty?
- assert !scope.where("comments.type" => "SubSpecialComment").empty?
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ assert_not_empty scope.where("comments.type" => "SubSpecialComment")
end
def test_does_not_override_select
authors = Author.select("authors.name, #{%{(authors.author_address_id || ' ' || authors.author_address_extra_id) as addr_id}}").left_outer_joins(:posts)
- assert authors.any?
- assert authors.first.respond_to?(:addr_id)
+ assert_predicate authors, :any?
+ assert_respond_to authors.first, :addr_id
end
test "the default scope of the target is applied when joining associations" do
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index b040485d99..35da74102d 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
@@ -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
@@ -536,10 +548,19 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_through_association_preload_doesnt_reset_source_association_if_already_preloaded
+ blue = tags(:blue)
+ authors = Author.preload(posts: :first_blue_tags_2, misc_post_first_blue_tags_2: {}).to_a.sort_by(&:id)
+
+ assert_no_queries do
+ assert_equal [blue], authors[2].posts.first.first_blue_tags_2
+ end
+ end
+
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 +569,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 +583,46 @@ 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
+
+ def test_has_many_through_reset_source_reflection_after_loading_is_complete
+ preloaded = Category.preload(:ordered_post_comments).find(1, 2).last
+ original = Category.find(2)
+ assert_equal original.ordered_post_comments.ids, preloaded.ordered_post_comments.ids
end
private
diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb
index 3e5494e897..c7a78e6bc4 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,11 +20,14 @@ 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
+ test "belongs_to associations can be optional by default" do
+ original_value = ActiveRecord::Base.belongs_to_required_by_default
+ ActiveRecord::Base.belongs_to_required_by_default = false
+
model = subclass_of(Child) do
belongs_to :parent, inverse_of: false,
class_name: "RequiredAssociationsTest::Parent"
@@ -30,6 +35,8 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
assert model.new.save
assert model.new(parent: Parent.new).save
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
end
test "required belongs_to associations have presence validated" do
@@ -46,6 +53,25 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
assert record.save
end
+ test "belongs_to associations can be required by default" do
+ 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
+
test "has_one associations are not required by default" do
model = subclass_of(Parent) do
has_one :child, inverse_of: false,
@@ -92,11 +118,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 01a058918a..84130ec208 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -1,87 +1,91 @@
+# frozen_string_literal: true
+
require "cases/helper"
-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'
+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"
+require "models/pirate"
+require "models/parrot"
+require "models/bird"
+require "models/treasure"
+require "models/price_estimate"
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"
@@ -91,10 +95,10 @@ class AssociationsTest < ActiveRecord::TestCase
assert firm.clients.empty?, "New firm should have cached no client objects"
assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count"
- ActiveSupport::Deprecation.silence do
- assert !firm.clients(true).empty?, "New firm should have reloaded client objects"
- assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count"
- end
+ firm.clients.reload
+
+ assert_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,105 +107,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)
-
- ActiveSupport::Deprecation.silence do
- ActiveRecord::Base.cache do
- firm.clients.each {}
- assert_queries(0) { assert_not_nil firm.clients.each {} }
- assert_queries(1) { assert_not_nil firm.clients(true).each {} }
- end
- end
+ 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
@@ -219,7 +217,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
@@ -231,7 +229,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
@@ -247,16 +252,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
@@ -264,18 +278,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
@@ -359,3 +373,97 @@ class GeneratedMethodsTest < ActiveRecord::TestCase
assert_equal :none, MyArticle.new.comments
end
end
+
+class WithAnnotationsTest < ActiveRecord::TestCase
+ fixtures :pirates, :parrots
+
+ def test_belongs_to_with_annotation_includes_a_query_comment
+ pirate = SpacePirate.where.not(parrot_id: nil).first
+ assert pirate, "should have a Pirate record"
+
+ log = capture_sql do
+ pirate.parrot
+ end
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+
+ assert_sql(%r{/\* that tells jokes \*/}) do
+ pirate.parrot_with_annotation
+ end
+ end
+
+ def test_has_and_belongs_to_many_with_annotation_includes_a_query_comment
+ pirate = SpacePirate.first
+ assert pirate, "should have a Pirate record"
+
+ log = capture_sql do
+ pirate.parrots.first
+ end
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+
+ assert_sql(%r{/\* that are very colorful \*/}) do
+ pirate.parrots_with_annotation.first
+ end
+ end
+
+ def test_has_one_with_annotation_includes_a_query_comment
+ pirate = SpacePirate.first
+ assert pirate, "should have a Pirate record"
+
+ log = capture_sql do
+ pirate.ship
+ end
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+
+ assert_sql(%r{/\* that is a rocket \*/}) do
+ pirate.ship_with_annotation
+ end
+ end
+
+ def test_has_many_with_annotation_includes_a_query_comment
+ pirate = SpacePirate.first
+ assert pirate, "should have a Pirate record"
+
+ log = capture_sql do
+ pirate.birds.first
+ end
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+
+ assert_sql(%r{/\* that are also parrots \*/}) do
+ pirate.birds_with_annotation.first
+ end
+ end
+
+ def test_has_many_through_with_annotation_includes_a_query_comment
+ pirate = SpacePirate.first
+ assert pirate, "should have a Pirate record"
+
+ log = capture_sql do
+ pirate.treasure_estimates.first
+ end
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+
+ assert_sql(%r{/\* yarrr \*/}) do
+ pirate.treasure_estimates_with_annotation.first
+ end
+ end
+
+ def test_has_many_through_with_annotation_includes_a_query_comment_when_eager_loading
+ pirate = SpacePirate.first
+ assert pirate, "should have a Pirate record"
+
+ log = capture_sql do
+ pirate.treasure_estimates.first
+ end
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+
+ assert_sql(%r{/\* yarrr \*/}) do
+ SpacePirate.includes(:treasure_estimates_with_annotation, :treasures).first
+ end
+ end
+end
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 1db52af59b..9fd62dcf72 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -1,15 +1,17 @@
+# frozen_string_literal: true
+
require "cases/helper"
-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'
+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
@@ -19,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
@@ -27,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)
@@ -56,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
@@ -64,44 +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)
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?(:Mysql2Adapter)
- 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
+ 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,26 @@ 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 "write_attribute allows writing to aliased attributes" do
+ topic = Topic.first
+ assert_nothing_raised { topic.update_columns(heading: "Hello!") }
+ assert_nothing_raised { topic.update(heading: "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 +339,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 +373,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 +388,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 +407,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,41 +421,108 @@ 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?
+
+ ActiveModel::Type::Boolean::FALSE_VALUES.each do |value|
+ assert_predicate Topic.new(author_name: value), :author_name?
+ end
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
- object = Company.find_by_sql(<<-SQL).first
+ test "user-defined text attribute predicate" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = Topic.table_name
+
+ attribute :user_defined_text, :text
+ end
+
+ topic = klass.new(user_defined_text: "text")
+ assert_predicate topic, :user_defined_text?
+
+ ActiveModel::Type::Boolean::FALSE_VALUES.each do |value|
+ topic = klass.new(user_defined_text: value)
+ assert_predicate topic, :user_defined_text?
+ end
+ end
+
+ test "user-defined date attribute predicate" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = Topic.table_name
+
+ attribute :user_defined_date, :date
+ end
+
+ topic = klass.new(user_defined_date: Date.current)
+ assert_predicate topic, :user_defined_date?
+ end
+
+ test "user-defined datetime attribute predicate" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = Topic.table_name
+
+ attribute :user_defined_datetime, :datetime
+ end
+
+ topic = klass.new(user_defined_datetime: Time.current)
+ assert_predicate topic, :user_defined_datetime?
+ end
+
+ test "user-defined time attribute predicate" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = Topic.table_name
+
+ attribute :user_defined_time, :time
+ end
+
+ topic = klass.new(user_defined_time: Time.current)
+ assert_predicate topic, :user_defined_time?
+ end
+
+ test "user-defined json attribute predicate" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = Topic.table_name
+
+ attribute :user_defined_json, :json
+ end
+
+ topic = klass.new(user_defined_json: { key: "value" })
+ assert_predicate topic, :user_defined_json?
+
+ topic = klass.new(user_defined_json: {})
+ assert_not_predicate topic, :user_defined_json?
+ end
+
+ 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
WHERE c1.firm_id = c2.id
@@ -423,107 +530,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 +638,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
@@ -546,7 +653,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
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
@@ -554,7 +661,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)
@@ -562,7 +669,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
@@ -574,7 +681,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
@@ -585,11 +692,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
@@ -597,23 +704,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
@@ -622,30 +729,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
@@ -654,10 +761,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
@@ -665,15 +772,19 @@ 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"
assert_equal record, YAML.load(YAML.dump(record))
end
+ ensure
+ # NOTE: Reset column info because global topics
+ # don't have tz-aware attributes by default.
+ Topic.reset_column_information
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"
@@ -683,12 +794,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)
@@ -702,26 +813,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_time_zone_aware_attributes_dont_recurse_infinitely_on_invalid_values
+ test "time zone-aware attributes do not recurse infinitely on invalid values" do
in_time_zone "Pacific Time (US & Canada)" do
record = @target.new(bonus_time: [])
- assert_equal nil, record.bonus_time
+ assert_nil record.bonus_time
end
end
- def test_setting_time_zone_conversion_for_attributes_should_write_value_on_class_variable
+ 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]
@@ -729,44 +850,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")
}
@@ -775,43 +896,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_overwritten_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
@@ -827,14 +950,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
@@ -850,9 +980,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
@@ -868,18 +998,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
@@ -890,7 +1020,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
@@ -902,46 +1032,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
@@ -951,40 +1081,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(Topic::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 7a24b85a36..0000000000
--- a/activerecord/test/cases/attribute_set_test.rb
+++ /dev/null
@@ -1,253 +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, 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 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
-
- 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
-
- 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
-
- 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
- end
-end
diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb
deleted file mode 100644
index a24a4fc6a4..0000000000
--- a/activerecord/test/cases/attribute_test.rb
+++ /dev/null
@@ -1,246 +0,0 @@
-require 'cases/helper'
-
-module ActiveRecord
- class AttributeTest < ActiveRecord::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 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 is not changed if it hasn't been assigned or mutated" do
- attribute = Attribute.from_database(:foo, 1, Type::Value.new)
-
- refute attribute.changed?
- end
-
- test "an attribute is changed if it's been assigned a new value" do
- attribute = Attribute.from_database(:foo, 1, Type::Value.new)
- changed = attribute.with_value_from_user(2)
-
- assert changed.changed?
- end
-
- test "an attribute is not changed if it's assigned the same value" do
- attribute = Attribute.from_database(:foo, 1, Type::Value.new)
- unchanged = attribute.with_value_from_user(1)
-
- refute unchanged.changed?
- 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?
- end
-
- test "an attribute is changed if it has been mutated" do
- attribute = Attribute.from_database(:foo, "bar", Type::String.new)
- attribute.value << "!"
-
- assert attribute.changed_in_place?
- assert attribute.changed?
- 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
- refute 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
- end
-end
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
index 2bebbfa205..a6fb9f0af7 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,71 @@ 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 "extra options are forwarded to the type caster constructor" do
+ klass = Class.new(OverloadedType) do
+ attribute :starts_at, :datetime, precision: 3, limit: 2, scale: 1
+ end
+
+ starts_at_type = klass.type_for_attribute(:starts_at)
+ assert_equal 3, starts_at_type.precision
+ assert_equal 2, starts_at_type.limit
+ assert_equal 1, starts_at_type.scale
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 +119,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 +159,20 @@ 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
@@ -196,5 +234,63 @@ module ActiveRecord
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 9e3266b7d6..1a0732c14b 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -1,56 +1,60 @@
-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'
-require 'models/guitar'
-require 'models/tuning_peg'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/author"
+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
@@ -73,60 +77,60 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
ship = ShipWithoutNestedAttributes.new
ship.prisoners.build
- assert_not ship.valid?
+ assert_not_predicate ship, :valid?
assert_equal 1, ship.errors[:name].length
end
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
@@ -135,10 +139,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
@@ -146,16 +150,16 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
firm.account = account = Account.new("credit_limit" => 1000)
assert_equal account, firm.account
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
assert firm.save
assert_equal account, firm.account
- assert account.persisted?
+ assert_predicate account, :persisted?
end
def test_assignment_before_parent_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.find(1)
- assert !firm.persisted?
+ assert_not_predicate firm, :persisted?
assert_equal a, firm.account
assert firm.save
assert_equal a, firm.account
@@ -166,20 +170,20 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
def test_assignment_before_either_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.new("credit_limit" => 1000)
- assert !firm.persisted?
- assert !a.persisted?
+ assert_not_predicate firm, :persisted?
+ assert_not_predicate a, :persisted?
assert_equal a, firm.account
assert firm.save
- assert firm.persisted?
- assert a.persisted?
+ assert_predicate firm, :persisted?
+ assert_predicate a, :persisted?
assert_equal a, firm.account
firm.association(:account).reload
assert_equal a, firm.account
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
@@ -196,21 +200,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
@@ -219,34 +223,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
@@ -255,10 +259,10 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
apple = Firm.new("name" => "Apple")
client.firm = apple
assert_equal apple, client.firm
- assert !apple.persisted?
+ assert_not_predicate apple, :persisted?
assert client.save
assert apple.save
- assert apple.persisted?
+ assert_predicate apple, :persisted?
assert_equal apple, client.firm
client.association(:firm).reload
assert_equal apple, client.firm
@@ -268,11 +272,11 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
final_cut = Client.new("name" => "Final Cut")
apple = Firm.new("name" => "Apple")
final_cut.firm = apple
- assert !final_cut.persisted?
- assert !apple.persisted?
+ assert_not_predicate final_cut, :persisted?
+ assert_not_predicate apple, :persisted?
assert final_cut.save
- assert final_cut.persisted?
- assert apple.persisted?
+ assert_predicate final_cut, :persisted?
+ assert_predicate apple, :persisted?
assert_equal apple, final_cut.firm
final_cut.association(:firm).reload
assert_equal apple, final_cut.firm
@@ -362,41 +366,41 @@ 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
@@ -407,9 +411,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid]
- assert_not tuning_peg_invalid.valid?
- assert tuning_peg_valid.valid?
- assert_not guitar.valid?
+ assert_not_predicate tuning_peg_invalid, :valid?
+ assert_predicate tuning_peg_valid, :valid?
+ assert_not_predicate guitar, :valid?
assert_equal ["is not a number"], guitar.errors["tuning_pegs[1].pitch"]
assert_not_equal ["is not a number"], guitar.errors["tuning_pegs.pitch"]
end
@@ -419,14 +423,14 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
ActiveRecord::Base.index_nested_attribute_errors = true
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]
- assert_not invalid_electron.valid?
- assert valid_electron.valid?
- assert_not molecule.valid?
+ assert_not_predicate invalid_electron, :valid?
+ assert_predicate valid_electron, :valid?
+ assert_not_predicate molecule, :valid?
assert_equal ["can't be blank"], molecule.errors["electrons[1].name"]
assert_not_equal ["can't be blank"], molecule.errors["electrons.name"]
ensure
@@ -435,15 +439,15 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
def test_errors_details_should_be_set
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]
- assert_not invalid_electron.valid?
- assert valid_electron.valid?
- assert_not molecule.valid?
- assert_equal [{error: :blank}], molecule.errors.details["electrons.name"]
+ 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
@@ -454,11 +458,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid]
- assert_not tuning_peg_invalid.valid?
- assert tuning_peg_valid.valid?
- assert_not guitar.valid?
- assert_equal [{error: :not_a_number, value: nil}] , guitar.errors.details["tuning_pegs[1].pitch"]
- assert_equal [], guitar.errors.details["tuning_pegs.pitch"]
+ 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
@@ -466,54 +470,66 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
ActiveRecord::Base.index_nested_attribute_errors = true
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]
- assert_not invalid_electron.valid?
- assert valid_electron.valid?
- assert_not molecule.valid?
- assert_equal [{error: :blank}], molecule.errors.details["electrons[1].name"]
- assert_equal [], molecule.errors.details["electrons.name"]
+ 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
@@ -521,10 +537,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
@@ -533,24 +549,52 @@ 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_not companies(:first_firm).save
+ assert_not_predicate new_client, :persisted?
assert_equal 2, companies(:first_firm).clients_of_firm.reload.size
end
@@ -569,8 +613,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
assert_equal no_of_firms, Firm.count # Firm was not saved to database.
assert_equal no_of_clients, Client.count # Clients were not saved to database.
assert new_firm.save
- assert new_firm.persisted?
- assert c.persisted?
+ assert_predicate new_firm, :persisted?
+ assert_predicate c, :persisted?
assert_equal new_firm, c.firm
assert_equal no_of_firms + 1, Firm.count # Firm was saved to database.
assert_equal no_of_clients + 2, Client.count # Clients were saved to database.
@@ -585,58 +629,74 @@ 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_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.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_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.reload.size
end
@@ -647,7 +707,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
@@ -656,67 +716,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
@@ -727,8 +787,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
@@ -744,18 +804,18 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.mark_for_destruction
@pirate.ship.mark_for_destruction
- assert !@pirate.reload.marked_for_destruction?
- assert !@pirate.ship.reload.marked_for_destruction?
+ assert_not_predicate @pirate.reload, :marked_for_destruction?
+ assert_not_predicate @pirate.ship.reload, :marked_for_destruction?
end
# has_one
def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction
- assert !@pirate.ship.marked_for_destruction?
+ assert_not_predicate @pirate.ship, :marked_for_destruction?
@pirate.ship.mark_for_destruction
id = @pirate.ship.id
- assert @pirate.ship.marked_for_destruction?
+ assert_predicate @pirate.ship, :marked_for_destruction?
assert Ship.find_by_id(id)
@pirate.save
@@ -764,12 +824,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
@@ -787,13 +848,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
@@ -804,18 +866,19 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved
- @pirate.ship.expects(:save).never
- assert @pirate.save
+ assert_not_called(@pirate.ship, :save) do
+ assert @pirate.save
+ end
end
# belongs_to
def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction
- assert !@ship.pirate.marked_for_destruction?
+ assert_not_predicate @ship.pirate, :marked_for_destruction?
@ship.pirate.mark_for_destruction
id = @ship.pirate.id
- assert @ship.pirate.marked_for_destruction?
+ assert_predicate @ship.pirate, :marked_for_destruction?
assert Pirate.find_by_id(id)
@ship.save
@@ -824,12 +887,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
@@ -847,29 +911,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
@@ -879,7 +943,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
@@ -887,62 +951,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!
@@ -968,8 +1037,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>",
@@ -982,7 +1051,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
@@ -999,71 +1068,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
@@ -1072,8 +1143,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>",
@@ -1086,7 +1157,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
@@ -1108,60 +1179,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
@@ -1169,49 +1240,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
@@ -1219,51 +1290,75 @@ 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 ship.valid?
+ assert_not_predicate ship, :valid?
end
end
class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
- def setup
- super
+ def create_member_with_organization
organization = Organization.create
- @member = Member.create
- MemberDetail.create(organization: organization, member: @member)
+ member = Member.create
+ MemberDetail.create(organization: organization, member: member)
+
+ member
end
def test_should_not_has_one_through_model
- class << @member.organization
+ member = create_member_with_organization
+
+ class << member.organization
+ def save(*args)
+ super
+ raise "Oh noes!"
+ end
+ end
+ assert_nothing_raised { member.save }
+ end
+
+ def create_author_with_post_with_comment
+ Author.create! name: "David" # make comment_id not match author_id
+ author = Author.create! name: "Sergiy"
+ post = Post.create! author: author, title: "foo", body: "bar"
+ Comment.create! post: post, body: "cool comment"
+
+ author
+ end
+
+ def test_should_not_reversed_has_one_through_model
+ author = create_author_with_post_with_comment
+
+ class << author.comment_on_first_post
def save(*args)
super
- raise 'Oh noes!'
+ raise "Oh noes!"
end
end
- assert_nothing_raised { @member.save }
+ assert_nothing_raised { author.save }
end
end
@@ -1272,70 +1367,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
@@ -1343,41 +1438,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
@@ -1390,51 +1485,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], [
@@ -1443,7 +1546,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
@@ -1461,64 +1564,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
@@ -1533,9 +1636,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
@@ -1551,8 +1654,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
@@ -1568,8 +1671,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
@@ -1580,15 +1683,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
@@ -1597,21 +1700,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
@@ -1620,19 +1723,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
@@ -1641,21 +1744,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
@@ -1676,7 +1779,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
@@ -1684,7 +1787,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
@@ -1695,6 +1798,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_association_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 eef2d29d02..866818b2ab 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1,32 +1,32 @@
+# frozen_string_literal: true
+
require "cases/helper"
-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 'concurrent/atomic/count_down_latch'
+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
@@ -35,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
@@ -47,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
@@ -58,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
@@ -75,17 +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' => '"',
- 'Mysql2Adapter' => '`',
- 'PostgreSQLAdapter' => '"',
- 'OracleAdapter' => '"',
- 'FbAdapter' => '"'
+ "SQLite3Adapter" => '"',
+ "Mysql2Adapter" => "`",
+ "PostgreSQLAdapter" => '"',
+ "OracleAdapter" => '"',
+ "FbAdapter" => '"'
}.fetch(classname) {
raise "need a bad char for #{classname}"
}
@@ -102,19 +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_deprecated do
- assert Topic.limit("1,2").to_a
- end
- end
+ def test_primary_key_and_references_columns_should_be_identical_type
+ pk = Author.columns_hash["id"]
+ ref = Post.columns_hash["author_id"]
+
+ assert_equal pk.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
@@ -139,10 +135,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_limit_should_sanitize_sql_injection_for_limit_with_commas
- assert_deprecated do
- assert_raises(ArgumentError) do
- Topic.limit("1, 7 procedure help()").to_a
- end
+ assert_raises(ArgumentError) do
+ Topic.limit("1, 7 procedure help()").to_a
end
end
@@ -152,8 +146,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_table_exists
- assert !NonExistentTable.table_exists?
- assert Topic.table_exists?
+ assert_not_predicate NonExistentTable, :table_exists?
+ assert_predicate Topic, :table_exists?
end
def test_preserving_date_objects
@@ -166,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
@@ -213,7 +207,7 @@ class BasicsTest < ActiveRecord::TestCase
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
@@ -225,9 +219,9 @@ class BasicsTest < ActiveRecord::TestCase
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc
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
@@ -241,7 +235,7 @@ class BasicsTest < ActiveRecord::TestCase
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
@@ -253,9 +247,9 @@ class BasicsTest < ActiveRecord::TestCase
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local
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
@@ -281,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)
@@ -443,12 +438,6 @@ class BasicsTest < ActiveRecord::TestCase
Post.reset_table_name
end
- 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!')
- end
- end
-
def test_null_fields
assert_nil Topic.find(1).parent_id
assert_nil Topic.create("title" => "Hey you").parent_id
@@ -456,7 +445,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
@@ -464,7 +453,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
@@ -490,12 +479,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
@@ -520,16 +509,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, 2]), Topic.find(['1-meowmeow', '2-hello'])
- assert_equal 'The Second Topic of the day', Topic.find(['2-hello', '1-meowmeow']).first.title
+ 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
@@ -538,7 +527,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
@@ -547,8 +536,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
@@ -557,8 +546,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
@@ -566,7 +555,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
@@ -574,8 +563,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
@@ -583,8 +572,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
@@ -606,24 +595,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
@@ -635,9 +624,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
@@ -649,8 +638,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)
@@ -660,7 +649,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
@@ -668,21 +657,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
@@ -696,6 +685,9 @@ class BasicsTest < ActiveRecord::TestCase
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time
+
+ topic.save!
+ assert_equal topic, Topic.find_by(attributes)
end
end
@@ -711,46 +703,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
-
- subclass = Class.new(Boolean).find true_id
- superclass = Boolean.find true_id
-
- assert_equal superclass.read_attribute(:has_fun), subclass.read_attribute(:has_fun)
- end
+ def test_attributes
+ category = Category.new(name: "Ruby")
- 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
+ expected_attributes = category.attribute_names.map do |attribute_name|
+ [attribute_name, category.public_send(attribute_name)]
+ end.to_h
- 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
@@ -763,7 +724,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"
@@ -781,7 +742,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
@@ -791,8 +752,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)
@@ -802,7 +763,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_nothing_raised { dup = dev.dup }
assert_kind_of DeveloperSalary, dup.salary
assert_equal dev.salary.amount, dup.salary.amount
- assert !dup.persisted?
+ assert_not_predicate dup, :persisted?
# test if the attributes have been duped
original_amount = dup.salary.amount
@@ -810,7 +771,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal original_amount, dup.salary.amount
assert dup.save
- assert dup.persisted?
+ assert_predicate dup, :persisted?
assert_not_equal dup.id, dev.id
end
@@ -830,154 +791,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_numeric_fields_with_scale
- m = NumericData.new(
- :bank_balance => 1586.43122334,
- :big_bank_balance => BigDecimal("234000567.952344"),
- :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("234000567.95"), m1.big_bank_balance
- end
-
def test_auto_id
auto = AutoId.new
auto.save
@@ -1001,28 +895,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
@@ -1034,12 +928,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)
@@ -1052,7 +940,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"
@@ -1095,7 +983,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
@@ -1111,7 +999,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
@@ -1129,45 +1017,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
@@ -1177,37 +1051,37 @@ 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.order("developers.salary ASC").last
+ assert_equal last, Developer.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.order("developers.salary DESC").last
+ assert_equal last, Developer.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.order("developers.name, developers.salary DESC").last
+ assert_equal last, Developer.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.order("developers.name, developers.salary").to_a
+ assert_equal combined, Developer.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
+ 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
@@ -1218,7 +1092,7 @@ class BasicsTest < ActiveRecord::TestCase
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 +1101,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 +1112,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)
@@ -1253,7 +1127,7 @@ class BasicsTest < ActiveRecord::TestCase
log = StringIO.new
ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
ActiveRecord::Base.logger.level = Logger::DEBUG
- ActiveRecord::Base.benchmark("Logging", :level => :debug, :silence => false) { ActiveRecord::Base.logger.debug "Quiet" }
+ ActiveRecord::Base.benchmark("Logging", level: :debug, silence: false) { ActiveRecord::Base.logger.debug "Quiet" }
assert_match(/Quiet/, log.string)
ensure
ActiveRecord::Base.logger = original_logger
@@ -1261,9 +1135,9 @@ class BasicsTest < ActiveRecord::TestCase
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
@@ -1280,13 +1154,13 @@ class BasicsTest < ActiveRecord::TestCase
ActiveSupport::Dependencies.remove_unloadable_constants!
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
@@ -1349,16 +1223,17 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_attribute_names
- assert_equal ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"],
- Company.attribute_names
+ expected = ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description", "metadata"]
+ assert_equal expected, Company.attribute_names
end
def test_has_attribute
- assert Company.has_attribute?('id')
- assert Company.has_attribute?('type')
- assert Company.has_attribute?('name')
- assert_not Company.has_attribute?('lastname')
- assert_not Company.has_attribute?('age')
+ assert Company.has_attribute?("id")
+ assert Company.has_attribute?("type")
+ assert Company.has_attribute?("name")
+ assert Company.has_attribute?("metadata")
+ assert_not Company.has_attribute?("lastname")
+ assert_not Company.has_attribute?("age")
end
def test_has_attribute_with_symbol
@@ -1375,18 +1250,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
@@ -1397,37 +1266,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
@@ -1438,7 +1317,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
@@ -1504,31 +1383,167 @@ 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'
- refute_includes Developer.columns_hash.keys, 'first_name'
+ assert_includes cache_columns.keys, "first_name"
+ assert_not_includes Developer.columns_hash.keys, "first_name"
+ assert_not_includes SubDeveloper.columns_hash.keys, "first_name"
+ assert_not_includes SymbolIgnoredDeveloper.columns_hash.keys, "first_name"
end
test "ignored columns have no attribute methods" do
- refute Developer.new.respond_to?(:first_name)
- refute Developer.new.respond_to?(:first_name=)
- refute Developer.new.respond_to?(:first_name?)
+ assert_not_respond_to Developer.new, :first_name
+ assert_not_respond_to Developer.new, :first_name=
+ assert_not_respond_to Developer.new, :first_name?
+ assert_not_respond_to SubDeveloper.new, :first_name
+ assert_not_respond_to SubDeveloper.new, :first_name=
+ assert_not_respond_to SubDeveloper.new, :first_name?
+ assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name
+ assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name=
+ assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name?
end
test "ignored columns don't prevent explicit declaration of attribute methods" do
- assert Developer.new.respond_to?(:last_name)
- assert Developer.new.respond_to?(:last_name=)
- assert Developer.new.respond_to?(:last_name?)
+ assert_respond_to Developer.new, :last_name
+ assert_respond_to Developer.new, :last_name=
+ assert_respond_to Developer.new, :last_name?
+ assert_respond_to SubDeveloper.new, :last_name
+ assert_respond_to SubDeveloper.new, :last_name=
+ assert_respond_to SubDeveloper.new, :last_name?
+ assert_respond_to SymbolIgnoredDeveloper.new, :last_name
+ assert_respond_to SymbolIgnoredDeveloper.new, :last_name=
+ assert_respond_to SymbolIgnoredDeveloper.new, :last_name?
+ end
+
+ test "ignored columns are stored as an array of string" do
+ assert_equal(%w(first_name last_name), Developer.ignored_columns)
+ assert_equal(%w(first_name last_name), SymbolIgnoredDeveloper.ignored_columns)
+ end
+
+ test "when #reload called, ignored columns' attribute methods are not defined" do
+ developer = Developer.create!(name: "Developer")
+ assert_not_respond_to developer, :first_name
+ assert_not_respond_to developer, :first_name=
+
+ developer.reload
+
+ assert_not_respond_to developer, :first_name
+ assert_not_respond_to developer, :first_name=
+ end
+
+ test "when ignored attribute is loaded, cast type should be preferred over DB type" do
+ developer = AttributedDeveloper.create
+ developer.update_column :name, "name"
+
+ loaded_developer = AttributedDeveloper.where(id: developer.id).select("*").first
+ assert_equal "Developer: name", loaded_developer.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
+
+ test "creating a record raises if preventing writes" do
+ error = assert_raises ActiveRecord::ReadOnlyError do
+ ActiveRecord::Base.connection.while_preventing_writes do
+ Bird.create! name: "Bluejay"
+ end
+ end
+
+ assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, error.message
+ end
+
+ test "updating a record raises if preventing writes" do
+ bird = Bird.create! name: "Bluejay"
+
+ error = assert_raises ActiveRecord::ReadOnlyError do
+ ActiveRecord::Base.connection.while_preventing_writes do
+ bird.update! name: "Robin"
+ end
+ end
+
+ assert_match %r/\AWrite query attempted while in readonly mode: UPDATE /, error.message
+ end
+
+ test "deleting a record raises if preventing writes" do
+ bird = Bird.create! name: "Bluejay"
+
+ error = assert_raises ActiveRecord::ReadOnlyError do
+ ActiveRecord::Base.connection.while_preventing_writes do
+ bird.destroy!
+ end
+ end
+
+ assert_match %r/\AWrite query attempted while in readonly mode: DELETE /, error.message
+ end
+
+ test "selecting a record does not raise if preventing writes" do
+ bird = Bird.create! name: "Bluejay"
+
+ ActiveRecord::Base.connection.while_preventing_writes do
+ assert_equal bird, Bird.where(name: "Bluejay").first
+ end
+ end
+
+ test "an explain query does not raise if preventing writes" do
+ Bird.create!(name: "Bluejay")
+
+ ActiveRecord::Base.connection.while_preventing_writes do
+ assert_queries(2) { Bird.where(name: "Bluejay").explain }
+ end
+ end
+
+ test "an empty transaction does not raise if preventing writes" do
+ ActiveRecord::Base.connection.while_preventing_writes do
+ assert_queries(2, ignore_none: true) do
+ Bird.transaction do
+ ActiveRecord::Base.connection.materialize_transactions
+ end
+ end
+ end
end
end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index 91ff5146fd..cf6e280898 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, start: 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
@@ -62,16 +63,32 @@ 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
- assert_called(ActiveRecord::Base.logger, :warn) do
- 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
@@ -84,7 +101,7 @@ class EachTest < ActiveRecord::TestCase
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
@@ -92,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
@@ -119,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
@@ -138,11 +155,11 @@ 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 = +"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
+ 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|
+ Post.find_in_batches(batch_size: 1) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
@@ -166,37 +183,37 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_error_on_ignore_the_order
assert_raise(ArgumentError) do
- PostWithDefaultScope.find_in_batches(error_on_ignore: true){}
+ PostWithDefaultScope.find_in_batches(error_on_ignore: true) { }
end
end
- def test_find_in_batches_should_not_error_if_config_overriden
- # Set the config option which will be overriden
- prev = ActiveRecord::Base.error_on_ignored_order_or_limit
- ActiveRecord::Base.error_on_ignored_order_or_limit = true
+ 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){}
+ PostWithDefaultScope.find_in_batches(error_on_ignore: false) { }
end
ensure
# Set back to default
- ActiveRecord::Base.error_on_ignored_order_or_limit = prev
+ 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_or_limit
- ActiveRecord::Base.error_on_ignored_order_or_limit = true
+ prev = ActiveRecord::Base.error_on_ignored_order
+ ActiveRecord::Base.error_on_ignored_order = true
assert_raise(ArgumentError) do
- PostWithDefaultScope.find_in_batches(){}
+ PostWithDefaultScope.find_in_batches() { }
end
ensure
# Set back to default
- ActiveRecord::Base.error_on_ignored_order_or_limit = prev
+ 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(){}
+ PostWithDefaultScope.find_in_batches() { }
end
end
@@ -211,12 +228,12 @@ 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, start: 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 = []
@@ -239,7 +256,7 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_return_an_enumerator
enum = nil
assert_no_queries do
- enum = Post.find_in_batches(:batch_size => 1)
+ enum = Post.find_in_batches(batch_size: 1)
end
assert_queries(4) do
enum.first(4) do |batch|
@@ -249,6 +266,28 @@ class EachTest < ActiveRecord::TestCase
end
end
+ 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)
@@ -274,7 +313,7 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_each_record_should_yield_record_if_block_is_given
assert_queries(6) do
Post.in_batches(of: 2).each_record do |post|
- assert post.title.present?
+ assert_predicate post.title, :present?
assert_kind_of Post, post
end
end
@@ -283,14 +322,14 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_each_record_should_return_enumerator_if_no_block_given
assert_queries(6) do
Post.in_batches(of: 2).each_record.with_index do |post, i|
- assert post.title.present?
+ assert_predicate post.title, :present?
assert_kind_of Post, post
end
end
end
def test_in_batches_each_record_should_be_ordered_by_id
- ids = Post.order('id ASC').pluck(: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
@@ -306,32 +345,32 @@ class EachTest < ActiveRecord::TestCase
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
+ 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 relation.loaded?
+ assert_not_predicate relation, :loaded?
end
Post.in_batches(of: 1, load: false) do |relation|
- assert_not relation.loaded?
+ assert_not_predicate relation, :loaded?
end
end
def test_in_batches_should_be_loaded
Post.in_batches(of: 1, load: true) do |relation|
- assert relation.loaded?
+ assert_predicate relation, :loaded?
end
end
def test_in_batches_if_not_loaded_executes_more_queries
assert_queries(@total + 1) do
Post.in_batches(of: 1, load: false) do |relation|
- assert_not relation.loaded?
+ assert_not_predicate relation, :loaded?
end
end
end
@@ -345,7 +384,7 @@ class EachTest < ActiveRecord::TestCase
end
def test_in_batches_should_start_from_the_start_option
- post = Post.order('id ASC').where('id >= ?', 2).first
+ 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
@@ -353,7 +392,7 @@ class EachTest < ActiveRecord::TestCase
end
def test_in_batches_should_end_at_the_finish_option
- post = Post.order('id DESC').where('id <= ?', 5).first
+ 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
@@ -372,7 +411,7 @@ class EachTest < ActiveRecord::TestCase
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
+ 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
@@ -381,7 +420,7 @@ class EachTest < ActiveRecord::TestCase
end
def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified
- not_a_post = "not a post"
+ not_a_post = +"not a post"
def not_a_post.id
raise StandardError.new("not_a_post had #id called on it")
end
@@ -391,7 +430,7 @@ class EachTest < ActiveRecord::TestCase
assert_kind_of ActiveRecord::Relation, relation
assert_kind_of Post, relation.first
- relation = [not_a_post] * relation.count
+ [not_a_post] * relation.count
end
end
end
@@ -407,12 +446,12 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_should_not_modify_passed_options
assert_nothing_raised do
- Post.in_batches({ of: 42, start: 1 }.freeze){}
+ 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')
+ nick_order_subscribers = Subscriber.order("nick asc")
start_nick = nick_order_subscribers.second.nick
subscribers = []
@@ -469,21 +508,156 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches
Post.update_all(author_id: 0)
person = Post.last
- person.update_attributes(author_id: 1)
+ person.update(author_id: 1)
Post.in_batches(of: 2) do |batch|
- batch.where('author_id >= 1').update_all('author_id = author_id + 1')
+ batch.where("author_id >= 1").update_all("author_id = author_id + 1")
end
assert_equal 2, person.reload.author_id # incremented only once
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, 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
+ 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
+
+ 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
+
+ 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 9eb5352150..58abdb47f7 100644
--- a/activerecord/test/cases/binary_test.rb
+++ b/activerecord/test/cases/binary_test.rb
@@ -1,26 +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
- assert_equal 'ã„ãŸã ãã¾ã™ï¼', name
+ assert_equal "ã„ãŸã ãã¾ã™ï¼", name
end
def test_load_save
@@ -28,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 fa924fe4cb..85685d1d00 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -1,50 +1,146 @@
-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/reply"
+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_statement_cache
+ @connection.clear_cache!
+
+ topics = Topic.where(id: 1)
+ assert_equal [1], topics.map(&:id)
+ assert_includes statement_cache, to_sql_key(topics.arel)
+
+ @connection.clear_cache!
+
+ assert_not_includes statement_cache, to_sql_key(topics.arel)
+ end
+
+ def test_statement_cache_with_query_cache
+ @connection.enable_query_cache!
+ @connection.clear_cache!
+
+ topics = Topic.where(id: 1)
+ assert_equal [1], topics.map(&:id)
+ assert_includes statement_cache, to_sql_key(topics.arel)
+ ensure
+ @connection.disable_query_cache!
+ end
+
+ def test_statement_cache_with_find
+ @connection.clear_cache!
+
+ assert_equal 1, Topic.find(1).id
+ assert_raises(RecordNotFound) { SillyReply.find(2) }
+
+ topic_sql = cached_statement(Topic, Topic.primary_key)
+ assert_includes statement_cache, to_sql_key(topic_sql)
+
+ e = assert_raise { cached_statement(SillyReply, SillyReply.primary_key) }
+ assert_equal "SillyReply has no cached statement by \"id\"", e.message
+
+ replies = SillyReply.where(id: 2).limit(1)
+ assert_includes statement_cache, to_sql_key(replies.arel)
+ end
+
+ def test_statement_cache_with_find_by
+ @connection.clear_cache!
+
+ assert_equal 1, Topic.find_by!(id: 1).id
+ assert_raises(RecordNotFound) { SillyReply.find_by!(id: 2) }
+
+ topic_sql = cached_statement(Topic, [:id])
+ assert_includes statement_cache, to_sql_key(topic_sql)
+
+ e = assert_raise { cached_statement(SillyReply, [:id]) }
+ assert_equal "SillyReply has no cached statement by [:id]", e.message
+
+ replies = SillyReply.where(id: 2).limit(1)
+ assert_includes statement_cache, to_sql_key(replies.arel)
+ end
+
+ def test_statement_cache_with_in_clause
+ @connection.clear_cache!
+
+ topics = Topic.where(id: [1, 3])
+ assert_equal [1, 3], topics.map(&:id)
+ assert_not_includes statement_cache, to_sql_key(topics.arel)
+ end
+
+ def test_statement_cache_with_sql_string_literal
+ @connection.clear_cache!
+
+ topics = Topic.where("topics.id = ?", 1)
+ assert_equal [1], topics.map(&:id)
+ assert_not_includes statement_cache, to_sql_key(topics.arel)
+ 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
+
+ def test_too_many_binds_with_query_cache
+ @connection.enable_query_cache!
+
+ bind_params_length = @connection.send(:bind_params_length)
+ topics = Topic.where(id: (1 .. bind_params_length + 1).to_a)
+ assert_equal Topic.count, topics.count
+
+ topics = Topic.where.not(id: (1 .. bind_params_length + 1).to_a)
+ assert_equal 0, topics.count
+ ensure
+ @connection.disable_query_cache!
+ end
- if ActiveRecord::Base.connection.supports_statement_cache? &&
- ActiveRecord::Base.connection.prepared_statements
def test_bind_from_join_in_subquery
- subquery = Author.joins(:thinking_posts).where(name: 'David')
- scope = Author.from(subquery, 'authors').where(id: 1)
+ 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 = Arel::Nodes::BindParam.new
+ 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]
@@ -53,37 +149,67 @@ 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'
- 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
+ assert message, "expected a message with binds"
+ end
- def debug str
- @debugs << str
- end
- }.new
+ def test_logs_binds_after_type_cast
+ binds = [Relation::QueryAttribute.new("id", "10", Type::Integer.new)]
+ assert_logs_binds(binds)
+ end
- logger.sql event
- assert_match([[@pk.name, 10]].inspect, logger.debugs.first)
+ def test_logs_legacy_binds_after_type_cast
+ binds = [[@pk, "10"]]
+ assert_logs_binds(binds)
end
+
+ private
+ def to_sql_key(arel)
+ sql = @connection.to_sql(arel)
+ @connection.respond_to?(:sql_key, true) ? @connection.send(:sql_key, sql) : sql
+ end
+
+ def cached_statement(klass, key)
+ cache = klass.send(:cached_find_by_statement, key) do
+ raise "#{klass} has no cached statement by #{key.inspect}"
+ end
+ cache.send(:query_builder).instance_variable_get(:@sql)
+ end
+
+ def statement_cache
+ @connection.instance_variable_get(:@statements).send(:cache)
+ end
+
+ def assert_logs_binds(binds)
+ payload = {
+ name: "SQL",
+ sql: "select * from topics where id = ?",
+ binds: binds,
+ type_casted_binds: @connection.send(: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..18824004d2
--- /dev/null
+++ b/activerecord/test/cases/boolean_test.rb
@@ -0,0 +1,52 @@
+# 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
+
+ def test_find_by_falsy_boolean_symbol
+ ActiveModel::Type::Boolean::FALSE_VALUES.each do |value|
+ b_false = Boolean.create!(value: value)
+
+ assert_not_predicate b_false, :value?
+ assert_equal b_false, Boolean.find_by(id: b_false.id, value: value.to_s.to_sym)
+ end
+ end
+end
diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb
index bb2829b3c1..c27eb8a65d 100644
--- a/activerecord/test/cases/cache_key_test.rb
+++ b/activerecord/test/cases/cache_key_test.rb
@@ -1,25 +1,131 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
class CacheKeyTest < ActiveRecord::TestCase
self.use_transactional_tests = false
- class CacheMe < ActiveRecord::Base; end
+ 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) { |t| t.timestamps }
+ @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 "test_cache_key_format_is_not_too_precise" do
+ 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.utc.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.utc.to_s(:usec)}", r2.cache_key_with_version
+ end
+
+ test "cache_version is the same when it comes from the DB or from the user" do
+ skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
+
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.find(record.id)
+ assert_not_called(record_from_db, :updated_at) do
+ record_from_db.cache_version
+ end
+
+ assert_equal record.cache_version, record_from_db.cache_version
+ end
+
+ test "cache_version does not truncate zeros when timestamp ends in zeros" do
+ skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
+
+ travel_to Time.now.beginning_of_day do
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.find(record.id)
+ assert_not_called(record_from_db, :updated_at) do
+ record_from_db.cache_version
+ end
+
+ assert_equal record.cache_version, record_from_db.cache_version
+ end
+ end
+
+ test "cache_version calls updated_at when the value is generated at create time" do
+ record = CacheMeWithVersion.create
+ assert_called(record, :updated_at) do
+ record.cache_version
+ end
+ end
+
+ test "cache_version does NOT call updated_at when value is from the database" do
+ skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
+
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.find(record.id)
+ assert_not_called(record_from_db, :updated_at) do
+ record_from_db.cache_version
+ end
+ end
+
+ test "cache_version does call updated_at when it is assigned via a Time object" do
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.find(record.id)
+ assert_called(record_from_db, :updated_at) do
+ record_from_db.updated_at = Time.now
+ record_from_db.cache_version
+ end
+ end
+
+ test "cache_version does call updated_at when it is assigned via a string" do
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.find(record.id)
+ assert_called(record_from_db, :updated_at) do
+ record_from_db.updated_at = Time.now.to_s
+ record_from_db.cache_version
+ end
+ end
+
+ test "cache_version does call updated_at when it is assigned via a hash" do
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.find(record.id)
+ assert_called(record_from_db, :updated_at) do
+ record_from_db.updated_at = { 1 => 2016, 2 => 11, 3 => 12, 4 => 1, 5 => 2, 6 => 3, 7 => 22 }
+ record_from_db.cache_version
+ end
+ end
+
+ test "updated_at on class but not on instance raises an error" do
+ record = CacheMeWithVersion.create
+ record_from_db = CacheMeWithVersion.where(id: record.id).select(:id).first
+ assert_raises(ActiveModel::MissingAttributeError) do
+ record_from_db.cache_version
+ end
+ end
end
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 8f2682c781..f1e35d6ab9 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -1,31 +1,28 @@
+# 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"
+require "support/stubs/strong_parameters"
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)
@@ -56,7 +53,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
@@ -91,25 +88,25 @@ class CalculationsTest < ActiveRecord::TestCase
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"]]
@@ -165,14 +162,14 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_limit_should_apply_before_count
- accounts = Account.limit(3).where('firm_id IS NOT NULL')
+ accounts = Account.limit(4)
assert_equal 3, accounts.count(:firm_id)
assert_equal 3, accounts.select(:firm_id).count
end
def test_limit_should_apply_before_count_arel_attribute
- accounts = Account.limit(3).where('firm_id IS NOT NULL')
+ accounts = Account.limit(4)
firm_id_attribute = Account.arel_table[:firm_id]
assert_equal 3, accounts.count(firm_id_attribute)
@@ -222,19 +219,92 @@ class CalculationsTest < ActiveRecord::TestCase
Account.select("credit_limit, firm_name").count
}
- assert_match %r{accounts}i, e.message
- assert_match "credit_limit, firm_name", e.message
+ assert_match %r{accounts}i, e.sql
+ assert_match "credit_limit, firm_name", e.sql
+ 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_joins_count_with_group_by
+ expected = { nil => 4, 1 => 1, 2 => 1, 4 => 1, 5 => 1, 7 => 1 }
+ assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.count(:author_id)
+ assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.select(:author_id).count
+ assert_equal expected, Post.left_joins(:comments).group(:post_id).count("DISTINCT posts.author_id")
+ assert_equal expected, Post.left_joins(:comments).group(:post_id).select("DISTINCT posts.author_id").count
+
+ expected = { nil => 6, 1 => 1, 2 => 1, 4 => 1, 5 => 1, 7 => 1 }
+ assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.count(:all)
+ assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.select(:author_id).count(:all)
+ 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]
@@ -248,52 +318,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]
@@ -307,8 +377,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
@@ -317,7 +387,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)]
@@ -327,17 +397,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
@@ -353,24 +423,26 @@ 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
assert_equal 6, Account.includes(:firm).distinct.count
assert_equal 4, Account.includes(:firm).distinct.select(:credit_limit).count
+ assert_equal 4, Account.includes(:firm).distinct.count("DISTINCT credit_limit")
+ assert_equal 4, Account.includes(:firm).distinct.count("DISTINCT(credit_limit)")
end
def test_should_not_perform_joined_include_by_default
@@ -380,7 +452,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
@@ -391,16 +463,34 @@ 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_should_count_manual_select_with_count_all
+ assert_equal 5, Account.select("DISTINCT accounts.firm_id").count(:all)
+ end
+
+ def test_should_count_with_manual_distinct_select_and_distinct
+ assert_equal 4, Account.select("DISTINCT accounts.firm_id").distinct(true).count
+ end
+
+ def test_should_count_manual_select_with_group_with_count_all
+ expected = { nil => 1, 1 => 1, 2 => 1, 6 => 2, 9 => 1 }
+ actual = Account.select("DISTINCT accounts.firm_id").group("accounts.firm_id").count(:all)
+ assert_equal expected, actual
+ end
+
+ def test_should_count_manual_with_count_all
+ assert_equal 6, Account.count(:all)
+ 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
@@ -420,10 +510,6 @@ class CalculationsTest < ActiveRecord::TestCase
def test_count_with_distinct
assert_equal 4, Account.select(:credit_limit).distinct.count
-
- assert_deprecated do
- assert_equal 4, Account.select(:credit_limit).uniq.count
- end
end
def test_count_with_aliased_attribute
@@ -435,8 +521,8 @@ 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
@@ -450,14 +536,16 @@ class CalculationsTest < ActiveRecord::TestCase
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 c.keys.include?(firm_id) }
+ [1, 6, 2, 9].each { |firm_id| assert_includes c.keys, firm_id }
end
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)
+ expected = { 1 => 2, 2 => 1, 4 => 5, 5 => 2, 7 => 1 }
+ assert_equal expected, Post.joins(:comments).group(:post_id).count
+ assert_equal expected, Post.joins(:comments).group("comments.post_id").count
+ assert_equal expected, Post.joins(:comments).group(:post_id).select("DISTINCT posts.author_id").count(:all)
end
def test_count_with_no_parameters_isnt_deprecated
@@ -477,14 +565,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
@@ -492,69 +583,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
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)
+ 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
@@ -567,22 +658,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
@@ -594,42 +700,43 @@ 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)])
- assert_equal [7], Company.joins(:contracts).pluck(:developer_id)
+ company = Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
+ metadata = company.contracts.first.metadata
+ assert_equal [metadata], Company.joins(:contracts).pluck(:metadata)
end
def test_pluck_with_selection_clause
- 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
@@ -638,11 +745,38 @@ 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_pluck_with_join
+ assert_equal [[2, 2], [4, 4]], Reply.includes(:topic).pluck(:id, :"topics.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
@@ -663,12 +797,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
@@ -676,21 +810,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
@@ -709,26 +843,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(id: 9999999999999999999).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(id: 9999999999999999999).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!
@@ -743,7 +900,7 @@ class CalculationsTest < ActiveRecord::TestCase
def test_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association
assert_nothing_raised do
- developer = Developer.create!(name: 'developer')
+ developer = Developer.create!(name: "developer")
developer.ratings.includes(comment: :post).where(posts: { id: 1 }).count
end
end
@@ -759,34 +916,73 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_having_with_strong_parameters
- protected_params = Class.new do
- attr_reader :permitted
- alias :permitted? :permitted
+ params = ProtectedParams.new(credit_limit: "50")
- def initialize(parameters)
- @parameters = parameters
- @permitted = false
- end
+ assert_raises(ActiveModel::ForbiddenAttributesError) do
+ Account.group(:id).having(params)
+ end
- def to_h
- @parameters
+ 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_count_with_block_and_column_name_raises_an_error
+ assert_raises(ArgumentError) do
+ Account.count(:firm_id) { true }
+ end
+ end
+
+ def test_sum_with_block_and_column_name_raises_an_error
+ assert_raises(ArgumentError) do
+ 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
- def permit!
- @permitted = true
- self
+ assert_queries(2) do
+ Account.all.skip_query_cache!.pluck(:credit_limit)
+ Account.all.skip_query_cache!.pluck(:credit_limit)
end
end
+ end
- params = protected_params.new(credit_limit: '50')
+ 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_raises(ActiveModel::ForbiddenAttributesError) do
- Account.group(:id).having(params)
+ 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
- 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
+ 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 4f70ae3a1d..4d6a112af5 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -1,29 +1,27 @@
+# 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
def callback_object(callback_method)
klass = Class.new
- klass.send(:define_method, callback_method) do |model|
+ klass.define_method(callback_method) do |model|
model.history << [callback_method, :object]
end
klass.new
@@ -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)
- ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method)) }
send(callback_method, callback_proc(callback_method))
send(callback_method, callback_object(callback_method))
send(callback_method) { |model| model.history << [callback_method, :block] }
@@ -44,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
index a2874438c1..483383257b 100644
--- a/activerecord/test/cases/collection_cache_key_test.rb
+++ b/activerecord/test/cases/collection_cache_key_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/computer"
require "models/developer"
@@ -11,44 +13,116 @@ module ActiveRecord
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)
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, Developer.collection_cache_key)
end
test "cache_key for relation" do
- developers = Developer.where(name: "David")
- last_developer_timestamp = developers.order(updated_at: :desc).first.updated_at
+ 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)
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
- /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/ =~ developers.cache_key
+ /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key
- assert_equal Digest::MD5.hexdigest(developers.to_sql), $1
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
assert_equal developers.count.to_s, $2
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
end
+ test "cache_key for relation with 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")
+ developers = Developer.where(name: "David")
assert_queries(1) { developers.cache_key }
- assert_queries(0) { 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_queries(0) { developers.cache_key }
+ 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")
+ 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)
+ assert_match(/\Adevelopers\/query-(\h+)-0\z/, developers.cache_key)
end
test "cache_key with custom timestamp column" do
@@ -64,7 +138,7 @@ module ActiveRecord
test "collection proxy provides a cache_key" do
developers = projects(:active_record).developers
- assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key)
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
end
test "cache_key for loaded collection with zero size" do
@@ -72,18 +146,30 @@ module ActiveRecord
posts = Post.includes(:comments)
empty_loaded_collection = posts.first.comments
- assert_match(/\Acomments\/query-(\h+)-0\Z/, empty_loaded_collection.cache_key)
+ 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)
+ 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)
+ 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 81162b7e98..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,86 +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?(:Mysql2Adapter)
- def test_should_set_default_for_mysql_binary_data_types
- type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)")
- binary_column = MySQL::Column.new("title", "a", type)
- assert_equal "a", binary_column.default
-
- type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary")
- varbinary_column = MySQL::Column.new("title", "a", type)
- assert_equal "a", varbinary_column.default
- end
-
- def test_should_be_empty_string_default_for_mysql_binary_data_types
- type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)")
- binary_column = MySQL::Column.new("title", "", type, false)
- assert_equal "", binary_column.default
-
- type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary")
- varbinary_column = MySQL::Column.new("title", "", type, false)
- assert_equal "", varbinary_column.default
- end
-
- def test_should_not_set_default_for_blob_and_text_data_types
- assert_raise ArgumentError do
- MySQL::Column.new("title", "a", SqlTypeMetadata.new(sql_type: "blob"))
- end
-
- text_type = MySQL::TypeMetadata.new(
- SqlTypeMetadata.new(type: :text))
- assert_raise ArgumentError do
- MySQL::Column.new("title", "Hello", text_type)
- end
-
- text_column = MySQL::Column.new("title", nil, text_type)
- assert_equal nil, text_column.default
-
- not_null_text_column = MySQL::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 = MySQL::Column.new("title", nil, binary_type)
- assert !blob_column.has_default?
-
- text_type = SqlTypeMetadata.new(type: :text)
- text_column = MySQL::Column.new("title", nil, text_type)
- assert !text_column.has_default?
- end
+ 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..6282759a10 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_establish_connection_using_two_level_configurations
+ config = { "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ @handler.establish_connection(:development)
+
+ assert_not_nil pool = @handler.retrieve_connection_pool("development")
+ assert_equal "db/primary.sqlite3", pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ end
+
+ def test_establish_connection_using_top_level_key_in_two_level_config
+ config = {
+ "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" },
+ "development_readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ @handler.establish_connection(:development_readonly)
+
+ assert_not_nil pool = @handler.retrieve_connection_pool("development_readonly")
+ assert_equal "db/readonly.sqlite3", pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
end
- def test_retrieve_connection_pool_uses_superclass_when_no_subclass_connection
- assert_not_nil @handler.retrieve_connection_pool(@subklass)
+ 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_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
+ assert @handler.retrieve_connection(@spec_name)
+ end
- @handler.remove_connection @subklass
- assert_same @pool, @handler.retrieve_connection_pool(@subklass)
+ 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
+ assert_not_nil @handler.retrieve_connection_pool(@spec_name)
+ end
+
+ 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,76 @@ 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
+
+ def test_default_handlers_are_writing_and_reading
+ assert_equal :writing, ActiveRecord::Base.writing_role
+ assert_equal :reading, ActiveRecord::Base.reading_role
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
new file mode 100644
index 0000000000..36591097b6
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
@@ -0,0 +1,391 @@
+# 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]
+ assert_equal :reading, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :reading)
+ assert_not ActiveRecord::Base.connected_to?(role: :writing)
+ 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
+ assert_equal :writing, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ assert_not ActiveRecord::Base.connected_to?(role: :reading)
+ end
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ ENV["RAILS_ENV"] = previous_env
+ end
+
+ def test_establish_connection_using_3_levels_config_with_non_default_handlers
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+
+ config = {
+ "default_env" => {
+ "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" },
+ "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }
+ }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.connects_to(database: { default: :primary, readonly: :readonly })
+
+ assert_not_nil pool = ActiveRecord::Base.connection_handlers[:default].retrieve_connection_pool("primary")
+ assert_equal "db/primary.sqlite3", pool.spec.config[:database]
+
+ assert_not_nil pool = ActiveRecord::Base.connection_handlers[:readonly].retrieve_connection_pool("primary")
+ assert_equal "db/readonly.sqlite3", pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ ENV["RAILS_ENV"] = previous_env
+ end
+
+ def test_switching_connections_with_database_url
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+ previous_url, ENV["DATABASE_URL"] = ENV["DATABASE_URL"], "postgres://localhost/foo"
+
+ ActiveRecord::Base.connected_to(database: { writing: "postgres://localhost/bar" }) do
+ assert_equal :writing, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+
+ 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
+ assert_equal :writing, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+
+ 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
+ assert_equal :readonly, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :readonly)
+
+ 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]
+ assert_equal :writing, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :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
+
+ def test_connection_handlers_are_per_thread_and_not_per_fiber
+ original_handlers = ActiveRecord::Base.connection_handlers
+
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler, reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new }
+
+ reading_handler = ActiveRecord::Base.connection_handlers[:reading]
+
+ reading = ActiveRecord::Base.with_handler(:reading) do
+ Person.connection_handler
+ end
+
+ assert_not_equal reading, ActiveRecord::Base.connection_handler
+ assert_equal reading, reading_handler
+ ensure
+ ActiveRecord::Base.connection_handlers = original_handlers
+ end
+
+ def test_connection_handlers_swapping_connections_in_fiber
+ original_handlers = ActiveRecord::Base.connection_handlers
+
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler, reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new }
+
+ reading_handler = ActiveRecord::Base.connection_handlers[:reading]
+
+ enum = Enumerator.new do |r|
+ r << ActiveRecord::Base.connection_handler
+ end
+
+ reading = ActiveRecord::Base.with_handler(:reading) do
+ enum.next
+ end
+
+ assert_equal reading, reading_handler
+ ensure
+ ActiveRecord::Base.connection_handlers = original_handlers
+ end
+
+ def test_calling_connected_to_on_a_non_existent_handler_raises
+ error = assert_raises ActiveRecord::ConnectionNotEstablished do
+ ActiveRecord::Base.connected_to(role: :reading) do
+ Person.first
+ end
+ end
+
+ assert_equal "No connection pool with 'primary' found for the 'reading' role.", error.message
+ end
+
+ def test_default_handlers_are_writing_and_reading
+ assert_equal :writing, ActiveRecord::Base.writing_role
+ assert_equal :reading, ActiveRecord::Base.reading_role
+ end
+
+ def test_an_application_can_change_the_default_handlers
+ old_writing = ActiveRecord::Base.writing_role
+ old_reading = ActiveRecord::Base.reading_role
+ ActiveRecord::Base.writing_role = :default
+ ActiveRecord::Base.reading_role = :readonly
+
+ assert_equal :default, ActiveRecord::Base.writing_role
+ assert_equal :readonly, ActiveRecord::Base.reading_role
+ ensure
+ ActiveRecord::Base.writing_role = old_writing
+ ActiveRecord::Base.reading_role = old_reading
+ end
+ end
+ end
+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..515bf5df06 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,83 @@ 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_nil_database_url_and_current_env
+ ENV["RAILS_ENV"] = "foo"
+ config = { "foo" => { "adapter" => "postgres", "url" => ENV["DATABASE_URL"] } }
+ actual = resolve_spec(:foo, config)
+ expected = { "adapter" => "postgres", "url" => nil, "name" => "foo" }
assert_equal expected, actual
end
def test_resolver_with_database_uri_and_current_env_symbol_key_and_rack_env
- ENV['DATABASE_URL'] = "postgres://localhost/foo"
- ENV['RACK_ENV'] = "foo"
+ 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_multiple_envs
+ ENV["DATABASE_URL"] = "postgres://localhost"
+ ENV["RAILS_ENV"] = "test"
+
+ config = { "production" => { "adapter" => "postgresql", "database" => "foo_prod" }, "test" => { "adapter" => "postgresql", "database" => "foo_test" } }
+ actual = resolve_spec(:test, config)
+ expected = { "adapter" => "postgresql", "database" => "foo_test", "host" => "localhost", "name" => "test" }
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 +105,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 +157,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 +165,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 +185,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 +207,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 +231,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 +245,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 +260,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 f2b1d9e4e7..38331aa641 100644
--- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
+++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
@@ -1,69 +1,82 @@
+# frozen_string_literal: true
+
require "cases/helper"
+require "support/connection_helper"
if current_adapter?(:Mysql2Adapter)
-module ActiveRecord
- module ConnectionAdapters
- class MysqlTypeLookupTest < ActiveRecord::TestCase
- setup do
- @connection = ActiveRecord::Base.connection
- end
+ 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_set_type_with_value_matching_other_type
- assert_lookup_type :string, "SET('unicode', '8bit', 'none', 'time')"
- 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_enum_type_with_value_matching_other_type
- assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')"
- 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, "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')"
+ assert_lookup_type :string, "set ('one', 'two', 'three')"
+ assert_lookup_type :string, "SET ('one', 'two', 'three')"
+ end
- def test_binary_types
- assert_lookup_type :binary, 'bit'
- assert_lookup_type :binary, 'BIT'
- end
+ def test_set_type_with_value_matching_other_type
+ assert_lookup_type :string, "SET('unicode', '8bit', 'none', 'time')"
+ 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_enum_type_with_value_matching_other_type
+ assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')"
end
- end
- private
+ def test_binary_types
+ assert_lookup_type :binary, "bit"
+ assert_lookup_type :binary, "BIT"
+ end
- def assert_lookup_type(type, lookup)
- cast_type = @connection.type_map.lookup(lookup)
- assert_equal type, cast_type.type
- 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 emulate_booleans(value)
- old_emulate_booleans = @connection.emulate_booleans
- change_emulate_booleans(value)
- yield
- ensure
- change_emulate_booleans(old_emulate_booleans)
- end
+ private
+
+ def assert_lookup_type(type, lookup)
+ cast_type = @connection.send(:type_map).lookup(lookup)
+ assert_equal type, cast_type.type
+ end
- def change_emulate_booleans(value)
- @connection.emulate_booleans = value
- @connection.clear_cache!
+ 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 db832fe55d..5113548091 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -1,36 +1,85 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class SchemaCacheTest < ActiveRecord::TestCase
def setup
- connection = ActiveRecord::Base.connection
- @cache = SchemaCache.new connection
+ @connection = ActiveRecord::Base.connection
+ @cache = SchemaCache.new @connection
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")
+ @cache.indexes("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")
+ assert_equal 1, new_cache.indexes("posts").size
+ end
+ end
+
+ def test_yaml_loads_5_1_dump
+ @cache = YAML.load(File.read(schema_dump_path))
+
+ 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_yaml_loads_5_1_dump_without_indexes_still_queries_for_indexes
+ @cache = YAML.load(File.read(schema_dump_path))
+
+ # Simulate assignment in railtie after loading the cache.
+ old_cache, @connection.schema_cache = @connection.schema_cache, @cache
+
+ assert_queries :any, ignore_none: true do
+ assert_equal 1, @cache.indexes("posts").size
+ end
+ ensure
+ @connection.schema_cache = old_cache
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_caches_indexes
+ indexes = @cache.indexes("posts")
+ assert_equal indexes, @cache.indexes("posts")
end
def test_clearing
- @cache.columns('posts')
- @cache.columns_hash('posts')
- @cache.data_sources('posts')
- @cache.primary_keys('posts')
+ @cache.columns("posts")
+ @cache.columns_hash("posts")
+ @cache.data_sources("posts")
+ @cache.primary_keys("posts")
+ @cache.indexes("posts")
@cache.clear!
@@ -38,24 +87,52 @@ module ActiveRecord
end
def test_dump_and_load
- @cache.columns('posts')
- @cache.columns_hash('posts')
- @cache.data_sources('posts')
- @cache.primary_keys('posts')
+ @cache.columns("posts")
+ @cache.columns_hash("posts")
+ @cache.data_sources("posts")
+ @cache.primary_keys("posts")
+ @cache.indexes("posts")
@cache = Marshal.load(Marshal.dump(@cache))
- assert_equal 11, @cache.columns('posts').size
- assert_equal 11, @cache.columns_hash('posts').size
- assert @cache.data_sources('posts')
- assert_equal 'id', @cache.primary_keys('posts')
+ assert_no_queries do
+ assert_equal 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")
+ assert_equal 1, @cache.indexes("posts").size
+ 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
+
+ test "#columns_hash? is populated by #columns_hash" do
+ assert_not @cache.columns_hash?("posts")
+
+ @cache.columns_hash("posts")
+
+ assert @cache.columns_hash?("posts")
end
- def test_table_methods_deprecation
- assert_deprecated { assert @cache.table_exists?('posts') }
- assert_deprecated { assert @cache.tables('posts') }
- assert_deprecated { @cache.clear_table_cache!('posts') }
+ test "#columns_hash? is not populated by #data_source_exists?" do
+ assert_not @cache.columns_hash?("posts")
+
+ @cache.data_source_exists?("posts")
+
+ assert_not @cache.columns_hash?("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 c4c2c69d1c..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,7 +16,7 @@ module ActiveRecord
def call(env)
@calls << env
- [200, {}, ['hi mom']]
+ [200, {}, ["hi mom"]]
end
end
@@ -23,7 +27,7 @@ module ActiveRecord
# make sure we have an active connection
assert ActiveRecord::Base.connection
- assert ActiveRecord::Base.connection_handler.active_connections?
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
def test_app_delegation
@@ -37,20 +41,20 @@ module ActiveRecord
_, _, 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
- executor.wrap do
+ def test_active_connections_are_not_cleared_on_body_close_during_transaction
+ ActiveRecord::Base.transaction do
_, _, body = @management.call(@env)
body.close
- assert ActiveRecord::Base.connection_handler.active_connections?
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
end
@@ -58,22 +62,22 @@ module ActiveRecord
app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
explosive = middleware(app)
assert_raises(NotImplementedError) { explosive.call(@env) }
- assert !ActiveRecord::Base.connection_handler.active_connections?
+ assert_not_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
- def test_connections_not_closed_if_exception_and_test
- executor.wrap do
- app = Class.new(App) { def call(env); raise; end }.new
+ 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 ActiveRecord::Base.connection_handler.active_connections?
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
end
test "doesn't clear active connections when running in a test case" do
executor.wrap do
@management.call(@env)
- assert ActiveRecord::Base.connection_handler.active_connections?
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
end
@@ -81,15 +85,15 @@ module ActiveRecord
body = Class.new(String) { def to_path; "/path"; end }.new
app = lambda { |_| [200, {}, body] }
response_body = middleware(app).call(@env)[2]
- assert response_body.respond_to?(:to_path)
+ assert_respond_to response_body, :to_path
assert_equal "/path", response_body.to_path
end
test "doesn't mutate the original response" do
- original_response = [200, {}, 'hi']
+ original_response = [200, {}, "hi"]
app = lambda { |_| original_response }
middleware(app).call(@env)[2]
- assert_equal 'hi', original_response.last
+ assert_equal "hi", original_response.last
end
private
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index efa3e0455e..a15ad9a45b 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 'concurrent/atomic/count_down_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 }
@@ -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
@@ -384,8 +542,8 @@ module ActiveRecord
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.count_down
@@ -394,6 +552,7 @@ module ActiveRecord
end
def test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns
+ Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception
@pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout
[:disconnect, :clear_reloadable_connections].each do |group_action_method|
@pool.with_connection do |connection|
@@ -402,37 +561,40 @@ 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
[:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method|
- begin
- thread = timed_join_result = nil
- @pool.with_connection do |connection|
- thread = Thread.new { @pool.send(group_action_method) }
-
- # give the other `thread` some time to get stuck in `group_action_method`
- timed_join_result = thread.join(0.3)
- # thread.join # => `nil` means the other thread hasn't finished running and is still waiting for us to
- # release our connection
- 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?
- end
- ensure
- thread.join if thread && !timed_join_result # clean up the other thread
+ thread = timed_join_result = nil
+ @pool.with_connection do |connection|
+ thread = Thread.new { @pool.send(group_action_method) }
+
+ # give the other `thread` some time to get stuck in `group_action_method`
+ timed_join_result = thread.join(0.3)
+ # thread.join # => `nil` means the other thread hasn't finished running and is still waiting for us to
+ # release our connection
+ assert_nil timed_join_result
+
+ # assert that since this is within default timeout our connection hasn't been forcefully taken away from us
+ assert_predicate @pool, :active_connection?
end
+ ensure
+ thread.join if thread && !timed_join_result # clean up the other thread
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 +603,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 = Concurrent::CountDownLatch.new
-
- # create a first_thread and let it get into the FIFO queue first
- first_thread = Thread.new do
- pool.with_connection { second_thread_done.wait }
- end
+ 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.count_down
- end
-
- # wait for second_thread to get in queue
- Thread.pass until pool.num_waiting_in_queue == 2
-
- # return the only available connection
- pool.checkin(conn)
-
- # if the second_thread is not able to preempt the first_thread,
- # they will temporarily (until either of them timeouts with ConnectionTimeoutError)
- # deadlock and a join(2) timeout will be reached
- failed = true unless second_thread.join(2)
-
- #--- post test clean up start
- second_thread_done.count_down if failed
-
- # after `pool.disconnect()` the first thread will be left stuck in queue, no need to wait for
- # it to timeout with ConnectionTimeoutError
- if (group_action_method == :disconnect || group_action_method == :disconnect!) && pool.num_waiting_in_queue > 0
- pool.with_connection {} # create a new connection in case there are threads still stuck in a queue
+ begin
+ # create a first_thread and let it get into the FIFO queue first
+ first_thread = Thread.new do
+ pool.with_connection { second_thread_done.wait }
+ end
+
+ # wait for first_thread to get in queue
+ Thread.pass until pool.num_waiting_in_queue == 1
+
+ # create a different, later thread, that will attempt to do a "group action",
+ # but because of the group action semantics it should be able to preempt the
+ # first_thread when a connection is made available
+ second_thread = Thread.new do
+ pool.send(group_action_method)
+ second_thread_done.set
+ end
+
+ # wait for second_thread to get in queue
+ Thread.pass until pool.num_waiting_in_queue == 2
+
+ # return the only available connection
+ pool.checkin(conn)
+
+ # if the second_thread is not able to preempt the first_thread,
+ # they will temporarily (until either of them timeouts with ConnectionTimeoutError)
+ # deadlock and a join(2) timeout will be reached
+ assert second_thread.join(2), "#{group_action_method} is not able to preempt other waiting threads"
+
+ ensure
+ # post test clean up
+ failed = !second_thread_done.set?
+
+ if failed
+ second_thread_done.set
+
+ 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 +659,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 +668,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 358b6ad537..72be14f507 100644
--- a/activerecord/test/cases/connection_specification/resolver_test.rb
+++ b/activerecord/test/cases/connection_specification/resolver_test.rb
@@ -1,56 +1,65 @@
+# 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",
@@ -58,13 +67,13 @@ module ActiveRecord
end
def test_url_missing_scheme
- spec = resolve 'foo'
+ 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",
@@ -73,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,
@@ -82,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 66b4c3f1ff..cc4f86a0fb 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -1,30 +1,32 @@
-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'
+# 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
@@ -32,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
@@ -48,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
@@ -58,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
@@ -103,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
@@ -116,35 +118,35 @@ 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
test "update other counters on parent destroy" do
david, joanna = dog_lovers(:david, :joanna)
- joanna = joanna # squelch a warning
+ _ = joanna # squelch a warning
- assert_difference 'joanna.reload.dogs_count', -1 do
+ assert_difference "joanna.reload.dogs_count", -1 do
david.destroy
end
end
@@ -157,12 +159,12 @@ class CounterCacheTest < ActiveRecord::TestCase
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
@@ -174,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
@@ -203,12 +205,163 @@ class CounterCacheTest < ActiveRecord::TestCase
test "update counters in a polymorphic relationship" do
aircraft = Aircraft.create!
- assert_difference 'aircraft.reload.wheels_count' do
+ assert_difference "aircraft.reload.wheels_count" do
aircraft.wheels << Wheel.create!
end
- assert_difference 'aircraft.reload.wheels_count', -1 do
+ 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 26d015bf71..f52b26e9ec 100644
--- a/activerecord/test/cases/custom_locking_test.rb
+++ b/activerecord/test/cases/custom_locking_test.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/person'
+require "models/person"
module ActiveRecord
class CustomLockingTest < ActiveRecord::TestCase
@@ -7,9 +9,9 @@ module ActiveRecord
def test_custom_lock
if current_adapter?(:Mysql2Adapter)
- assert_match 'SHARE MODE', Person.lock('LOCK IN SHARE MODE').to_sql
+ 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_configurations_test.rb b/activerecord/test/cases/database_configurations_test.rb
new file mode 100644
index 0000000000..ed8151f01a
--- /dev/null
+++ b/activerecord/test/cases/database_configurations_test.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class DatabaseConfigurationsTest < ActiveRecord::TestCase
+ unless in_memory_db?
+ def test_empty_returns_true_when_db_configs_are_empty
+ old_config = ActiveRecord::Base.configurations
+ config = {}
+
+ ActiveRecord::Base.configurations = config
+
+ assert_predicate ActiveRecord::Base.configurations, :empty?
+ assert_predicate ActiveRecord::Base.configurations, :blank?
+ ensure
+ ActiveRecord::Base.configurations = old_config
+ ActiveRecord::Base.establish_connection :arunit
+ end
+ end
+
+ def test_configs_for_getter_with_env_name
+ configs = ActiveRecord::Base.configurations.configs_for(env_name: "arunit")
+
+ assert_equal 1, configs.size
+ assert_equal ["arunit"], configs.map(&:env_name)
+ end
+
+ def test_configs_for_getter_with_env_and_spec_name
+ config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", spec_name: "primary")
+
+ assert_equal "arunit", config.env_name
+ assert_equal "primary", config.spec_name
+ end
+
+ def test_default_hash_returns_config_hash_from_default_env
+ original_rails_env = ENV["RAILS_ENV"]
+ ENV["RAILS_ENV"] = "arunit"
+
+ assert_equal ActiveRecord::Base.configurations.configs_for(env_name: "arunit", spec_name: "primary").config, ActiveRecord::Base.configurations.default_hash
+ ensure
+ ENV["RAILS_ENV"] = original_rails_env
+ end
+
+ def test_find_db_config_returns_a_db_config_object_for_the_given_env
+ config = ActiveRecord::Base.configurations.find_db_config("arunit2")
+
+ assert_equal "arunit2", config.env_name
+ assert_equal "primary", config.spec_name
+ end
+
+ def test_to_h_turns_db_config_object_back_into_a_hash
+ configs = ActiveRecord::Base.configurations
+ assert_equal "ActiveRecord::DatabaseConfigurations", configs.class.name
+ assert_equal "Hash", configs.to_h.class.name
+ assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements"], ActiveRecord::Base.configurations.to_h.keys.sort
+ end
+end
+
+class LegacyDatabaseConfigurationsTest < ActiveRecord::TestCase
+ unless in_memory_db?
+ def test_setting_configurations_hash
+ old_config = ActiveRecord::Base.configurations
+ config = { "adapter" => "sqlite3" }
+
+ assert_deprecated do
+ ActiveRecord::Base.configurations["readonly"] = config
+ end
+
+ assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements", "readonly"], ActiveRecord::Base.configurations.configs_for.map(&:env_name).sort
+ ensure
+ ActiveRecord::Base.configurations = old_config
+ ActiveRecord::Base.establish_connection :arunit
+ end
+ end
+
+ 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
+
+ def test_unsupported_method_raises
+ assert_raises NotImplementedError do
+ ActiveRecord::Base.configurations.select { |a| a == "foo" }
+ end
+ end
+end
diff --git a/activerecord/test/cases/database_selector_test.rb b/activerecord/test/cases/database_selector_test.rb
new file mode 100644
index 0000000000..fd02d2acb4
--- /dev/null
+++ b/activerecord/test/cases/database_selector_test.rb
@@ -0,0 +1,166 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/person"
+require "action_dispatch"
+
+module ActiveRecord
+ class DatabaseSelectorTest < ActiveRecord::TestCase
+ setup do
+ @session_store = {}
+ @session = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store)
+ end
+
+ teardown do
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
+
+ def test_empty_session
+ assert_equal Time.at(0), @session.last_write_timestamp
+ end
+
+ def test_writing_the_session_timestamps
+ assert @session.update_last_write_timestamp
+
+ session2 = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store)
+ assert_equal @session.last_write_timestamp, session2.last_write_timestamp
+ end
+
+ def test_writing_session_time_changes
+ assert @session.update_last_write_timestamp
+
+ before = @session.last_write_timestamp
+ sleep(0.1)
+
+ assert @session.update_last_write_timestamp
+ assert_not_equal before, @session.last_write_timestamp
+ end
+
+ def test_read_from_replicas
+ @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now - 5.seconds)
+
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ called = false
+ resolver.read do
+ called = true
+ assert ActiveRecord::Base.connected_to?(role: :reading)
+ end
+ assert called
+ end
+
+ def test_read_from_primary
+ @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now)
+
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ called = false
+ resolver.read do
+ called = true
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ end
+ assert called
+ end
+
+ def test_write_to_primary
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ # Session should start empty
+ assert_nil @session_store[:last_write]
+
+ called = false
+ resolver.write do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ called = true
+ end
+ assert called
+
+ # and be populated by the last write time
+ assert @session_store[:last_write]
+ end
+
+ def test_write_to_primary_with_exception
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ # Session should start empty
+ assert_nil @session_store[:last_write]
+
+ called = false
+ assert_raises(ActiveRecord::RecordNotFound) do
+ resolver.write do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ called = true
+ raise ActiveRecord::RecordNotFound
+ end
+ end
+ assert called
+
+ # and be populated by the last write time
+ assert @session_store[:last_write]
+ end
+
+ def test_read_from_primary_with_options
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 5.seconds)
+
+ # Session should start empty
+ assert_nil @session_store[:last_write]
+
+ called = false
+ resolver.write do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ called = true
+ end
+ assert called
+
+ # and be populated by the last write time
+ assert @session_store[:last_write]
+
+ read = false
+ resolver.read do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ read = true
+ end
+ assert read
+ end
+
+ def test_read_from_replica_with_no_delay
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 0.seconds)
+
+ # Session should start empty
+ assert_nil @session_store[:last_write]
+
+ called = false
+ resolver.write do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ called = true
+ end
+ assert called
+
+ # and be populated by the last write time
+ assert @session_store[:last_write]
+
+ read = false
+ resolver.read do
+ assert ActiveRecord::Base.connected_to?(role: :reading)
+ read = true
+ end
+ assert read
+ end
+
+ def test_the_middleware_chooses_writing_role_with_POST_request
+ middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env|
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ [200, {}, ["body"]]
+ })
+ assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "POST")
+ end
+
+ def test_the_middleware_chooses_reading_role_with_GET_request
+ middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env|
+ assert ActiveRecord::Base.connected_to?(role: :reading)
+ [200, {}, ["body"]]
+ })
+ assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "GET")
+ end
+ end
+end
diff --git a/activerecord/test/cases/database_statements_test.rb b/activerecord/test/cases/database_statements_test.rb
index 3169408ac0..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,6 +7,13 @@ class DatabaseStatementsTest < ActiveRecord::TestCase
@connection = ActiveRecord::Base.connection
end
+ 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
+ end
+
def test_insert_should_return_the_inserted_id
assert_not_nil return_the_inserted_id(method: :insert)
end
@@ -13,22 +22,16 @@ class DatabaseStatementsTest < ActiveRecord::TestCase
assert_not_nil return_the_inserted_id(method: :create)
end
- def test_insert_update_delete_sql_is_deprecated
- assert_deprecated { @connection.insert_sql("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") }
- assert_deprecated { @connection.update_sql("UPDATE accounts SET credit_limit = 6000 WHERE firm_id = 42") }
- assert_deprecated { @connection.delete_sql("DELETE FROM accounts WHERE firm_id = 42") }
- end
-
private
- def return_the_inserted_id(method:)
- # 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)")
+ 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
end
diff --git a/activerecord/test/cases/date_test.rb b/activerecord/test/cases/date_test.rb
new file mode 100644
index 0000000000..2475d4a34b
--- /dev/null
+++ b/activerecord/test/cases/date_test.rb
@@ -0,0 +1,36 @@
+# 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)
+ assert_equal(topic.last_read, Date.new(*date_src))
+ 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)
+ 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
diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb
index e996d142a2..9d1af9362d 100644
--- a/activerecord/test/cases/date_time_precision_test.rb
+++ b/activerecord/test/cases/date_time_precision_test.rb
@@ -1,88 +1,127 @@
-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
- Foo.reset_column_information
- 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, Foo.columns_hash['created_at'].precision
- assert_equal 5, Foo.columns_hash['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, Foo.columns_hash['created_at'].precision
- assert_equal 4, Foo.columns_hash['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 Foo.columns_hash['created_at'].limit
- assert_nil Foo.columns_hash['updated_at'].limit
- end
- def test_invalid_datetime_precision_raises_error
- assert_raises ActiveRecord::ActiveRecordError do
+ unless current_adapter?(:Mysql2Adapter)
+ def test_no_datetime_precision_isnt_truncated_on_assignment
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :created_at, :datetime
+ @connection.add_column :foos, :updated_at, :datetime, precision: 6
+
+ time = ::Time.now.change(nsec: 123)
+ foo = Foo.new(created_at: time, updated_at: time)
+
+ assert_equal 123, foo.created_at.nsec
+ assert_equal 0, foo.updated_at.nsec
+
+ foo.save!
+ foo.reload
+
+ assert_equal 0, foo.created_at.nsec
+ assert_equal 0, foo.updated_at.nsec
+ end
+ end
+
+ 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_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_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
- 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_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
+
+ 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
-end
+ 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
+ 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 067513e24c..5d02e59ef6 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -1,13 +1,15 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'support/schema_dumping_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
%w(id name course_id).each do |name|
column = Entrant.columns_hash[name]
- assert !column.null, "#{name} column should be NOT NULL"
+ assert_not column.null, "#{name} column should be NOT NULL"
assert_not column.default, "#{name} column should be DEFAULT 'nil'"
end
end
@@ -51,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,9 +89,14 @@ if current_adapter?(:PostgreSQLAdapter)
test "schema dump includes default expression" do
output = dump_table_schema("defaults")
- assert_match %r/t\.date\s+"modified_date",\s+default: -> { "\('now'::text\)::date" }/, output
+ 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",\s+default: -> { "now\(\)" }/, output
assert_match %r/t\.datetime\s+"modified_time_function",\s+default: -> { "now\(\)" }/, output
end
end
@@ -99,10 +106,37 @@ if current_adapter?(:Mysql2Adapter)
class MysqlDefaultExpressionTest < ActiveRecord::TestCase
include SchemaDumpingHelper
- if ActiveRecord::Base.connection.version >= '5.6.0'
+ 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+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP" }/, output
+ 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
@@ -127,92 +161,66 @@ if current_adapter?(:Mysql2Adapter)
ActiveRecord::Base.establish_connection connection
end
- # MySQL cannot have defaults on text/blob columns. It reports the
- # default value as null.
+ # 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.)
#
- # 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.
+ # 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.
#
- # Therefore, in non-strict mode, we want column.default to report
- # an empty string as its default, to be consistent with that.
- #
- # 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
- assert_equal nil, 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 cd1967c373..a2a501a794 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.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 + 1
+
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,30 +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
travel(1.second) do
- topic.content[:hello] = 'world'
+ 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
@@ -496,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
@@ -512,11 +506,11 @@ 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
@@ -530,11 +524,11 @@ class DirtyTest < ActiveRecord::TestCase
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!")
@@ -544,11 +538,11 @@ class DirtyTest < ActiveRecord::TestCase
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)
@@ -556,11 +550,11 @@ class DirtyTest < ActiveRecord::TestCase
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)
@@ -568,63 +562,60 @@ class DirtyTest < ActiveRecord::TestCase
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')
- ensure
- travel_back
+ 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
- if ActiveRecord::Base.connection.supports_migrations?
- class Testings < ActiveRecord::Base; end
- def test_field_named_field
- ActiveRecord::Base.connection.create_table :testings do |t|
- t.string :field
- end
- assert_nothing_raised do
- Testings.new.attributes
- end
- ensure
- ActiveRecord::Base.connection.drop_table :testings rescue nil
+ class Testings < ActiveRecord::Base; end
+ def test_field_named_field
+ ActiveRecord::Base.connection.create_table :testings do |t|
+ t.string :field
end
+ assert_nothing_raised do
+ Testings.new.attributes
+ end
+ ensure
+ ActiveRecord::Base.connection.drop_table :testings rescue nil
+ ActiveRecord::Base.clear_cache!
end
def test_datetime_attribute_can_be_updated_with_fractional_seconds
skip "Fractional seconds are not supported" unless subsecond_precision_supported?
- in_time_zone 'Paris' do
+ 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
@@ -642,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!"]
}
@@ -650,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
@@ -667,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
@@ -689,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!"
@@ -734,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?
@@ -744,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 babacd1ee9..ae0ce195b3 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,6 +39,14 @@ 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 negative scope" do
+ assert Book.not_published.exclude?(@book)
+ assert Book.not_proposed.include?(@book)
end
test "find via where with values" do
@@ -54,6 +67,7 @@ class EnumTest < ActiveRecord::TestCase
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
@@ -63,49 +77,50 @@ class EnumTest < ActiveRecord::TestCase
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?
- assert_not Book.written.build.proposed?
+ assert_predicate Book.written.build, :written?
+ assert_not_predicate Book.written.build, :proposed?
end
test "build from where" do
- assert Book.where(status: Book.statuses[:written]).build.written?
- assert_not Book.where(status: Book.statuses[:written]).build.proposed?
- assert Book.where(status: :written).build.written?
- assert_not Book.where(status: :written).build.proposed?
- assert Book.where(status: "written").build.written?
- assert_not Book.where(status: "written").build.proposed?
+ assert_predicate Book.where(status: Book.statuses[:written]).build, :written?
+ assert_not_predicate Book.where(status: Book.statuses[:written]).build, :proposed?
+ assert_predicate Book.where(status: :written).build, :written?
+ assert_not_predicate Book.where(status: :written).build, :proposed?
+ assert_predicate Book.where(status: "written").build, :written?
+ assert_not_predicate Book.where(status: "written").build, :proposed?
end
test "update by declaration" do
@book.written!
- assert @book.written?
+ assert_predicate @book, :written?
@book.in_english!
- assert @book.in_english?
+ assert_predicate @book, :in_english?
@book.author_visibility_visible!
- assert @book.author_visibility_visible?
+ assert_predicate @book, :author_visibility_visible?
end
test "update by setter" do
@book.update! status: :written
- assert @book.written?
+ assert_predicate @book, :written?
end
test "enum methods are overwritable" do
assert_equal "do publish work...", @book.published!
- assert @book.published?
+ assert_predicate @book, :published?
end
test "direct assignment" do
@book.status = :written
- assert @book.written?
+ assert_predicate @book, :written?
end
test "assign string value" do
@book.status = "written"
- assert @book.written?
+ assert_predicate @book, :written?
end
test "enum changed attributes" do
@@ -122,8 +137,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 +160,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 +178,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 +231,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 +247,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 +342,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 +382,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 +420,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 +436,25 @@ 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 "attempting to modify enum raises error" do
+ e = assert_raises(RuntimeError) do
+ Book.statuses["bad_enum"] = 40
+ end
+
+ assert_match(/can't modify frozen/, e.message)
+
+ e = assert_raises(RuntimeError) do
+ Book.statuses.delete("published")
+ end
+
+ assert_match(/can't modify frozen/, e.message)
end
test "declare multiple enums at a time" do
@@ -387,23 +465,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
@@ -414,11 +545,24 @@ class EnumTest < ActiveRecord::TestCase
test "uses default value from database on initialization" do
book = Book.new
- assert book.proposed?
+ assert_predicate book, :proposed?
end
test "uses default value from database on initialization when using custom mapping" do
book = Book.new
- assert book.hard?
+ assert_predicate book, :hard?
+ end
+
+ test "data type of Enum type" do
+ assert_equal :integer, Book.type_for_attribute("status").type
+ end
+
+ test "scopes can be disabled" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [:proposed, :written], _scopes: false
+ end
+
+ assert_raises(NoMethodError) { klass.proposed }
end
end
diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb
index 0711a372f2..0d2be944b5 100644
--- a/activerecord/test/cases/errors_test.rb
+++ b/activerecord/test/cases/errors_test.rb
@@ -1,16 +1,16 @@
-require_relative "../cases/helper"
+# 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.each do |error_klass|
- begin
- error_klass.new.inspect
- rescue ArgumentError
- raise "Instance of #{error_klass} can't be initialized with no arguments"
- end
+ (error_klasses - [ActiveRecord::AmbiguousSourceReflectionForThroughAssociation]).each do |error_klass|
+ error_klass.new.inspect
+ rescue ArgumentError
+ raise "Instance of #{error_klass} can't be initialized with no arguments"
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 64dfd86ce2..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
@@ -40,21 +41,18 @@ if ActiveRecord::Base.connection.supports_explain?
queries = sqls.zip(binds)
stub_explain_for_query_plans do
- expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n")
+ 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
- object = Struct.new(:name)
- cols = [object.new('wadus'), object.new('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)
stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do
- expected = <<-SQL.strip_heredoc
+ expected = <<~SQL
EXPLAIN for: #{sqls[0]} [["wadus", 1]]
query plan foo
@@ -68,20 +66,23 @@ if ActiveRecord::Base.connection.supports_explain?
def test_unsupported_connection_adapter
connection.stub(:supports_explain?, false) do
assert_not_called(base.logger, :warn) do
- Car.where(:name => 'honda').to_a
+ Car.where(name: "honda").to_a
end
end
end
private
- def stub_explain_for_query_plans(query_plans = ['query plan foo', 'query plan bar'])
+ 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
+ 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..2f4c9b0ef7
--- /dev/null
+++ b/activerecord/test/cases/filter_attributes_test.rb
@@ -0,0 +1,132 @@
+# 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
+ 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
+
+ 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
+ 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
diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb
index 6ab2657c44..66413a98e4 100644
--- a/activerecord/test/cases/finder_respond_to_test.rb
+++ b/activerecord/test/cases/finder_respond_to_test.rb
@@ -1,20 +1,21 @@
+# 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
- class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) { }
+ Topic.singleton_class.define_method(:method_added_for_finder_respond_to_test) { }
assert_respond_to Topic, :method_added_for_finder_respond_to_test
ensure
- class << Topic; self; end.send(:remove_method, :method_added_for_finder_respond_to_test)
+ Topic.singleton_class.remove_method :method_added_for_finder_respond_to_test
end
def test_should_preserve_normal_respond_to_behaviour_and_respond_to_standard_object_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)
+ Topic.singleton_class.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 692c6bf2d0..ca114d468e 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -1,35 +1,41 @@
+# 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/account"
+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"
+require "support/stubs/strong_parameters"
class FinderTest < ActiveRecord::TestCase
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
@@ -49,76 +55,91 @@ class FinderTest < ActiveRecord::TestCase
end
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
- 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(: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
+ 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])
+ 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
+ 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])
+ 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
+ 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])
+ 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
+ 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])
+ 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
+ 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_deprecated
- assert_deprecated do
+ def test_find_passing_active_record_object_is_not_permitted
+ assert_raises(ArgumentError) do
Topic.find(Topic.last)
end
end
@@ -127,7 +148,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
@@ -136,7 +157,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
@@ -144,22 +165,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)
@@ -167,17 +215,33 @@ 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)
+ 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_with_strong_parameters
+ assert_equal false, Subscriber.exists?(ProtectedParams.new(nick: "foo").permit!)
+
+ Subscriber.create!(nick: "foo")
+
+ assert_equal true, Subscriber.exists?(ProtectedParams.new(nick: "foo").permit!)
+
+ assert_raises(ActiveModel::ForbiddenAttributesError) do
+ Subscriber.exists?(ProtectedParams.new(nick: "foo"))
end
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
@@ -204,26 +268,76 @@ 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
+
+ def test_exists_with_distinct_and_offset_and_joins
+ assert Post.left_joins(:comments).distinct.offset(10).exists?
+ assert_not Post.left_joins(:comments).distinct.offset(11).exists?
+ end
+
+ def test_exists_with_distinct_and_offset_and_select
+ assert Post.select(:body).distinct.offset(3).exists?
+ assert_not Post.select(:body).distinct.offset(4).exists?
+ end
+
+ # Ensure +exists?+ runs without an error by excluding distinct value.
+ # See https://github.com/rails/rails/pull/26981.
+ def test_exists_with_order_and_distinct
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_large_number
+ assert_equal true, Topic.where(id: [1, 9223372036854775808]).exists?
+ assert_equal true, Topic.where(id: 1..9223372036854775808).exists?
+ assert_equal true, Topic.where(id: -9223372036854775809..9223372036854775808).exists?
+ assert_equal false, Topic.where(id: 9223372036854775808..9223372036854775809).exists?
+ assert_equal false, Topic.where(id: -9223372036854775810..-9223372036854775809).exists?
+ assert_equal false, Topic.where(id: 9223372036854775808..1).exists?
+ assert_equal true, Topic.where(id: 1).or(Topic.where(id: 9223372036854775808)).exists?
+ assert_equal true, Topic.where.not(id: 9223372036854775808).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
@@ -233,17 +347,14 @@ 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
@@ -263,10 +374,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
- entrants = Entrant.limit(3).offset(2).find([1,3,2])
+ 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
+ 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
@@ -274,34 +385,46 @@ 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
+ 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
+ assert_equal topics(:first), Topic.where(id: [1, 9999999999999999999999999999999]).find(1)
+ end
+
+ def test_find_by_on_relation_with_large_number
+ assert_nil Topic.where("1=1").find_by(id: 9999999999999999999999999999999)
+ assert_equal topics(:first), Topic.where(id: [1, 9999999999999999999999999999999]).find_by(id: 1)
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
+ assert_equal topics(:first), Topic.where(id: [1, 9999999999999999999999999999999]).find_by!(id: 1)
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
@@ -313,7 +436,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 }
@@ -339,13 +462,18 @@ class FinderTest < ActiveRecord::TestCase
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))
+ firm = companies(:first_firm)
+ assert_equal firm.account, Account.find_by(firm: Firm.where(id: firm))
+ assert_equal firm.account, Account.find_by(firm_id: Firm.where(id: firm))
+ end
+
+ def test_find_by_and_where_consistency_with_active_record_instance
+ firm = companies(:first_firm)
+ assert_equal Account.where(firm_id: firm).take, Account.find_by(firm_id: firm)
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
@@ -388,6 +516,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
@@ -410,6 +539,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
@@ -432,6 +562,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
@@ -454,6 +585,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
@@ -476,6 +608,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
@@ -493,12 +626,12 @@ class FinderTest < ActiveRecord::TestCase
assert_equal topics(:fourth), Topic.offset(1).second_to_last
assert_equal topics(:fourth), Topic.offset(2).second_to_last
assert_equal topics(:fourth), Topic.offset(3).second_to_last
- assert_equal nil, Topic.offset(4).second_to_last
- assert_equal nil, Topic.offset(5).second_to_last
+ assert_nil Topic.offset(4).second_to_last
+ assert_nil Topic.offset(5).second_to_last
- #test with limit
- # assert_equal nil, Topic.limit(1).second # TODO: currently failing
- assert_equal nil, Topic.limit(1).second_to_last
+ # 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
@@ -521,15 +654,15 @@ class FinderTest < ActiveRecord::TestCase
# test with offset
assert_equal topics(:third), Topic.offset(1).third_to_last
assert_equal topics(:third), Topic.offset(2).third_to_last
- assert_equal nil, Topic.offset(3).third_to_last
- assert_equal nil, Topic.offset(4).third_to_last
- assert_equal nil, Topic.offset(5).third_to_last
+ assert_nil Topic.offset(3).third_to_last
+ assert_nil Topic.offset(4).third_to_last
+ assert_nil Topic.offset(5).third_to_last
# test with limit
- # assert_equal nil, Topic.limit(1).third # TODO: currently failing
- assert_equal nil, Topic.limit(1).third_to_last
- # assert_equal nil, Topic.limit(2).third # TODO: currently failing
- assert_equal nil, Topic.limit(2).third_to_last
+ assert_nil Topic.limit(1).third
+ assert_nil Topic.limit(1).third_to_last
+ assert_nil Topic.limit(2).third
+ assert_nil Topic.limit(2).third_to_last
end
def test_third_to_last_have_primary_key_order_by_default
@@ -567,9 +700,9 @@ class FinderTest < ActiveRecord::TestCase
end
def test_take_and_first_and_last_with_integer_should_use_sql_limit
- assert_sql(/LIMIT|ROWNUM <=/) { Topic.take(3).entries }
- assert_sql(/LIMIT|ROWNUM <=/) { Topic.first(2).entries }
- assert_sql(/LIMIT|ROWNUM <=/) { 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
@@ -579,17 +712,17 @@ class FinderTest < ActiveRecord::TestCase
def test_last_with_integer_and_order_should_use_sql_limit
relation = Topic.order("title")
assert_queries(1) { relation.last(5) }
- assert !relation.loaded?
+ assert_not_predicate relation, :loaded?
end
def test_last_with_integer_and_reorder_should_use_sql_limit
relation = Topic.reorder("title")
assert_queries(1) { relation.last(5) }
- assert !relation.loaded?
+ assert_not_predicate relation, :loaded?
end
def test_last_on_loaded_relation_should_not_use_sql
- relation = Topic.limit(10).load
+ relation = Topic.limit(10).load
assert_no_queries do
relation.last
relation.last(2)
@@ -597,25 +730,65 @@ class FinderTest < ActiveRecord::TestCase
end
def test_last_with_irreversible_order
- assert_deprecated do
- Topic.order("coalesce(author_name, title)").last
+ 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')
+ 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_implicit_order_column_is_configurable
+ old_implicit_order_column = Topic.implicit_order_column
+ Topic.implicit_order_column = "title"
+
+ assert_equal topics(:fifth), Topic.first
+ assert_equal topics(:third), Topic.last
+ ensure
+ Topic.implicit_order_column = old_implicit_order_column
+ end
+
def test_take_and_first_and_last_with_integer_should_return_an_array
assert_kind_of Array, Topic.take(5)
assert_kind_of Array, Topic.first(5)
@@ -632,12 +805,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
@@ -652,9 +825,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
@@ -663,28 +841,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
@@ -693,34 +871,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
@@ -755,9 +941,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
@@ -769,56 +955,94 @@ 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)
+ assert_equal Customer.where(balance: [david_balance.amount, zaphod_balance.amount]).to_sql, found_customers.to_sql
+ 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_hash_condition_find_nil_with_aggregate_having_one_mapping
+ assert_nil customers(:zaphod).gps_location
+ found_customer = Customer.where(gps_location: nil, name: customers(:zaphod).name).first
+ assert_equal customers(:zaphod), found_customer
+ end
+
+ def test_hash_condition_find_nil_with_aggregate_having_multiple_mappings
+ customers(:david).update(address: nil)
+ assert_nil customers(:david).address_street
+ assert_nil customers(:david).address_city
+ found_customer = Customer.where(address: nil, name: customers(:david).name).first
+ assert_equal customers(:david), found_customer
+ end
+
+ def test_hash_condition_find_empty_array_with_aggregate_having_multiple_mappings
+ assert_nil Customer.where(address: []).first
+ 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
@@ -827,16 +1051,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
@@ -853,18 +1077,18 @@ 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
+ Company.create("name" => "37signals' go'es against")
+ assert Company.where(["name = :name", { name: "37signals' go'es against" }]).first
end
def test_named_bind_variables
@@ -874,11 +1098,6 @@ class FinderTest < ActiveRecord::TestCase
assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on
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]))
@@ -898,7 +1117,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)
@@ -915,7 +1134,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
@@ -954,13 +1173,13 @@ 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
+ Account.singleton_class.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
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
@@ -983,15 +1202,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
@@ -1007,20 +1217,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
@@ -1034,12 +1234,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
@@ -1048,13 +1248,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
@@ -1066,8 +1266,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
@@ -1075,36 +1275,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).
where.not(author_addresses: { id: nil }).
- order('author_addresses.id DESC').limit(2).to_a.size
+ order("author_addresses.id DESC").limit(2).to_a.size
assert_equal 3, Post.includes(author: :author_address, authors: :author_address).
where.not(author_addresses_authors: { id: nil }).
- order('author_addresses_authors.id DESC').limit(3).to_a.size
+ 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
@@ -1112,20 +1317,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
@@ -1133,9 +1348,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
@@ -1158,16 +1373,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
@@ -1183,7 +1402,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
@@ -1216,11 +1435,39 @@ class FinderTest < ActiveRecord::TestCase
assert_equal tyre2, zyke.tyres.custom_find_by(id: tyre2.id)
end
- protected
+ 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
@@ -1229,5 +1476,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 e64b90507e..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 }
@@ -124,33 +126,33 @@ END
end
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 })
+ 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
+ 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'
+ 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
+ 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 da934ab8fe..0cb868da6e 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/comment'
-require 'models/company'
-require 'models/computer'
-require 'models/course'
-require 'models/developer'
-require 'models/doubloon'
-require 'models/joke'
-require 'models/matey'
-require 'models/parrot'
-require 'models/pirate'
-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,255 @@ 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
+ 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
+
+ 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 = 906
+ 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_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 +318,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 +330,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 +342,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 +369,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 +435,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
@@ -192,35 +457,46 @@ 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
- assert_equal(%(table "parrots" has no column named "arrr".), e.message)
+
+ assert_equal(%(table "parrots" has no columns named "arrr", "foobar".), e.message)
end
def test_yaml_file_with_symbol_columns
@@ -229,11 +505,11 @@ class FixturesTest < ActiveRecord::TestCase
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
@@ -249,10 +525,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
@@ -260,8 +537,8 @@ 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:"
+ 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
@@ -276,11 +553,11 @@ class FixturesTest < ActiveRecord::TestCase
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
@@ -291,17 +568,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
@@ -309,26 +586,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
@@ -337,9 +618,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
@@ -368,7 +650,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
@@ -385,14 +667,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
@@ -414,7 +696,7 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase
def test_reloading_fixtures_through_accessor_methods
topic = Struct.new(:title)
assert_equal "The First Topic", topics(:first).title
- assert_called(@loaded_fixtures['topics']['first'], :find, returns: topic.new("Fresh Topic!")) do
+ assert_called(@loaded_fixtures["topics"]["first"], :find, returns: topic.new("Fresh Topic!")) do
assert_equal "Fresh Topic!", topics(:first, true).title
end
end
@@ -427,7 +709,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
@@ -479,7 +761,6 @@ class SetupSubclassTest < SetupTest
end
end
-
class OverlappingFixturesTest < ActiveRecord::TestCase
fixtures :topics, :developers
fixtures :developers, :accounts
@@ -510,13 +791,13 @@ 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
@@ -553,7 +834,7 @@ class SetFixtureClassPrevailsTest < ActiveRecord::TestCase
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
@@ -565,7 +846,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
@@ -577,7 +858,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
@@ -593,7 +874,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
@@ -608,7 +889,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
@@ -622,6 +903,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, _lazy: 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
@@ -636,7 +977,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
@@ -679,7 +1020,7 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase
private
def load_fixtures(config)
- raise 'argh'
+ raise "argh"
end
end
@@ -689,7 +1030,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
@@ -698,11 +1039,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
@@ -711,7 +1052,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
@@ -720,28 +1061,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
@@ -758,8 +1101,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)
@@ -828,13 +1171,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
@@ -857,7 +1200,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
@@ -884,15 +1227,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
@@ -909,14 +1252,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
@@ -932,8 +1275,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
@@ -961,14 +1304,77 @@ end
class FixtureClassNamesTest < ActiveRecord::TestCase
def setup
- @saved_cache = self.fixture_class_names.dup
+ @saved_cache = fixture_class_names.dup
end
def teardown
- self.fixture_class_names.replace(@saved_cache)
+ fixture_class_names.replace(@saved_cache)
end
test "fixture_class_names returns nil for unregistered identifier" do
- assert_nil self.fixture_class_names['unregistered_identifier']
+ 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
+
+class MultipleDatabaseFixturesTest < ActiveRecord::TestCase
+ test "enlist_fixture_connections ensures multiple databases share a connection pool" do
+ with_temporary_connection_pool do
+ ActiveRecord::Base.connects_to database: { writing: :arunit, reading: :arunit2 }
+
+ rw_conn = ActiveRecord::Base.connection
+ ro_conn = ActiveRecord::Base.connection_handlers[:reading].connection_pool_list.first.connection
+
+ assert_not_equal rw_conn, ro_conn
+
+ enlist_fixture_connections
+
+ rw_conn = ActiveRecord::Base.connection
+ ro_conn = ActiveRecord::Base.connection_handlers[:reading].connection_pool_list.first.connection
+
+ assert_equal rw_conn, ro_conn
+ end
+ ensure
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.connection_handler }
+ 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
+end
diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb
index 91921469b8..e7e31b6d2d 100644
--- a/activerecord/test/cases/forbidden_attributes_protection_test.rb
+++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb
@@ -1,83 +1,49 @@
-require 'cases/helper'
-require 'active_support/core_ext/hash/indifferent_access'
+# frozen_string_literal: true
-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)
- @parameters = attributes.with_indifferent_access
- @permitted = false
- end
-
- def permit!
- @permitted = true
- 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
- end
- end
-end
+require "cases/helper"
+require "models/company"
+require "models/person"
+require "models/ship"
+require "models/ship_part"
+require "models/treasure"
+require "support/stubs/strong_parameters"
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
@@ -86,7 +52,7 @@ 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!
@@ -94,21 +60,21 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase
end
def test_create_with_works_with_permitted_params
- params = ProtectedParams.new(first_name: 'Guille').permit!
+ params = ProtectedParams.new(first_name: "Guille").permit!
person = Person.create_with(params).create!
- assert_equal 'Guille', person.first_name
+ 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!
@@ -116,21 +82,21 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase
end
def test_where_works_with_permitted_params
- params = ProtectedParams.new(first_name: 'Guille').permit!
+ params = ProtectedParams.new(first_name: "Guille").permit!
person = Person.where(params).create!
- assert_equal 'Guille', person.first_name
+ 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')
+ params = ProtectedParams.new(first_name: "Guille", gender: "m")
assert_raises(ActiveModel::ForbiddenAttributesError) do
Person.where().not(params)
@@ -138,13 +104,13 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase
end
def test_where_not_works_with_permitted_params
- params = ProtectedParams.new(first_name: 'Guille').permit!
+ params = ProtectedParams.new(first_name: "Guille").permit!
Person.create!(params)
- assert_empty Person.where.not(params).select {|p| p.first_name == 'Guille' }
+ 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!
+ params = ProtectedParams.new(name: "Stern", ship_attributes: ProtectedParams.new(name: "The Black Rock").permit!).permit!
part = ShipPart.new(params)
assert_equal "Stern", part.name
@@ -155,11 +121,10 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase
params = ProtectedParams.new(
trinkets_attributes: ProtectedParams.new(
"0" => ProtectedParams.new(name: "Necklace").permit!,
- "1" => ProtectedParams.new(name: "Spoon").permit! ) ).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..9dbd339fe7 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
@@ -28,34 +30,32 @@ 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")
- # add a before destroy to student
- Student.class_eval do
- before_destroy do
- raise ActiveRecord::Rollback unless lessons.empty?
- end
+ sicp = Lesson.new(name: "SICP")
+ ben = Student.new(name: "Ben Bitdiddle")
+ # add a before destroy to student
+ Student.class_eval do
+ before_destroy do
+ raise ActiveRecord::Rollback unless lessons.empty?
end
- ben.lessons << sicp
- ben.save!
- ben.destroy
- assert !ben.reload.lessons.empty?
- ensure
- # get rid of it so Student is still like it was
- Student.reset_callbacks(:destroy)
end
+ ben.lessons << sicp
+ ben.save!
+ ben.destroy
+ assert_not_empty ben.reload.lessons
+ ensure
+ # get rid of it so Student is still like it was
+ Student.reset_callbacks(:destroy)
end
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 d2fdf03e9d..f95d082907 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -1,17 +1,16 @@
-require 'config'
+# frozen_string_literal: true
-require 'active_support/testing/autorun'
-require 'active_support/testing/method_call_assertions'
-require 'stringio'
+require "config"
-require 'active_record'
-require 'cases/test_case'
-require 'active_support/dependencies'
-require 'active_support/logger'
-require 'active_support/core_ext/string/strip'
+require "stringio"
-require 'support/config'
-require 'support/connection'
+require "active_record"
+require "cases/test_case"
+require "active_support/dependencies"
+require "active_support/logger"
+
+require "support/config"
+require "support/connection"
# TODO: Move all these random hacks into the ARTest namespace and into the support/ dir
@@ -27,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|
@@ -49,18 +45,37 @@ def subsecond_precision_supported?
end
def mysql_enforcing_gtid_consistency?
- current_adapter?(: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?
+%w[
+ supports_savepoints?
+ supports_partial_index?
+ supports_insert_returning?
+ supports_insert_on_duplicate_skip?
+ supports_insert_on_duplicate_update?
+ supports_insert_conflict_target?
+ supports_optimizer_hints?
+].each do |method_name|
+ define_method method_name do
+ ActiveRecord::Base.connection.public_send(method_name)
+ end
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)
@@ -134,21 +149,6 @@ def disable_extension!(extension, connection)
connection.reconnect!
end
-require "cases/validations_repair_helper"
-class ActiveSupport::TestCase
- include ActiveRecord::TestFixtures
- include ActiveRecord::ValidationsRepairHelper
- include ActiveSupport::Testing::MethodCallAssertions
-
- 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
@@ -162,6 +162,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
@@ -188,17 +190,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 9fc75b7377..7b388ebc5e 100644
--- a/activerecord/test/cases/hot_compatibility_test.rb
+++ b/activerecord/test/cases/hot_compatibility_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 HotCompatibilityTest < ActiveRecord::TestCase
self.use_transactional_tests = false
@@ -12,7 +14,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase
t.string :bar
end
- def self.name; 'HotCompatibility'; end
+ def self.name; "HotCompatibility"; end
end
end
@@ -35,29 +37,29 @@ 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)
+ 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'
+ record = @klass.create! bar: "bar"
# prepare the reload statement in a transaction
@klass.transaction do
@@ -83,7 +85,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase
test "cleans up after prepared statement failure in nested transactions" do
with_two_connections do |original_connection, ddl_connection|
- record = @klass.create! bar: 'bar'
+ record = @klass.create! bar: "bar"
# prepare the reload statement in a transaction
@klass.transaction do
@@ -114,29 +116,29 @@ class HotCompatibilityTest < ActiveRecord::TestCase
private
- def get_prepared_statement_cache(connection)
- connection.instance_variable_get(:@statements)
- .instance_variable_get(:@cache)[Process.pid]
- end
+ 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
+ # 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
- yield original_connection, ddl_connection
+ 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.connection_pool.checkin ddl_connection
+ ActiveRecord::Base.clear_all_connections!
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 e234b9a6a9..629167e9ed 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -1,13 +1,16 @@
-require 'cases/helper'
-require 'models/author'
-require 'models/company'
-require 'models/person'
-require 'models/post'
-require 'models/project'
-require 'models/subscriber'
-require 'models/vegetables'
-require 'models/shop'
-require 'models/sponsor'
+# 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)
@@ -29,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
@@ -42,37 +45,37 @@ 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, ActiveRecord::Base.send(:compute_type, 'Author')
+ assert_equal Author, Company.send(:compute_type, "Author")
end
def test_compute_type_nonexistent_constant
e = assert_raises NameError do
- ActiveRecord::Base.send :compute_type, 'NonexistentModel'
+ Company.send :compute_type, "NonexistentModel"
end
- assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message
- assert_equal 'ActiveRecord::Base::NonexistentModel', e.name
+ assert_equal "uninitialized constant Company::NonexistentModel", e.message
+ assert_equal "Company::NonexistentModel", e.name
end
def test_compute_type_no_method_error
- ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise NoMethodError }) do
+ ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise NoMethodError }) do
assert_raises NoMethodError do
- ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ Company.send :compute_type, "InvalidModel"
end
end
end
@@ -87,19 +90,18 @@ class InheritanceTest < ActiveRecord::TestCase
error = e
end
- ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise e }) do
-
+ ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise e }) do
exception = assert_raises NameError do
- ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ Company.send :compute_type, "InvalidModel"
end
assert_equal error.message, exception.message
end
end
def test_compute_type_argument_error
- ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise ArgumentError }) do
+ ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise ArgumentError }) do
assert_raises ArgumentError do
- ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ Company.send :compute_type, "InvalidModel"
end
end
end
@@ -107,14 +109,14 @@ class InheritanceTest < ActiveRecord::TestCase
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
@@ -127,56 +129,70 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_descends_from_active_record
- assert !ActiveRecord::Base.descends_from_active_record?
+ assert_not_predicate ActiveRecord::Base, :descends_from_active_record?
# Abstract subclass of AR::Base.
- assert LoosePerson.descends_from_active_record?
+ assert_predicate LoosePerson, :descends_from_active_record?
# Concrete subclass of an abstract class.
- assert LooseDescendant.descends_from_active_record?
+ assert_predicate LooseDescendant, :descends_from_active_record?
# Concrete subclass of AR::Base.
- assert TightPerson.descends_from_active_record?
+ assert_predicate TightPerson, :descends_from_active_record?
# Concrete subclass of a concrete class but has no type column.
- assert TightDescendant.descends_from_active_record?
+ assert_predicate TightDescendant, :descends_from_active_record?
# Concrete subclass of AR::Base.
- assert Post.descends_from_active_record?
+ assert_predicate Post, :descends_from_active_record?
+
+ # Concrete subclasses of a concrete class which has a type column.
+ assert_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 !StiPost.descends_from_active_record?
+ assert_not_predicate AbstractStiPost, :descends_from_active_record?
- # Concrete subclasses an abstract class which has a type column.
- assert !SubStiPost.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 !ActiveRecord::Base.abstract_class?
- assert LoosePerson.abstract_class?
- assert !LooseDescendant.abstract_class?
+ assert_not_predicate ActiveRecord::Base, :abstract_class?
+ assert_predicate LoosePerson, :abstract_class?
+ assert_not_predicate LooseDescendant, :abstract_class?
end
def test_inheritance_base_class
assert_equal Post, Post.base_class
+ assert_predicate Post, :base_class?
assert_equal Post, SpecialPost.base_class
+ assert_not_predicate SpecialPost, :base_class?
assert_equal Post, StiPost.base_class
- assert_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
@@ -214,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
@@ -224,18 +240,18 @@ class InheritanceTest < ActiveRecord::TestCase
cabbage = vegetable.becomes!(Cabbage)
assert_equal "Cabbage", cabbage.custom_type
- vegetable = cabbage.becomes!(Vegetable)
+ cabbage.becomes!(Vegetable)
assert_nil cabbage.custom_type
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
@@ -250,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)
@@ -263,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
@@ -287,17 +318,41 @@ 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
@@ -305,21 +360,21 @@ class InheritanceTest < ActiveRecord::TestCase
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 }
@@ -352,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
@@ -374,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
@@ -428,13 +484,17 @@ 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
@@ -449,15 +509,17 @@ 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.
assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) }
+ assert_raise(ActiveRecord::RecordNotFound) { Firm.find_by!(id: foo.id) }
# Nest FirmOnTheFly in the test case where Dependencies won't see it.
self.class.const_set :FirmOnTheFly, Class.new(Firm)
assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) }
+ assert_raise(ActiveRecord::SubclassNotFound) { Firm.find_by!(id: foo.id) }
# Nest FirmOnTheFly in Firm where Dependencies will see it.
# This is analogous to nesting models in a migration.
@@ -466,34 +528,35 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase
# And instantiate will find the existing constant rather than trying
# to require firm_on_the_fly.
assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) }
+ assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find_by!(id: foo.id) }
end
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'
+ ActiveRecord::Base.connection.change_column_default :companies, :type, "Firm"
Company.reset_column_information
firm = Company.new # without arguments
- assert_equal 'Firm', firm.type
+ 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
+ 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_equal "Client", client.type
assert_instance_of Client, client
- firm = Company.new(type: 'Client') # overwrite the default type
- assert_equal 'Client', firm.type
+ 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
@@ -502,9 +565,8 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase
end
class InheritanceAttributeTest < ActiveRecord::TestCase
-
class Company < ActiveRecord::Base
- self.table_name = 'companies'
+ self.table_name = "companies"
attribute :type, :string, default: "InheritanceAttributeTest::Startup"
end
@@ -516,11 +578,11 @@ class InheritanceAttributeTest < ActiveRecord::TestCase
def test_inheritance_new_with_subclass_as_default
startup = Company.new # without arguments
- assert_equal 'InheritanceAttributeTest::Startup', startup.type
+ 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
+ empire = Company.new(type: "InheritanceAttributeTest::Empire") # without arguments
+ assert_equal "InheritanceAttributeTest::Empire", empire.type
assert_instance_of Empire, empire
end
end
@@ -555,7 +617,7 @@ class InheritanceAttributeMappingTest < ActiveRecord::TestCase
end
class Company < ActiveRecord::Base
- self.table_name = 'companies'
+ self.table_name = "companies"
attribute :type, :omg_sti
end
@@ -563,18 +625,18 @@ class InheritanceAttributeMappingTest < ActiveRecord::TestCase
class Empire < Company; end
class Sponsor < ActiveRecord::Base
- self.table_name = 'sponsors'
+ 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'
+ 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
+ ["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
@@ -583,19 +645,19 @@ class InheritanceAttributeMappingTest < ActiveRecord::TestCase
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
+ ["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'
+ 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')
+ assert_equal ["omg_inheritance_attribute_mapping_test/company"], ActiveRecord::Base.connection.select_values("SELECT sponsorable_type FROM sponsors")
- sponsor = Sponsor.first
+ sponsor = Sponsor.find(sponsor.id)
assert_equal startup, sponsor.sponsorable
end
end
diff --git a/activerecord/test/cases/insert_all_test.rb b/activerecord/test/cases/insert_all_test.rb
new file mode 100644
index 0000000000..fc25701c80
--- /dev/null
+++ b/activerecord/test/cases/insert_all_test.rb
@@ -0,0 +1,189 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/book"
+
+class ReadonlyNameBook < Book
+ attr_readonly :name
+end
+
+class InsertAllTest < ActiveRecord::TestCase
+ fixtures :books
+
+ def test_insert
+ skip unless supports_insert_on_duplicate_skip?
+
+ id = 1_000_000
+
+ assert_difference "Book.count", +1 do
+ Book.insert(id: id, name: "Rework", author_id: 1)
+ end
+
+ Book.upsert(id: id, name: "Remote", author_id: 1)
+
+ assert_equal "Remote", Book.find(id).name
+ end
+
+ def test_insert!
+ assert_difference "Book.count", +1 do
+ Book.insert! name: "Rework", author_id: 1
+ end
+ end
+
+ def test_insert_all
+ assert_difference "Book.count", +10 do
+ Book.insert_all! [
+ { name: "Rework", author_id: 1 },
+ { name: "Patterns of Enterprise Application Architecture", author_id: 1 },
+ { name: "Design of Everyday Things", author_id: 1 },
+ { name: "Practical Object-Oriented Design in Ruby", author_id: 1 },
+ { name: "Clean Code", author_id: 1 },
+ { name: "Ruby Under a Microscope", author_id: 1 },
+ { name: "The Principles of Product Development Flow", author_id: 1 },
+ { name: "Peopleware", author_id: 1 },
+ { name: "About Face", author_id: 1 },
+ { name: "Eloquent Ruby", author_id: 1 },
+ ]
+ end
+ end
+
+ def test_insert_all_should_handle_empty_arrays
+ assert_raise ArgumentError do
+ Book.insert_all! []
+ end
+ end
+
+ def test_insert_all_raises_on_duplicate_records
+ assert_raise ActiveRecord::RecordNotUnique do
+ Book.insert_all! [
+ { name: "Rework", author_id: 1 },
+ { name: "Patterns of Enterprise Application Architecture", author_id: 1 },
+ { name: "Agile Web Development with Rails", author_id: 1 },
+ ]
+ end
+ end
+
+ def test_insert_all_returns_ActiveRecord_Result
+ result = Book.insert_all! [{ name: "Rework", author_id: 1 }]
+ assert_kind_of ActiveRecord::Result, result
+ end
+
+ def test_insert_all_returns_primary_key_if_returning_is_supported
+ skip unless supports_insert_returning?
+
+ result = Book.insert_all! [{ name: "Rework", author_id: 1 }]
+ assert_equal %w[ id ], result.columns
+ end
+
+ def test_insert_all_returns_nothing_if_returning_is_empty
+ skip unless supports_insert_returning?
+
+ result = Book.insert_all! [{ name: "Rework", author_id: 1 }], returning: []
+ assert_equal [], result.columns
+ end
+
+ def test_insert_all_returns_nothing_if_returning_is_false
+ skip unless supports_insert_returning?
+
+ result = Book.insert_all! [{ name: "Rework", author_id: 1 }], returning: false
+ assert_equal [], result.columns
+ end
+
+ def test_insert_all_returns_requested_fields
+ skip unless supports_insert_returning?
+
+ result = Book.insert_all! [{ name: "Rework", author_id: 1 }], returning: [:id, :name]
+ assert_equal %w[ Rework ], result.pluck("name")
+ end
+
+ def test_insert_all_can_skip_duplicate_records
+ skip unless supports_insert_on_duplicate_skip?
+
+ assert_no_difference "Book.count" do
+ Book.insert_all [{ id: 1, name: "Agile Web Development with Rails" }]
+ end
+ end
+
+ def test_insert_all_will_raise_if_duplicates_are_skipped_only_for_a_certain_conflict_target
+ skip unless supports_insert_on_duplicate_skip? && supports_insert_conflict_target?
+
+ assert_raise ActiveRecord::RecordNotUnique do
+ Book.insert_all [{ id: 1, name: "Agile Web Development with Rails" }],
+ unique_by: :index_books_on_author_id_and_name
+ end
+ end
+
+ def test_insert_all_and_upsert_all_with_index_finding_options
+ skip unless supports_insert_conflict_target?
+
+ assert_difference "Book.count", +3 do
+ Book.insert_all [{ name: "Rework", author_id: 1 }], unique_by: :isbn
+ Book.insert_all [{ name: "Remote", author_id: 1 }], unique_by: %i( author_id name )
+ Book.insert_all [{ name: "Renote", author_id: 1 }], unique_by: :index_books_on_isbn
+ end
+
+ assert_raise ActiveRecord::RecordNotUnique do
+ Book.upsert_all [{ name: "Rework", author_id: 1 }], unique_by: :isbn
+ end
+ end
+
+ def test_insert_all_and_upsert_all_raises_when_index_is_missing
+ skip unless supports_insert_conflict_target?
+
+ [ :cats, %i( author_id isbn ), :author_id ].each do |missing_or_non_unique_by|
+ error = assert_raises ArgumentError do
+ Book.insert_all [{ name: "Rework", author_id: 1 }], unique_by: missing_or_non_unique_by
+ end
+ assert_match "No unique index", error.message
+
+ error = assert_raises ArgumentError do
+ Book.upsert_all [{ name: "Rework", author_id: 1 }], unique_by: missing_or_non_unique_by
+ end
+ assert_match "No unique index", error.message
+ end
+ end
+
+ def test_upsert_all_updates_existing_records
+ skip unless supports_insert_on_duplicate_update?
+
+ new_name = "Agile Web Development with Rails, 4th Edition"
+ Book.upsert_all [{ id: 1, name: new_name }]
+ assert_equal new_name, Book.find(1).name
+ end
+
+ def test_upsert_all_does_not_update_readonly_attributes
+ skip unless supports_insert_on_duplicate_update?
+
+ new_name = "Agile Web Development with Rails, 4th Edition"
+ ReadonlyNameBook.upsert_all [{ id: 1, name: new_name }]
+ assert_not_equal new_name, Book.find(1).name
+ end
+
+ def test_upsert_all_does_not_update_primary_keys
+ skip unless supports_insert_on_duplicate_update? && supports_insert_conflict_target?
+
+ Book.upsert_all [{ id: 101, name: "Perelandra", author_id: 7 }]
+ Book.upsert_all [{ id: 103, name: "Perelandra", author_id: 7, isbn: "1974522598" }],
+ unique_by: :index_books_on_author_id_and_name
+
+ book = Book.find_by(name: "Perelandra")
+ assert_equal 101, book.id, "Should not have updated the ID"
+ assert_equal "1974522598", book.isbn, "Should have updated the isbn"
+ end
+
+ def test_upsert_all_does_not_perform_an_upsert_if_a_partial_index_doesnt_apply
+ skip unless supports_insert_on_duplicate_update? && supports_insert_conflict_target? && supports_partial_index?
+
+ Book.upsert_all [{ name: "Out of the Silent Planet", author_id: 7, isbn: "1974522598", published_on: Date.new(1938, 4, 1) }]
+ Book.upsert_all [{ name: "Perelandra", author_id: 7, isbn: "1974522598" }],
+ unique_by: :index_books_on_isbn
+
+ assert_equal ["Out of the Silent Planet", "Perelandra"], Book.where(isbn: "1974522598").order(:name).pluck(:name)
+ end
+
+ def test_insert_all_raises_on_unknown_attribute
+ assert_raise ActiveRecord::UnknownAttributeError do
+ Book.insert_all! [{ unknown_attribute: "Test" }]
+ end
+ end
+end
diff --git a/activerecord/test/cases/instrumentation_test.rb b/activerecord/test/cases/instrumentation_test.rb
new file mode 100644
index 0000000000..06382b6c7c
--- /dev/null
+++ b/activerecord/test/cases/instrumentation_test.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/book"
+
+module ActiveRecord
+ class InstrumentationTest < ActiveRecord::TestCase
+ def setup
+ ActiveRecord::Base.connection.schema_cache.add(Book.table_name)
+ end
+
+ 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", format: "paperback")
+ book.update_attribute(:format, "ebook")
+ 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", format: "paperback")
+ Book.update_all(format: "ebook")
+ 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 08a186ae07..4185e8d682 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
@@ -130,20 +157,87 @@ class IntegrationTest < ActiveRecord::TestCase
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
- skip("Subsecond precision is not supported") unless subsecond_precision_supported?
dev = Developer.first
dev.touch
key = dev.cache_key
assert_equal key, dev.reload.cache_key
end
- def test_named_timestamps_for_cache_key
- owner = owners(:blackbeard)
- assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at)
+ 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_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 a16b52751a..a1be9c2780 100644
--- a/activerecord/test/cases/invalid_connection_test.rb
+++ b/activerecord/test/cases/invalid_connection_test.rb
@@ -1,24 +1,26 @@
+# frozen_string_literal: true
+
require "cases/helper"
if current_adapter?(:Mysql2Adapter)
-class TestAdapterWithInvalidConnection < ActiveRecord::TestCase
- self.use_transactional_tests = false
+ 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: 'mysql2', 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
-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 e030f6c588..d68cc40107 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
class Horse < ActiveRecord::Base
@@ -6,7 +8,7 @@ end
module ActiveRecord
class InvertibleMigrationTest < ActiveRecord::TestCase
class SilentMigration < ActiveRecord::Migration::Current
- def write(text = '')
+ def write(text = "")
# sssshhhhh!!
end
end
@@ -20,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
@@ -151,16 +161,31 @@ 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
teardown do
%w[horses new_horses].each do |table|
- ActiveSupport::Deprecation.silence do
- if ActiveRecord::Base.connection.table_exists?(table)
- ActiveRecord::Base.connection.drop_table(table)
- end
+ if ActiveRecord::Base.connection.table_exists?(table)
+ ActiveRecord::Base.connection.drop_table(table)
end
end
ActiveRecord::Migration.verbose = @verbose_was
@@ -191,14 +216,14 @@ module ActiveRecord
def test_migrate_up
migration = InvertibleMigration.new
migration.migrate(:up)
- ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses"), "horses should exist" }
+ assert migration.connection.table_exists?("horses"), "horses should exist"
end
def test_migrate_down
migration = InvertibleMigration.new
migration.migrate :up
migration.migrate :down
- ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
+ assert_not migration.connection.table_exists?("horses")
end
def test_migrate_revert
@@ -206,36 +231,30 @@ module ActiveRecord
revert = InvertibleRevertMigration.new
migration.migrate :up
revert.migrate :up
- ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
+ assert_not migration.connection.table_exists?("horses")
revert.migrate :down
- ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") }
+ assert migration.connection.table_exists?("horses")
migration.migrate :down
- ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
+ assert_not migration.connection.table_exists?("horses")
end
def test_migrate_revert_by_part
InvertibleMigration.new.migrate :up
received = []
migration = InvertibleByPartsMigration.new
- migration.test = ->(dir){
- ActiveSupport::Deprecation.silence do
- assert migration.connection.table_exists?("horses")
- assert migration.connection.table_exists?("new_horses")
- end
+ 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
- ActiveSupport::Deprecation.silence do
- assert !migration.connection.table_exists?("horses")
- assert migration.connection.table_exists?("new_horses")
- end
+ assert_not migration.connection.table_exists?("horses")
+ assert migration.connection.table_exists?("new_horses")
migration.migrate :down
assert_equal [:both, :up, :both, :down], received
- ActiveSupport::Deprecation.silence do
- assert migration.connection.table_exists?("horses")
- assert !migration.connection.table_exists?("new_horses")
- end
+ assert migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("new_horses")
end
def test_migrate_revert_whole_migration
@@ -244,20 +263,28 @@ module ActiveRecord
revert = RevertWholeMigration.new(klass)
migration.migrate :up
revert.migrate :up
- ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
+ assert_not migration.connection.table_exists?("horses")
revert.migrate :down
- ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") }
+ assert migration.connection.table_exists?("horses")
migration.migrate :down
- ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") }
+ assert_not migration.connection.table_exists?("horses")
end
end
def test_migrate_nested_revert_whole_migration
revert = NestedRevertWholeMigration.new(InvertibleRevertMigration)
revert.migrate :down
- ActiveSupport::Deprecation.silence { assert revert.connection.table_exists?("horses") }
+ assert revert.connection.table_exists?("horses")
revert.migrate :up
- ActiveSupport::Deprecation.silence { assert !revert.connection.table_exists?("horses") }
+ assert_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
@@ -281,23 +308,27 @@ module ActiveRecord
migration2 = DisableExtension1.new
migration3 = DisableExtension2.new
+ assert_equal true, Horse.connection.extension_available?("hstore")
+
migration1.migrate(:up)
migration2.migrate(:up)
- assert_equal true, Horse.connection.extension_enabled?('hstore')
+ assert_equal true, Horse.connection.extension_enabled?("hstore")
migration3.migrate(:up)
- assert_equal false, Horse.connection.extension_enabled?('hstore')
+ assert_equal false, Horse.connection.extension_enabled?("hstore")
migration3.migrate(:down)
- assert_equal true, Horse.connection.extension_enabled?('hstore')
+ assert_equal true, Horse.connection.extension_enabled?("hstore")
migration2.migrate(:down)
- assert_equal false, Horse.connection.extension_enabled?('hstore')
+ 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)
@@ -322,35 +353,42 @@ module ActiveRecord
def test_legacy_up
LegacyMigration.migrate :up
- ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" }
+ assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist"
end
def test_legacy_down
LegacyMigration.migrate :up
LegacyMigration.migrate :down
- ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" }
+ assert_not ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
end
def test_up
LegacyMigration.up
- ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" }
+ assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist"
end
def test_down
LegacyMigration.up
LegacyMigration.down
- ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" }
+ assert_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) }
- ActiveSupport::Deprecation.silence { 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
@@ -363,10 +401,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/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 6c59d7337a..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
+ 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
@@ -442,46 +695,44 @@ unless in_memory_db?
Person.transaction do
person = Person.find(1)
assert_sql(/LIMIT \$?\d FOR SHARE NOWAIT/) do
- person.lock!('FOR SHARE NOWAIT')
+ 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 707a2d1da1..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"
@@ -21,6 +23,7 @@ class LogSubscriberTest < ActiveRecord::TestCase
TRANSACTION: REGEXP_CYAN,
OTHER: REGEXP_MAGENTA
}
+ Event = Struct.new(:duration, :payload)
class TestDebugLogSubscriber < ActiveRecord::LogSubscriber
attr_reader :debugs
@@ -30,8 +33,9 @@ class LogSubscriberTest < ActiveRecord::TestCase
super
end
- def debug message
- @debugs << message
+ def debug(progname = nil, &block)
+ @debugs << progname
+ super
end
end
@@ -40,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
@@ -55,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
@@ -86,56 +88,51 @@ class LogSubscriberTest < ActiveRecord::TestCase
end
def test_basic_query_logging_coloration
- event = Struct.new(:duration, :payload)
logger = TestDebugLogSubscriber.new
logger.colorize_logging = true
SQL_COLORINGS.each do |verb, color_regex|
- logger.sql(event.new(0, sql: verb.to_s))
+ 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
- event = Struct.new(:duration, :payload)
logger = TestDebugLogSubscriber.new
logger.colorize_logging = true
SQL_COLORINGS.each do |verb, _|
- logger.sql(event.new(0, sql: verb.to_s))
- assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+ 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, {sql: verb.to_s, name: "SQL"}))
- assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0.0ms\)#{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
- event = Struct.new(:duration, :payload)
logger = TestDebugLogSubscriber.new
logger.colorize_logging = true
SQL_COLORINGS.each do |verb, _|
- logger.sql(event.new(0, {sql: verb.to_s, name: "Model Load"}))
- assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last)
+ 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, {sql: verb.to_s, name: "Model Exists"}))
- assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0.0ms\)#{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, {sql: verb.to_s, name: "ANY SPECIFIC NAME"}))
- assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0.0ms\)#{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
- event = Struct.new(:duration, :payload)
logger = TestDebugLogSubscriber.new
logger.colorize_logging = true
SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex|
- logger.sql(event.new(0, sql: "#{verb} WHERE ID IN SELECT"))
- assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last)
+ 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
- event = Struct.new(:duration, :payload)
logger = TestDebugLogSubscriber.new
logger.colorize_logging = true
SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex|
@@ -145,13 +142,12 @@ class LogSubscriberTest < ActiveRecord::TestCase
SELECT ID FROM THINGS
)
EOS
- logger.sql(event.new(0, sql: sql))
- assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last)
+ 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
- event = Struct.new(:duration, :payload)
logger = TestDebugLogSubscriber.new
logger.colorize_logging = true
sql = <<-EOS
@@ -159,14 +155,14 @@ class LogSubscriberTest < ActiveRecord::TestCase
(SELECT * FROM mytable FOR UPDATE) ss
WHERE col1 = 5;
EOS
- logger.sql(event.new(0, sql: sql))
- assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last)
+ 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, sql: sql))
- assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last)
+ 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
@@ -177,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
@@ -211,9 +237,15 @@ class LogSubscriberTest < ActiveRecord::TestCase
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 d6963b48d7..cc0587fa50 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,10 +42,10 @@ 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
@@ -53,11 +55,11 @@ module ActiveRecord
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
+ 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
+ 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
@@ -346,9 +366,9 @@ module ActiveRecord
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,15 +419,15 @@ 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
def test_drop_table_if_exists
connection.create_table(:testings)
- ActiveSupport::Deprecation.silence { assert connection.table_exists?(:testings) }
+ assert connection.table_exists?(:testings)
connection.drop_table(:testings, if_exists: true)
- ActiveSupport::Deprecation.silence { assert_not connection.table_exists?(:testings) }
+ assert_not connection.table_exists?(:testings)
end
def test_drop_table_if_exists_nothing_raised
@@ -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,11 @@ module ActiveRecord
end
def test_create_table_with_force_cascade_drops_dependent_objects
- skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:Mysql2Adapter)
+ if current_adapter?(:Mysql2Adapter)
+ skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE"
+ elsif current_adapter?(:SQLite3Adapter)
+ skip "SQLite3 does not support DROP TABLE CASCADE syntax"
+ end
# can't re-create table referenced by foreign key
assert_raises(ActiveRecord::StatementInvalid) do
@connection.create_table :trains, force: true
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index 2f9c50141f..c108d372d1 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/migration/helper"
module ActiveRecord
@@ -95,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
@@ -157,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
@@ -171,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
@@ -185,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
@@ -206,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
@@ -234,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 c7a1b81a75..6f9190c110 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
@@ -43,11 +45,11 @@ module ActiveRecord
assert_nil TestModel.columns_hash["description"].limit
end
- if current_adapter?(: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,15 +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?(:PostgreSQLAdapter)
- connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
else
connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
end
@@ -74,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
@@ -94,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
@@ -126,44 +138,37 @@ 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
@@ -171,11 +176,9 @@ module ActiveRecord
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 }
-
- unless current_adapter?(:PostgreSQLAdapter)
- assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff }
- end
+ assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, limit: 10 }
+ assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff }
+ assert_raise(ActiveRecordError) { add_column :test_models, :binary_too_big, :binary, 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 8294da0373..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
@@ -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 fca1cb7e97..dbbba9c5fa 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?(: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,159 @@ 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'
- if current_adapter? :OracleAdapter
- 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)
- end
+ rename_column "test_models", "hat_size", "size"
+ assert_equal ["index_test_models_on_hat_style_and_size"], connection.indexes("test_models").map(&:name)
- rename_column "test_models", "hat_style", 'style'
- if current_adapter? :OracleAdapter
- 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)
- end
+ rename_column "test_models", "hat_style", "style"
+ assert_equal ["index_test_models_on_style_and_size"], connection.indexes("test_models").map(&:name)
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
- change_column "test_models", "contributor", :boolean, :default => nil
+ 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, 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 +296,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
@@ -301,6 +310,17 @@ module ActiveRecord
ensure
connection.drop_table(:my_table) rescue nil
end
+
+ def test_add_column_without_column_name
+ e = assert_raise ArgumentError do
+ connection.create_table "my_table", force: true do |t|
+ t.timestamp
+ end
+ end
+ assert_equal "Missing column name(s) for timestamp", e.message
+ ensure
+ connection.drop_table :my_table, if_exists: true
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index 1e3529db54..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,26 +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 = Struct.new(:foo)
- recorder = CommandRecorder.new(recorder.new('bar'))
- assert_equal 'bar', recorder.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
@@ -58,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
@@ -115,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
@@ -143,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
@@ -166,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
@@ -203,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
@@ -222,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
@@ -254,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
@@ -283,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
@@ -327,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"]
@@ -345,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
index 60ca90464d..5753bd7117 100644
--- a/activerecord/test/cases/migration/compatibility_test.rb
+++ b/activerecord/test/cases/migration/compatibility_test.rb
@@ -1,4 +1,7 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/schema_dumping_helper"
module ActiveRecord
class Migration
@@ -13,8 +16,8 @@ module ActiveRecord
ActiveRecord::Migration.verbose = false
connection.create_table :testings do |t|
- t.column :foo, :string, :limit => 100
- t.column :bar, :string, :limit => 100
+ t.column :foo, :string, limit: 5
+ t.column :bar, :string, limit: 100
end
end
@@ -25,7 +28,7 @@ module ActiveRecord
end
def test_migration_doesnt_remove_named_index
- connection.add_index :testings, :foo, :name => "custom_index_name"
+ connection.add_index :testings, :foo, name: "custom_index_name"
migration = Class.new(ActiveRecord::Migration[4.2]) {
def version; 101 end
@@ -55,7 +58,7 @@ module ActiveRecord
end
def test_references_does_not_add_index_by_default
- migration = Class.new(ActiveRecord::Migration) {
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
def migrate(x)
create_table :more_testings do |t|
t.references :foo
@@ -73,7 +76,7 @@ module ActiveRecord
end
def test_timestamps_have_null_constraints_if_not_present_in_migration_of_create_table
- migration = Class.new(ActiveRecord::Migration) {
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
def migrate(x)
create_table :more_testings do |t|
t.timestamps
@@ -83,14 +86,46 @@ module ActiveRecord
ActiveRecord::Migrator.new(:up, [migration]).migrate
- assert connection.columns(:more_testings).find { |c| c.name == 'created_at' }.null
- assert connection.columns(:more_testings).find { |c| c.name == 'updated_at' }.null
+ assert connection.column_exists?(:more_testings, :created_at, null: true)
+ assert connection.column_exists?(:more_testings, :updated_at, null: true)
ensure
connection.drop_table :more_testings rescue nil
end
+ 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.column_exists?(:testings, :created_at, null: true)
+ assert connection.column_exists?(:testings, :updated_at, null: true)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_have_null_constraints_if_not_present_in_migration_of_change_table_with_bulk
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
+ def migrate(x)
+ change_table :testings, bulk: true do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: true)
+ assert connection.column_exists?(:testings, :updated_at, null: true)
+ end
+ end
+
def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table
- migration = Class.new(ActiveRecord::Migration) {
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
def migrate(x)
add_timestamps :testings
end
@@ -98,21 +133,339 @@ module ActiveRecord
ActiveRecord::Migrator.new(:up, [migration]).migrate
- assert connection.columns(:testings).find { |c| c.name == 'created_at' }.null
- assert connection.columns(:testings).find { |c| c.name == 'updated_at' }.null
+ assert connection.column_exists?(:testings, :created_at, null: true)
+ assert connection.column_exists?(:testings, :updated_at, null: true)
end
- def test_legacy_migrations_get_deprecation_warning_when_run
- migration = Class.new(ActiveRecord::Migration) {
- def up
- add_column :testings, :baz, :string
+ def test_timestamps_doesnt_set_precision_on_create_table
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ create_table :more_testings do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:more_testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:more_testings, :updated_at, null: false, **precision_implicit_default)
+ ensure
+ connection.drop_table :more_testings rescue nil
+ end
+
+ def test_timestamps_doesnt_set_precision_on_change_table
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ change_table :testings do |t|
+ t.timestamps default: Time.now
+ end
end
- }
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default)
+ end
- assert_deprecated do
- migration.migrate :up
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_doesnt_set_precision_on_change_table_with_bulk
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ change_table :testings, bulk: true do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default)
+ end
+ end
+
+ def test_timestamps_doesnt_set_precision_on_add_timestamps
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ add_timestamps :testings, default: Time.now
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default)
+ end
+
+ def test_legacy_migrations_raises_exception_when_inherited
+ 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
+
+ private
+ def precision_implicit_default
+ if current_adapter?(:Mysql2Adapter)
+ { presicion: 0 }
+ else
+ { presicion: nil }
+ 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 0a7b57455c..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,9 +14,7 @@ module ActiveRecord
teardown do
%w(artists_musics musics_videos catalog).each do |table_name|
- ActiveSupport::Deprecation.silence do
- connection.drop_table table_name if connection.table_exists?(table_name)
- end
+ connection.drop_table table_name, if_exists: true
end
end
@@ -31,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
@@ -55,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
@@ -69,7 +69,7 @@ module ActiveRecord
def test_create_join_table_without_indexes
connection.create_join_table :artists, :musics
- assert connection.indexes(:artists_musics).blank?
+ assert_predicate connection.indexes(:artists_musics), :blank?
end
def test_create_join_table_with_index
@@ -80,55 +80,73 @@ module ActiveRecord
assert_equal [%w(artist_id music_id)], connection.indexes(:artists_musics).map(&:columns)
end
+ def test_create_join_table_respects_reference_key_type
+ connection.create_join_table :artists, :musics do |t|
+ t.references :video
+ end
+
+ artist_id, music_id, video_id = connection.columns(:artists_musics).sort_by(&:name)
+
+ assert_equal video_id.sql_type, artist_id.sql_type
+ assert_equal video_id.sql_type, music_id.sql_type
+ end
+
def test_drop_join_table
connection.create_join_table :artists, :musics
connection.drop_join_table :artists, :musics
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') }
+ assert_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"
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') }
+ assert_not connection.table_exists?("artists_musics")
end
def test_drop_join_table_with_the_proper_order
connection.create_join_table :videos, :musics
connection.drop_join_table :videos, :musics
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?('musics_videos') }
+ assert_not connection.table_exists?("musics_videos")
end
def test_drop_join_table_with_the_table_name
connection.create_join_table :artists, :musics, table_name: :catalog
connection.drop_join_table :artists, :musics, table_name: :catalog
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') }
+ assert_not connection.table_exists?("catalog")
end
def test_drop_join_table_with_the_table_name_as_string
- connection.create_join_table :artists, :musics, table_name: 'catalog'
- connection.drop_join_table :artists, :musics, table_name: 'catalog'
+ connection.create_join_table :artists, :musics, table_name: "catalog"
+ connection.drop_join_table :artists, :musics, table_name: "catalog"
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') }
+ assert_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 }
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?('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'
- ActiveSupport::Deprecation.silence { assert connection.table_exists?('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'
- ActiveSupport::Deprecation.silence { assert !connection.table_exists?('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
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
index 01162dcefe..ba21923d79 100644
--- a/activerecord/test/cases/migration/foreign_key_test.rb
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -1,84 +1,246 @@
-require 'cases/helper'
-require 'support/ddl_helper'
-require 'support/schema_dumping_helper'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/schema_dumping_helper"
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
+ module ActiveRecord
+ class Migration
+ class ForeignKeyInCreateTest < ActiveRecord::TestCase
+ def test_foreign_keys
+ foreign_keys = ActiveRecord::Base.connection.foreign_keys("fk_test_has_fk")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "fk_test_has_fk", fk.from_table
+ assert_equal "fk_test_has_pk", fk.to_table
+ assert_equal "fk_id", fk.column
+ assert_equal "pk_id", fk.primary_key
+ assert_equal "fk_name", fk.name unless current_adapter?(:SQLite3Adapter)
+ end
end
- class Astronaut < ActiveRecord::Base
+ class ForeignKeyChangeColumnTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ class Rocket < ActiveRecord::Base
+ has_many :astronauts
+ end
+
+ class Astronaut < ActiveRecord::Base
+ belongs_to :rocket
+ end
+
+ class CreateRocketsMigration < ActiveRecord::Migration::Current
+ def up
+ create_table :rockets do |t|
+ t.string :name
+ end
+
+ create_table :astronauts do |t|
+ t.string :name
+ t.references :rocket, foreign_key: true
+ end
+ end
+
+ def down
+ drop_table :astronauts, if_exists: true
+ drop_table :rockets, if_exists: true
+ end
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @migration = CreateRocketsMigration.new
+ silence_stream($stdout) { @migration.migrate(:up) }
+ Rocket.reset_table_name
+ Rocket.reset_column_information
+ Astronaut.reset_table_name
+ Astronaut.reset_column_information
+ end
+
+ def teardown
+ silence_stream($stdout) { @migration.migrate(:down) }
+ Rocket.reset_table_name
+ Rocket.reset_column_information
+ Astronaut.reset_table_name
+ Astronaut.reset_column_information
+ end
+
+ def test_change_column_of_parent_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.change_column_null Rocket.table_name, :name, false
+
+ foreign_keys = @connection.foreign_keys(Astronaut.table_name)
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "myrocket", Rocket.first.name
+ assert_equal Astronaut.table_name, fk.from_table
+ assert_equal Rocket.table_name, fk.to_table
+ end
+
+ def test_rename_column_of_child_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.rename_column Astronaut.table_name, :name, :astronaut_name
+
+ foreign_keys = @connection.foreign_keys(Astronaut.table_name)
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "myrocket", Rocket.first.name
+ assert_equal Astronaut.table_name, fk.from_table
+ assert_equal Rocket.table_name, fk.to_table
+ end
+
+ def test_rename_reference_column_of_child_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.rename_column Astronaut.table_name, :rocket_id, :new_rocket_id
+
+ foreign_keys = @connection.foreign_keys(Astronaut.table_name)
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "myrocket", Rocket.first.name
+ assert_equal Astronaut.table_name, fk.from_table
+ assert_equal Rocket.table_name, fk.to_table
+ assert_equal "new_rocket_id", fk.options[:column]
+ end
+
+ def test_remove_reference_column_of_child_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.remove_column Astronaut.table_name, :rocket_id
+
+ assert_empty @connection.foreign_keys(Astronaut.table_name)
+ end
+
+ def test_remove_foreign_key_by_column
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.remove_foreign_key Astronaut.table_name, column: :rocket_id
+
+ assert_empty @connection.foreign_keys(Astronaut.table_name)
+ end
+
+ def test_remove_foreign_key_by_column_in_change_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.change_table Astronaut.table_name do |t|
+ t.remove_foreign_key column: :rocket_id
+ end
+
+ assert_empty @connection.foreign_keys(Astronaut.table_name)
+ end
+ end
+
+ class ForeignKeyChangeColumnWithPrefixTest < ForeignKeyChangeColumnTest
+ setup do
+ ActiveRecord::Base.table_name_prefix = "p_"
+ end
+
+ teardown do
+ ActiveRecord::Base.table_name_prefix = nil
+ end
end
- setup do
- @connection = ActiveRecord::Base.connection
- @connection.create_table "rockets", force: true do |t|
- t.string :name
+ class ForeignKeyChangeColumnWithSuffixTest < ForeignKeyChangeColumnTest
+ setup do
+ ActiveRecord::Base.table_name_suffix = "_p"
end
- @connection.create_table "astronauts", force: true do |t|
- t.string :name
- t.references :rocket
+ teardown do
+ ActiveRecord::Base.table_name_suffix = nil
end
end
+ end
+ end
- teardown do
- if defined?(@connection)
+ 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
- end
- def test_foreign_keys
- foreign_keys = @connection.foreign_keys("fk_test_has_fk")
- assert_equal 1, foreign_keys.size
+ 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
+ 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
- def test_add_foreign_key_inferes_column
- @connection.add_foreign_key :astronauts, :rockets
+ 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 "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
+ 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 unless current_adapter?(:SQLite3Adapter)
+ end
- def test_add_foreign_key_with_column
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+ 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
+ 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
+ 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 unless current_adapter?(:SQLite3Adapter)
+ 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 +249,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", to_table: "space_shuttles"
+ @connection.drop_table :space_shuttles
+ end
- @connection.remove_foreign_key :astronauts, name: "custom_pk"
+ 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
+
+ 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_restrict_foreign_key
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict
+ 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
- if current_adapter?(: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
+ assert_equal :cascade, fk.on_delete
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_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 :cascade, fk.on_delete
- end
+ fk = foreign_keys.first
+ assert_equal :nullify, 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_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
- foreign_keys = @connection.foreign_keys("astronauts")
- assert_equal 1, foreign_keys.size
+ def test_add_foreign_key_with_on_update
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify
- fk = foreign_keys.first
- assert_equal :nullify, fk.on_delete
- end
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
- 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
+ fk = foreign_keys.first
+ assert_equal :nullify, fk.on_update
end
- assert_raises ArgumentError do
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid
+ def test_foreign_key_exists
+ @connection.add_foreign_key :astronauts, :rockets
+
+ assert @connection.foreign_key_exists?(:astronauts, :rockets)
+ assert_not @connection.foreign_key_exists?(:astronauts, :stars)
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_by_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
- foreign_keys = @connection.foreign_keys("astronauts")
- assert_equal 1, foreign_keys.size
+ assert @connection.foreign_key_exists?(:astronauts, column: "rocket_id")
+ assert_not @connection.foreign_key_exists?(:astronauts, column: "star_id")
+ end
- fk = foreign_keys.first
- assert_equal :nullify, fk.on_update
- end
+ def test_foreign_key_exists_by_name
+ skip if current_adapter?(:SQLite3Adapter)
- def test_foreign_key_exists
- @connection.add_foreign_key :astronauts, :rockets
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
- assert @connection.foreign_key_exists?(:astronauts, :rockets)
- assert_not @connection.foreign_key_exists?(:astronauts, :stars)
- end
+ assert @connection.foreign_key_exists?(:astronauts, name: "fancy_named_fk")
+ assert_not @connection.foreign_key_exists?(:astronauts, name: "other_fancy_named_fk")
+ end
- def test_foreign_key_exists_by_column
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+ def test_remove_foreign_key_inferes_column
+ @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_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
+ skip if current_adapter?(:SQLite3Adapter)
- assert_equal 1, @connection.foreign_keys("astronauts").size
- @connection.remove_foreign_key :astronauts, column: "rocket_id"
- assert_equal [], @connection.foreign_keys("astronauts")
- end
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
- 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, name: "fancy_named_fk"
+ 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_non_existing_foreign_key_raises
+ e = assert_raises ArgumentError do
+ @connection.remove_foreign_key :astronauts, :rockets
+ end
+ assert_equal "Table 'astronauts' has no foreign key for rockets", e.message
+ end
- def test_remove_foreign_key_by_name
- @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
+ def test_remove_foreign_key_by_the_select_one_on_the_same_table
+ @connection.add_foreign_key :astronauts, :rockets
+ @connection.add_reference :astronauts, :myrocket, foreign_key: { to_table: :rockets }
- assert_equal 1, @connection.foreign_keys("astronauts").size
- @connection.remove_foreign_key :astronauts, name: "fancy_named_fk"
- assert_equal [], @connection.foreign_keys("astronauts")
- end
+ assert_equal 2, @connection.foreign_keys("astronauts").size
- def test_remove_foreign_non_existing_foreign_key_raises
- assert_raises ArgumentError do
- @connection.remove_foreign_key :astronauts, :rockets
+ @connection.remove_foreign_key :astronauts, :rockets, column: "myrocket_id"
+
+ assert_equal [["astronauts", "rockets", "rocket_id"]],
+ @connection.foreign_keys("astronauts").map { |fk| [fk.from_table, fk.to_table, fk.column] }
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
+ 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_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
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
- 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
+ fk = foreign_keys.first
+ assert_not_predicate fk, :validated?
+ end
- output = dump_table_schema "astronauts"
- assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output
- end
+ def test_validate_foreign_key_infers_column
+ @connection.add_foreign_key :astronauts, :rockets, validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, :rockets
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_key_by_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, column: "rocket_id"
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_key_by_symbol_column
+ @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id, validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, column: :rocket_id
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_key_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, name: "fancy_named_fk"
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_non_existing_foreign_key_raises
+ assert_raises ArgumentError do
+ @connection.validate_foreign_key :astronauts, :rockets
+ end
+ end
- class CreateCitiesAndHousesMigration < ActiveRecord::Migration::Current
- def change
- create_table("cities") { |t| }
+ def test_validate_constraint_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false
- create_table("houses") do |t|
- t.column :city_id, :integer
+ @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
- 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_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"
+ if current_adapter?(:SQLite3Adapter)
+ assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id"$}, output
+ else
+ assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
+ end
+ end
+
+ def test_schema_dumping_with_custom_fk_ignore_pattern
+ original_pattern = ActiveRecord::SchemaDumper.fk_ignore_pattern
+ ActiveRecord::SchemaDumper.fk_ignore_pattern = /^ignored_/
+ @connection.add_foreign_key :astronauts, :rockets, name: :ignored_fk_astronauts_rockets
- class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current
- def change
- create_table(:schools)
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output
- create_table(:classes) do |t|
- t.column :school_id, :integer
+ ActiveRecord::SchemaDumper.fk_ignore_pattern = original_pattern
+ end
+
+ def test_schema_dumping_on_delete_and_on_update_options
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade
+
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output
+ end
+
+ 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 :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_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_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
+ 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
- end
- end
-end
-else
-module ActiveRecord
- class Migration
- class NoForeignKeySupportTest < ActiveRecord::TestCase
- setup do
- @connection = ActiveRecord::Base.connection
- end
+ class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current
+ def up
+ create_table(:schools)
- def test_add_foreign_key_should_be_noop
- @connection.add_foreign_key :clubs, :categories
- end
+ create_table(:classes) do |t|
+ t.references :school
+ end
+ add_foreign_key :classes, :schools
+ end
- def test_remove_foreign_key_should_be_noop
- @connection.remove_foreign_key :clubs, :categories
- end
+ def down
+ drop_table :classes, if_exists: true
+ drop_table :schools, if_exists: true
+ end
+ end
- def test_foreign_keys_should_raise_not_implemented
- assert_raises NotImplementedError do
- @connection.foreign_keys("clubs")
+ 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
end
end
end
end
end
-end
diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb
index ad85684c0b..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
@@ -33,7 +35,7 @@ module ActiveRecord
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 5abd37bfa2..5e688efc2b 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,29 +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)
- assert connection.index_exists?(:testings, :foo, :name => "custom_index_name")
- assert !connection.index_exists?(:testings, :foo, :name => "other_index_name")
+ 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 => "custom_index_name"
+ 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 !connection.index_exists?(: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
@@ -154,65 +156,61 @@ 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"])
-
- # 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.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", "last_name_and_first_name")
- end
+ connection.remove_index("testings", column: ["last_name", "first_name"])
+
+ connection.add_index("testings", ["last_name", "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")
+
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, :Mysql2Adapter, :PostgreSQLAdapter)
- connection.add_index("testings", ["last_name"], :order => {:last_name => :desc})
+ 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 4f5589f32a..119bfd372a 100644
--- a/activerecord/test/cases/migration/pending_migrations_test.rb
+++ b/activerecord/test/cases/migration/pending_migrations_test.rb
@@ -1,50 +1,40 @@
-require 'cases/helper'
+# 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 85435f4dbc..90a50a5651 100644
--- a/activerecord/test/cases/migration/references_foreign_key_test.rb
+++ b/activerecord/test/cases/migration/references_foreign_key_test.rb
@@ -1,130 +1,155 @@
-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?
+ 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 "foreign keys can be created in one query when index is not added" do
- assert_queries(1) do
+ test "no foreign key is created by default" do
@connection.create_table :testings do |t|
- t.references :testing_parent, foreign_key: true, index: false
+ t.references :testing_parent
end
- end
- 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 "to_table option can be passed" do
- @connection.create_table :testings do |t|
- t.references :parent, foreign_key: { to_table: :testing_parents }
+ fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" }
+ assert_equal "other_id", fk.primary_key
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
- 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
+ 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
- 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
+ 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
+
+ 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
+ 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
+ test "foreign key methods respect pluralize_table_names" do
original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names
ActiveRecord::Base.pluralize_table_names = false
@connection.create_table :testing
@@ -143,44 +168,71 @@ module ActiveRecord
ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names
@connection.drop_table "testing", if_exists: true
end
- end
- test "multiple foreign keys can be added to the same table" do
- @connection.create_table :testings do |t|
- t.integer :col_1
- t.integer :col_2
+ class CreateDogsMigration < ActiveRecord::Migration::Current
+ def up
+ create_table :dog_owners
- t.foreign_key :testing_parents, column: :col_1
- t.foreign_key :testing_parents, column: :col_2
+ create_table :dogs do |t|
+ t.references :dog_owner, foreign_key: true
+ end
+ end
+
+ def down
+ drop_table :dogs, if_exists: true
+ drop_table :dog_owners, if_exists: true
+ end
end
- fks = @connection.foreign_keys("testings")
+ def test_references_foreign_key_with_prefix
+ ActiveRecord::Base.table_name_prefix = "p_"
+ migration = CreateDogsMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("p_dogs").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ ActiveRecord::Base.table_name_prefix = nil
+ end
- fk_definitions = fks.map {|fk| [fk.from_table, fk.to_table, fk.column] }
- assert_equal([["testings", "testing_parents", "col_1"],
- ["testings", "testing_parents", "col_2"]], fk_definitions)
- end
- end
- end
-end
-else
-class ReferencesWithoutForeignKeySupportTest < ActiveRecord::TestCase
- setup do
- @connection = ActiveRecord::Base.connection
- @connection.create_table(:testing_parents, force: true)
- 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
- teardown do
- @connection.drop_table("testings", if_exists: true)
- @connection.drop_table("testing_parents", if_exists: true)
- 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
- test "ignores foreign keys defined with the table" do
- @connection.create_table :testings do |t|
- t.references :testing_parent, foreign_key: true
- end
+ fks = @connection.foreign_keys("testings").sort_by(&:column)
- assert_includes @connection.data_sources, "testings"
+ 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
-end
diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb
index a9a7f0f4c4..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,10 +19,10 @@ 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_creates_index_by_default_even_if_index_option_is_not_passed
@@ -28,31 +30,31 @@ module ActiveRecord
t.references :foo
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_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,10 +64,10 @@ 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_creates_index_for_existing_table_even_if_index_option_is_not_passed
@@ -74,23 +76,23 @@ module ActiveRecord
t.references :foo
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_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 b9ce6bbc55..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
@@ -35,7 +37,7 @@ module ActiveRecord
assert_not index_exists?(table_name, :user_id)
end
- def test_create_reference_id_index_even_if_index_option_is_passed
+ def test_create_reference_id_index_even_if_index_option_is_not_passed
add_reference table_name, :user
assert index_exists?(table_name, :user_id)
end
@@ -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 b926a92849..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,13 +11,13 @@ 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
- ActiveSupport::Deprecation.silence { rename_table :octopi, :test_models if connection.table_exists? :octopi }
+ rename_table :octopi, :test_models if connection.table_exists? :octopi
super
end
@@ -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,15 +76,38 @@ 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
- connection.create_table :cats, id: :uuid
+ connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()"
assert_nothing_raised { rename_table :cats, :felines }
- ActiveSupport::Deprecation.silence { assert connection.table_exists? :felines }
+ assert connection.table_exists? :felines
+ assert_not connection.table_exists? :cats
ensure
connection.drop_table :cats, if_exists: true
connection.drop_table :felines, if_exists: true
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 5a6d2ce80c..788c8c36b8 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -1,12 +1,14 @@
-require 'cases/helper'
-require 'cases/migration/helper'
-require 'bigdecimal/util'
-require 'concurrent/atomic/count_down_latch'
+# frozen_string_literal: true
-require 'models/person'
-require 'models/topic'
-require 'models/developer'
-require 'models/computer'
+require "cases/helper"
+require "cases/migration/helper"
+require "bigdecimal/util"
+require "concurrent/atomic/count_down_latch"
+
+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"
@@ -43,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
@@ -59,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
@@ -69,52 +71,87 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Migration.verbose = @verbose_was
end
+ def test_passing_migrations_paths_to_assume_migrated_upto_version_is_deprecated
+ ActiveRecord::SchemaMigration.create_table
+ assert_deprecated do
+ ActiveRecord::Base.connection.assume_migrated_upto_version(0, [])
+ 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 false, ActiveRecord::Migrator.needs_migration?
+ migrator.up
+ assert_equal 3, migrator.current_version
+ assert_equal false, migrator.needs_migration?
- ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid")
- assert_equal 0, ActiveRecord::Migrator.current_version
- assert_equal true, ActiveRecord::Migrator.needs_migration?
+ migrator.down
+ assert_equal 0, migrator.current_version
+ assert_equal true, migrator.needs_migration?
ActiveRecord::SchemaMigration.create!(version: 3)
- assert_equal true, ActiveRecord::Migrator.needs_migration?
- ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ 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
- assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) }
+ migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/version_check")
+ assert_equal 0, migrator.current_version
+ migrator.up(20131219224947)
+ assert_equal 20131219224947, migrator.current_version
+ end
+
+ def test_create_table_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
@@ -124,7 +161,7 @@ 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
@@ -151,16 +188,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
@@ -172,11 +209,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
@@ -200,7 +235,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
@@ -210,15 +245,16 @@ class MigrationTest < ActiveRecord::TestCase
def test_filtering_migrations
assert_no_column Person, :last_name
- assert !Reminder.table_exists?
+ assert_not_predicate Reminder, :table_exists?
name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" }
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter)
+ migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid")
+ migrator.up(&name_filter)
assert_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
- ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter)
+ migrator.down(&name_filter)
assert_no_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
@@ -244,22 +280,22 @@ 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?
@@ -270,7 +306,7 @@ class MigrationTest < ActiveRecord::TestCase
def version; 100 end
def migrate(x)
add_column "people", "last_name", :string
- raise 'Something broke'
+ raise "Something broke"
end
}.new
@@ -291,7 +327,7 @@ class MigrationTest < ActiveRecord::TestCase
def version; 100 end
def migrate(x)
add_column "people", "last_name", :string
- raise 'Something broke'
+ raise "Something broke"
end
}.new
@@ -309,12 +345,12 @@ class MigrationTest < ActiveRecord::TestCase
assert_no_column Person, :last_name
migration = Class.new(ActiveRecord::Migration::Current) {
- self.disable_ddl_transaction!
+ disable_ddl_transaction!
def version; 101 end
def migrate(x)
add_column "people", "last_name", :string
- raise 'Something broke'
+ raise "Something broke"
end
}.new
@@ -326,27 +362,27 @@ 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
@@ -375,74 +411,44 @@ class MigrationTest < ActiveRecord::TestCase
def test_internal_metadata_stores_environment
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
migrations_path = MIGRATIONS_ROOT + "/valid"
- old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = migrations_path
+ migrator = ActiveRecord::MigrationContext.new(migrations_path)
- ActiveRecord::Migrator.up(migrations_path)
+ migrator.up
assert_equal current_env, ActiveRecord::InternalMetadata[:environment]
original_rails_env = ENV["RAILS_ENV"]
original_rack_env = ENV["RACK_ENV"]
ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo"
- new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+ new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
- refute_equal current_env, new_env
+ assert_not_equal current_env, new_env
sleep 1 # mysql by default does not store fractional seconds in the database
- ActiveRecord::Migrator.up(migrations_path)
+ migrator.up
assert_equal new_env, ActiveRecord::InternalMetadata[:environment]
ensure
- ActiveRecord::Migrator.migrations_paths = old_path
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"
- def test_migration_sets_internal_metadata_even_when_fully_migrated
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
migrations_path = MIGRATIONS_ROOT + "/valid"
- old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = migrations_path
- ActiveRecord::Migrator.up(migrations_path)
+ 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
-
- refute_equal current_env, new_env
-
- sleep 1 # mysql by default does not store fractional seconds in the database
-
- ActiveRecord::Migrator.up(migrations_path)
- assert_equal new_env, ActiveRecord::InternalMetadata[:environment]
- ensure
- ActiveRecord::Migrator.migrations_paths = old_path
- ENV["RAILS_ENV"] = original_rails_env
- ENV["RACK_ENV"] = original_rack_env
- end
-
- def test_rename_internal_metadata_table
- original_internal_metadata_table_name = ActiveRecord::Base.internal_metadata_table_name
-
- ActiveRecord::Base.internal_metadata_table_name = "active_record_internal_metadatas"
- Reminder.reset_table_name
-
- ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name
- Reminder.reset_table_name
-
- assert_equal "ar_internal_metadata", ActiveRecord::InternalMetadata.table_name
- ensure
- ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name
- Reminder.reset_table_name
+ 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
@@ -451,26 +457,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
@@ -489,9 +495,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
@@ -508,7 +514,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
}
@@ -522,11 +528,10 @@ class MigrationTest < ActiveRecord::TestCase
unless mysql_enforcing_gtid_consistency?
def test_create_table_with_query
- 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
@@ -534,11 +539,10 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_create_table_with_query_from_relation
- 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
@@ -546,58 +550,48 @@ class MigrationTest < ActiveRecord::TestCase
end
end
- if current_adapter? :OracleAdapter
- def test_create_table_with_custom_sequence_name
- # table name is 29 chars, the standard sequence name will
- # be 33 chars and should be shortened
- assert_nothing_raised do
- begin
- Person.connection.create_table :table_with_name_thats_just_ok do |t|
- t.column :foo, :string, :null => false
- end
- ensure
- Person.connection.drop_table :table_with_name_thats_just_ok rescue nil
- end
- end
-
- # should be all good w/ a custom sequence name
- 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
- 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
- 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
- # confirm the custom sequence got dropped
- assert_raise(ActiveRecord::StatementInvalid) do
- Person.connection.execute("select suitably_short_seq.nextval from dual")
+ def test_decimal_scale_without_precision_should_raise
+ e = assert_raise(ArgumentError) do
+ Person.connection.create_table :test_decimal_scales, force: true do |t|
+ t.decimal :scaleonly, scale: 10
end
end
+
+ assert_equal "Error adding decimal column: precision cannot be empty if scale is specified", e.message
+ ensure
+ Person.connection.drop_table :test_decimal_scales, if_exists: true
end
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)
+ assert_includes e.message, "No integer type has byte size 10"
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|
@@ -605,12 +599,38 @@ class MigrationTest < ActiveRecord::TestCase
end
end
- assert_match(/No text type has byte length #{0xfffffffff}/, e.message)
+ assert_includes e.message, "No text type has byte size #{0xfffffffff}"
+ ensure
+ Person.connection.drop_table :test_text_limits, if_exists: true
+ end
+
+ def test_out_of_range_binary_limit_should_raise
+ e = assert_raise(ActiveRecord::ActiveRecordError) do
+ Person.connection.create_table :test_text_limits, force: true do |t|
+ t.binary :bigbinary, limit: 0xfffffffff
+ end
+ end
+
+ assert_includes e.message, "No binary type has byte size #{0xfffffffff}"
ensure
Person.connection.drop_table :test_text_limits, if_exists: true
end
end
+ if current_adapter?(:Mysql2Adapter)
+ def test_invalid_text_size_should_raise
+ e = assert_raise(ArgumentError) do
+ Person.connection.create_table :test_text_sizes, force: true do |t|
+ t.text :bigtext, size: 0xfffffffff
+ end
+ end
+
+ assert_equal "#{0xfffffffff} is invalid :size value. Only :tiny, :medium, and :long are allowed.", e.message
+ ensure
+ Person.connection.drop_table :test_text_sizes, if_exists: true
+ end
+ end
+
if ActiveRecord::Base.connection.supports_advisory_locks?
def test_migrator_generates_valid_lock_id
migration = Class.new(ActiveRecord::Migration::Current).new
@@ -681,9 +701,28 @@ class MigrationTest < ActiveRecord::TestCase
assert_no_column Person, :last_name,
"without an advisory lock, the Migrator should not make any changes, but it did."
end
+
+ def test_with_advisory_lock_raises_the_right_error_when_it_fails_to_release_lock
+ migration = Class.new(ActiveRecord::Migration::Current).new
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+ lock_id = migrator.send(:generate_migrator_advisory_lock_id)
+
+ e = assert_raises(ActiveRecord::ConcurrentMigrationError) do
+ silence_stream($stderr) do
+ migrator.send(:with_advisory_lock) do
+ ActiveRecord::Base.connection.release_advisory_lock(lock_id)
+ end
+ end
+ end
+
+ assert_match(
+ /#{ActiveRecord::ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE}/,
+ e.message
+ )
+ end
end
- protected
+ private
# This is needed to isolate class_attribute assignments like `table_name_prefix`
# for each test case.
def new_isolated_reminder_class
@@ -698,15 +737,13 @@ class MigrationTest < ActiveRecord::TestCase
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
+ 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
thread_lock.wait # wait until the 'other process' has the lock
@@ -721,13 +758,13 @@ 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
@@ -742,8 +779,8 @@ class ExplicitlyNamedIndexMigrationTest < ActiveRecord::TestCase
end
assert_nothing_raised do
- connection.add_index :values, :value, name: 'a_different_name'
- connection.remove_index :values, column: :value, name: 'a_different_name'
+ 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
@@ -754,7 +791,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
@@ -764,19 +801,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
@@ -784,7 +830,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|
@@ -793,7 +839,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
@@ -804,10 +850,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
@@ -815,8 +868,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
@@ -829,14 +882,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
@@ -848,51 +909,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
@@ -912,18 +978,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
@@ -954,7 +1020,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",
@@ -962,9 +1028,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
@@ -999,14 +1065,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
@@ -1017,17 +1083,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
@@ -1042,7 +1108,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
@@ -1060,8 +1126,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
@@ -1074,7 +1140,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
@@ -1089,7 +1155,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
@@ -1101,7 +1167,7 @@ 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
diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb
index 86eca53141..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"
@@ -9,9 +11,9 @@ class MigratorTest < ActiveRecord::TestCase
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
- ActiveSupport::Deprecation.silence { assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') }
- migrator.migrate("valid", 1)
- ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?('schema_migrations') }
+ assert_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 486bcc22df..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,14 +52,14 @@ 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
@@ -73,8 +75,8 @@ class ModulesTest < ActiveRecord::TestCase
clients = []
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)
+ 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 af4183a601..f11c441c65 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?
@@ -102,14 +106,12 @@ class MultipleDbTest < ActiveRecord::TestCase
end
def test_associations_should_work_when_model_has_no_connection
- begin
- ActiveRecord::Base.remove_connection
- assert_nothing_raised do
- College.first.courses.first
- end
- ensure
- ActiveRecord::Base.establish_connection :arunit
+ ActiveRecord::Base.remove_connection
+ assert_nothing_raised do
+ College.first.courses.first
end
+ ensure
+ ActiveRecord::Base.establish_connection :arunit
end
end
end
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index 11fb164d50..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
@@ -63,7 +65,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
def test_should_raise_an_UnknownAttributeError_for_non_existing_nested_attributes
exception = assert_raise ActiveModel::UnknownAttributeError do
- Pirate.new(:ship_attributes => { :sail => true })
+ Pirate.new(ship_attributes: { sail: true })
end
assert_equal "unknown attribute 'sail' for Ship.", exception.message
end
@@ -72,85 +74,85 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
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 { 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
@@ -168,27 +170,27 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
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 => "" }
+ 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
@@ -196,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
@@ -212,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
@@ -236,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
@@ -257,54 +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.stub(:id, 'ABC1X') do
- @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
@@ -313,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
@@ -321,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
@@ -362,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
@@ -421,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
@@ -442,53 +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.stub(:id, 'ABC1X') do
- @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
@@ -509,33 +522,33 @@ 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 { @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' })
+ @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 } }
+ @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) }
@@ -547,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
@@ -597,10 +610,9 @@ module NestedAttributesOnACollectionAssociationTests
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
@@ -608,90 +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.stub(:id, 'ABC1X') do
- @child_2.stub(:id, 'ABC2X') do
-
+ @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' }
+ { 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]
+ 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 }] }
@@ -702,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 do
- @pirate.send(association_setter, { 'foo' => { '_destroy' => '0' }})
+ @pirate.send(association_setter, "foo" => { "_destroy" => "0" })
end
end
@@ -722,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
@@ -740,11 +751,11 @@ 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
@@ -754,51 +765,51 @@ module NestedAttributesOnACollectionAssociationTests
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
@@ -812,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
@@ -823,7 +834,7 @@ module NestedAttributesOnACollectionAssociationTests
end
def test_can_use_symbols_as_object_identifier
- @pirate.attributes = { :parrots_attributes => { :foo => { :name => 'Lovely Day' }, :bar => { :name => 'Blown Away' } } }
+ @pirate.attributes = { parrots_attributes: { foo: { name: "Lovely Day" }, bar: { name: "Blown Away" } } }
assert_nothing_raised { @pirate.save! }
end
@@ -832,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
@@ -855,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
@@ -877,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
@@ -896,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
@@ -930,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
@@ -940,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
@@ -952,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
@@ -1000,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
@@ -1027,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
@@ -1057,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
@@ -1092,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 56092aaa0c..d5057ad381 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -1,101 +1,87 @@
+# 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/ship'
-require 'models/toy'
-require 'models/admin'
-require 'models/admin/user'
-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_delete_all_with_joins_and_where_part_is_hash
- where_args = {:toys => {:name => 'Bone'}}
- count = Pet.joins(:toys).where(where_args).count
+ def test_update_many_with_invalid_id
+ topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" }, 99999 => {} }
- assert_equal count, 1
- assert_equal count, Pet.joins(:toys).where(where_args).delete_all
+ 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_not_hash
- where_args = ['toys.name = ?', 'Bone']
- count = Pet.joins(:toys).where(where_args).count
+ def test_class_level_update_without_ids
+ topics = Topic.all
+ assert_equal 5, topics.length
+ topics.each do |topic|
+ assert_not_equal "updated", topic.content
+ end
+
+ updated = Topic.update(content: "updated")
+ assert_equal 5, updated.length
+ updated.each do |topic|
+ assert_equal "updated", topic.content
+ end
+ end
+
+ def test_class_level_update_is_affected_by_scoping
+ topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } }
+
+ assert_raise(ActiveRecord::RecordNotFound) do
+ Topic.where("1=0").scoping { Topic.update(topic_data.keys, topic_data.values) }
+ end
- assert_equal count, 1
- assert_equal count, Pet.joins(:toys).where(where_args).delete_all
+ assert_not_equal "1 updated", Topic.find(1).content
+ assert_not_equal "2 updated", Topic.find(2).content
+ end
+
+ def test_delete_all
+ assert Topic.count > 0
+
+ assert_equal Topic.count, Topic.delete_all
end
def test_increment_attribute
@@ -131,36 +117,70 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal initial_credit + 2, a1.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_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.where(conditions).destroy_all.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.keys, client.errors.keys
@@ -170,7 +190,7 @@ class PersistenceTest < ActiveRecord::TestCase
child_class = Class.new(Admin::User) do
store_accessor :settings, :foo
- def self.name; 'Admin::ChildUser'; end
+ def self.name; "Admin::ChildUser"; end
end
admin = Admin::User.new
@@ -190,6 +210,17 @@ class PersistenceTest < ActiveRecord::TestCase
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)
@@ -222,6 +253,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"
@@ -231,7 +286,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
@@ -261,7 +316,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
@@ -269,8 +324,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
@@ -281,12 +336,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
@@ -330,9 +386,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
@@ -369,9 +427,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
@@ -386,25 +460,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
@@ -413,103 +485,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
@@ -529,14 +636,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
@@ -569,17 +676,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
@@ -598,35 +705,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
@@ -643,35 +750,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
@@ -697,18 +804,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
@@ -726,62 +833,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
error = assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do
- topic.update_attributes(id: 3, title: "Hm is it possible?")
+ topic.update(id: 3, title: "Hm is it possible?")
end
assert_not_nil error.cause
assert_not_equal "Hm is it possible?", Topic.find(3).title
- topic.update_attributes(id: 1234)
+ topic.update(id: 1234)
assert_nothing_raised { topic.reload }
assert_equal topic.title, Topic.find(1234).title
end
- def test_update_attributes_parameters
+ def test_update_attributes
+ topic = Topic.find(1)
+ assert_deprecated do
+ topic.update_attributes("title" => "The First Topic Updated")
+ end
+ end
+
+ def test_update_parameters
topic = Topic.find(1)
assert_nothing_raised do
- topic.update_attributes({})
+ topic.update({})
end
assert_raises(ArgumentError) do
- topic.update_attributes(nil)
+ topic.update(nil)
end
end
@@ -807,24 +905,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
@@ -840,7 +924,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?
@@ -860,18 +944,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
@@ -909,7 +1016,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]
@@ -918,20 +1025,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)
@@ -940,60 +1047,48 @@ 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
-
- 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
+ parrot.name = "Barb"
+ parrot.save!(touch: false)
+ assert_equal parrot.created_at, created_at
+ assert_equal parrot.updated_at, updated_at
end
def test_reset_column_information_resets_children
- child = Class.new(Topic)
- child.new # force schema to load
+ child_class = Class.new(Topic)
+ child_class.new # force schema to load
ActiveRecord::Base.connection.add_column(:topics, :foo, :string)
Topic.reset_column_information
- assert_equal "bar", child.new(foo: :bar).foo
+ # this should redefine attribute methods
+ child_class.new
+
+ assert child_class.instance_methods.include?(:foo)
+ assert child_class.instance_methods.include?(:foo_changed?)
+ assert_equal "bar", child_class.new(foo: :bar).foo
ensure
ActiveRecord::Base.connection.remove_column(:topics, :foo)
Topic.reset_column_information
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index bca50dd008..080aeb0989 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,36 +20,32 @@ 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
Thread.new do
- begin
- conn = ActiveRecord::Base.connection_pool.checkout
- sleep 0.1
- ActiveRecord::Base.connection_pool.checkin conn
- @connection_count += 1
- rescue ActiveRecord::ConnectionTimeoutError
- @timed_out += 1
- end
+ conn = ActiveRecord::Base.connection_pool.checkout
+ sleep 0.1
+ ActiveRecord::Base.connection_pool.checkin conn
+ @connection_count += 1
+ rescue ActiveRecord::ConnectionTimeoutError
+ @timed_out += 1
end.join
end
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
- begin
- conn = ActiveRecord::Base.connection_pool.checkout
- ActiveRecord::Base.connection_pool.checkin conn
- @connection_count += 1
- ActiveRecord::Base.connection.data_sources
- rescue ActiveRecord::ConnectionTimeoutError
- @timed_out += 1
- end
+ conn = ActiveRecord::Base.connection_pool.checkout
+ ActiveRecord::Base.connection_pool.checkin conn
+ @connection_count += 1
+ ActiveRecord::Base.connection.data_sources
+ rescue ActiveRecord::ConnectionTimeoutError
+ @timed_out += 1
end
end
@@ -66,7 +64,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 +73,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 32bccce2ed..4759d3b6b2 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,48 +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 do
- ActiveRecord::Base.connection.supports_primary_key?
+ def test_primary_key_returns_value_if_it_exists
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "developers"
end
- end
- if ActiveRecord::Base.connection.supports_primary_key?
- def test_primary_key_returns_value_if_it_exists
- klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'developers'
- end
+ assert_equal "id", klass.primary_key
+ end
- assert_equal 'id', klass.primary_key
+ def test_primary_key_returns_nil_if_it_does_not_exist
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "developers_projects"
end
- def test_primary_key_returns_nil_if_it_does_not_exist
- klass = Class.new(ActiveRecord::Base) do
- self.table_name = 'developers_projects'
- end
-
- assert_nil klass.primary_key
- end
+ 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
@@ -166,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
@@ -197,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
@@ -229,14 +295,25 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
assert_equal "code", Barcode.primary_key
column = Barcode.column_for_attribute(Barcode.primary_key)
- assert_not column.null unless current_adapter?(:SQLite3Adapter)
+ 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
@@ -247,75 +324,91 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
- @connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t|
+ @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(:barcodes, if_exists: true)
+ @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_composite_primary_key
- assert_equal ["region", "code"], @connection.primary_keys("barcodes")
+ 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
+ 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 @connection.primary_key("barcodes")
+ assert_nil model.primary_key
end
- assert_match(/WARNING: Rails does not support composite primary key\./, warning)
+ assert_match(/WARNING: Active Record does not support composite primary key\./, warning)
end
def test_collectly_dump_composite_primary_key
- schema = dump_table_schema "barcodes"
- assert_match %r{create_table "barcodes", primary_key: \["region", "code"\]}, schema
+ schema = dump_table_schema "uber_barcodes"
+ assert_match %r{create_table "uber_barcodes", primary_key: \["region", "code"\]}, schema
end
-end
-if current_adapter?(:Mysql2Adapter)
- class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase
- self.use_transactional_tests = false
-
- 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!
- end
+ def test_dumping_composite_primary_key_out_of_order
+ schema = dump_table_schema "barcodes_reverse"
+ assert_match %r{create_table "barcodes_reverse", primary_key: \["code", "region"\]}, schema
end
+end
- class PrimaryKeyBigintNilDefaultTest < ActiveRecord::TestCase
- include SchemaDumpingHelper
+class PrimaryKeyIntegerNilDefaultTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
- self.use_transactional_tests = false
+ self.use_transactional_tests = false
- def setup
- @connection = ActiveRecord::Base.connection
- @connection.create_table(:bigint_defaults, id: :bigint, default: nil, force: true)
- end
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
- def teardown
- @connection.drop_table :bigint_defaults, if_exists: true
- end
+ def teardown
+ @connection.drop_table :int_defaults, if_exists: true
+ end
- test "primary key with bigint allows default override via nil" do
- column = @connection.columns(:bigint_defaults).find { |c| c.name == 'id' }
- assert column.bigint?
- assert_not column.auto_increment?
- end
+ def test_schema_dump_primary_key_integer_with_default_nil
+ skip if current_adapter?(:SQLite3Adapter)
+ @connection.create_table(:int_defaults, id: :integer, default: nil, force: true)
+ schema = dump_table_schema "int_defaults"
+ assert_match %r{create_table "int_defaults", id: :integer, default: nil}, schema
+ end
- test "schema dump primary key with bigint default nil" do
- schema = dump_table_schema "bigint_defaults"
- assert_match %r{create_table "bigint_defaults", id: :bigint, default: nil}, schema
- end
+ def test_schema_dump_primary_key_bigint_with_default_nil
+ @connection.create_table(:int_defaults, id: :bigint, default: nil, force: true)
+ schema = dump_table_schema "int_defaults"
+ assert_match %r{create_table "int_defaults", id: :bigint, default: nil}, schema
end
end
if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter)
- class PrimaryKeyBigSerialTest < ActiveRecord::TestCase
+ class PrimaryKeyIntegerTest < ActiveRecord::TestCase
include SchemaDumpingHelper
self.use_transactional_tests = false
@@ -325,46 +418,55 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter)
setup do
@connection = ActiveRecord::Base.connection
- if current_adapter?(:PostgreSQLAdapter)
- @connection.create_table(:widgets, id: :bigserial, force: true)
- else
- @connection.create_table(:widgets, id: :bigint, force: true)
- end
+ @pk_type = current_adapter?(:PostgreSQLAdapter) ? :serial : :integer
end
teardown do
@connection.drop_table :widgets, if_exists: true
- Widget.reset_column_information
end
- test "primary key column type with bigserial" do
- column_type = Widget.type_for_attribute(Widget.primary_key)
- assert_equal :integer, column_type.type
- assert_equal 8, column_type.limit
+ test "primary key column type with serial/integer" do
+ @connection.create_table(:widgets, id: @pk_type, force: true)
+ column = @connection.columns(:widgets).find { |c| c.name == "id" }
+ assert_equal :integer, column.type
+ assert_not_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, force: :cascade}, schema
- else
- assert_match %r{create_table "widgets", id: :bigint, force: :cascade}, schema
- end
+ assert_match %r{create_table "widgets", id: :#{@pk_type}, }, schema
end
if current_adapter?(:Mysql2Adapter)
test "primary key column type with options" do
- @connection.create_table(:widgets, id: :primary_key, limit: 8, unsigned: true, force: true)
- 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 column.unsigned?
+ assert_not_predicate column, :bigint?
+ assert_predicate column, :unsigned?
+
+ schema = dump_table_schema "widgets"
+ assert_match %r{create_table "widgets", id: :integer, unsigned: true, }, schema
+ 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 e53239cdee..eb32b690aa 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -1,54 +1,152 @@
+# 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 = 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
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'
+ assert_cache :off
end
- def test_exceptional_middleware_leaves_enabled_cache_alone
- ActiveRecord::Base.connection.enable_query_cache!
+ def test_query_cache_is_applied_to_connections_in_all_handlers
+ ActiveRecord::Base.connection_handlers = {
+ writing: ActiveRecord::Base.default_connection_handler,
+ reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new
+ }
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"])
+ end
mw = middleware { |env|
- raise "lol borked"
+ ro_conn = ActiveRecord::Base.connection_handlers[:reading].connection_pool_list.first.connection
+ assert_predicate ActiveRecord::Base.connection, :query_cache_enabled
+ assert_predicate ro_conn, :query_cache_enabled
}
- assert_raises(RuntimeError) { mw.call({}) }
- assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on'
+ mw.call({})
+ ensure
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
end
- def test_exceptional_middleware_assigns_original_connection_id_on_error
- connection_id = ActiveRecord::Base.connection_id
+ def test_query_cache_across_threads
+ with_temporary_connection_pool do
+ 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
- mw = middleware { |env|
- ActiveRecord::Base.connection_id = self.object_id
- raise "lol borked"
- }
- assert_raises(RuntimeError) { mw.call({}) }
+ 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
- assert_equal connection_id, ActiveRecord::Base.connection_id
+ 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
def test_middleware_delegates
@@ -58,24 +156,25 @@ class QueryCacheTest < ActiveRecord::TestCase
[200, {}, nil]
}
mw.call({})
- assert called, 'middleware should delegate'
+ assert called, "middleware should delegate"
end
def test_middleware_caches
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 = middleware { |env|
- assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on'
+ assert_cache :clean
[200, {}, nil]
}
mw.call({})
@@ -110,7 +209,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
@@ -120,6 +219,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
@@ -131,9 +276,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 FrozenError 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
@@ -142,15 +333,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
@@ -176,49 +362,226 @@ 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
+ 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
+
+ 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
+
+ def test_clear_query_cache_is_called_on_all_connections
+ skip "with in memory db, reading role won't be able to see database on writing role" if in_memory_db?
+ with_temporary_connection_pool do
+ ActiveRecord::Base.connection_handlers = {
+ writing: ActiveRecord::Base.default_connection_handler,
+ reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new
+ }
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"])
+ end
+
+ mw = middleware { |env|
+ ActiveRecord::Base.connected_to(role: :reading) do
+ @topic = Topic.first
+ end
+
+ assert @topic
+
+ ActiveRecord::Base.connected_to(role: :writing) do
+ @topic.title = "It doesn't have to be crazy at work"
+ @topic.save!
+ end
+
+ assert_equal "It doesn't have to be crazy at work", @topic.title
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ @topic = Topic.first
+ assert_equal "It doesn't have to be crazy at work", @topic.title
+ end
+ }
+
+ mw.call({})
+ end
+ ensure
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
+
private
+
+ def with_temporary_connection_pool
+ 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)
@@ -233,19 +596,19 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_find
assert_called(Task.connection, :clear_query_cache) do
- assert !Task.connection.query_cache_enabled
+ assert_not Task.connection.query_cache_enabled
Task.cache do
assert Task.connection.query_cache_enabled
Task.find(1)
Task.uncached do
- assert !Task.connection.query_cache_enabled
+ assert_not Task.connection.query_cache_enabled
Task.find(1)
end
assert Task.connection.query_cache_enabled
end
- assert !Task.connection.query_cache_enabled
+ assert_not Task.connection.query_cache_enabled
end
end
@@ -289,9 +652,21 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do
ActiveRecord::Base.cache do
p = Post.find(1)
- assert p.categories.any?
+ assert_predicate p.categories, :any?
p.categories.delete_all
end
end
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 5f6eb41240..059fa76132 100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
@@ -1,28 +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/ship'
+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 }
@@ -35,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
@@ -110,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..402ddcf05a 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,27 +27,32 @@ 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
- until fp.reaped
+ until fp.flushed
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 710c86b151..abadafbad4 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -1,30 +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/mocktail_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
@@ -65,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
@@ -80,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
@@ -97,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
@@ -108,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)
@@ -142,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
@@ -181,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
@@ -227,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
@@ -246,46 +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
-
- 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
- end
-
def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case
- @hotel = Hotel.create!
- @department = @hotel.departments.create!
- @department.chefs.create!(employable: CakeDesigner.create!)
- @department.chefs.create!(employable: DrinkDesigner.create!)
+ hotel = Hotel.create!
+ department = hotel.departments.create!
+ department.chefs.create!(employable: CakeDesigner.create!)
+ department.chefs.create!(employable: DrinkDesigner.create!)
- assert_equal 1, @hotel.cake_designers.size
- assert_equal 1, @hotel.drink_designers.size
- assert_equal 2, @hotel.chefs.size
+ assert_equal 1, hotel.cake_designers.size
+ assert_equal 1, hotel.cake_designers.count
+ assert_equal 1, hotel.drink_designers.size
+ assert_equal 1, hotel.drink_designers.count
+ assert_equal 2, hotel.chefs.size
+ assert_equal 2, hotel.chefs.count
end
def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case_and_sti
- @hotel = Hotel.create!
- @hotel.mocktail_designers << MocktailDesigner.create!
+ hotel = Hotel.create!
+ hotel.mocktail_designers << MocktailDesigner.create!
+
+ assert_equal 1, hotel.mocktail_designers.size
+ assert_equal 1, hotel.mocktail_designers.count
+ assert_equal 1, hotel.chef_lists.size
+ assert_equal 1, hotel.chef_lists.count
+
+ hotel.mocktail_designers = []
- assert_equal 1, @hotel.mocktail_designers.size
- assert_equal 1, @hotel.mocktail_designers.count
- assert_equal 1, @hotel.chef_lists.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
@@ -305,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
@@ -345,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
@@ -388,73 +405,74 @@ 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.stub(:klass, category) do
- assert_equal 'categories_products', reflection.join_table
+ assert_equal "categories_products", reflection.join_table
end
reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
reflection.stub(:klass, product) do
- assert_equal 'categories_products', reflection.join_table
+ 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.stub(:klass, category) do
- assert_equal 'catalog_categories_products', reflection.join_table
+ assert_equal "catalog_categories_products", reflection.join_table
end
reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
reflection.stub(:klass, product) do
- assert_equal 'catalog_categories_products', reflection.join_table
+ 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.stub(:klass, category) do
- assert_equal 'catalog_categories_content_pages', reflection.join_table
+ assert_equal "catalog_categories_content_pages", reflection.join_table
end
reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category)
reflection.stub(:klass, page) do
- assert_equal 'catalog_categories_content_pages', reflection.join_table
+ 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 = ActiveRecord::Reflection.create(:has_many, :categories, nil, { join_table: "product_categories" }, product)
reflection.stub(:klass, category) do
- assert_equal 'product_categories', reflection.join_table
+ assert_equal "product_categories", reflection.join_table
end
- reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category)
+ 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
+ assert_equal "product_categories", reflection.join_table
end
end
@@ -474,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 f0e07e0731..172fa20bc3 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 DelegationTests
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,23 +21,55 @@ module ActiveRecord
assert_respond_to target, method
end
end
+
+ def test_not_respond_to_arel_method
+ assert_not_respond_to target, :exists
+ end
end
- class DelegationAssociationTest < DelegationTest
- include DelegationWhitelistBlacklistTests
+ class DelegationAssociationTest < ActiveRecord::TestCase
+ include DelegationTests
def target
- Post.first.comments
+ Post.new.comments
end
end
- class DelegationRelationTest < DelegationTest
- include DelegationWhitelistBlacklistTests
-
- fixtures :comments
+ class DelegationRelationTest < ActiveRecord::TestCase
+ include DelegationTests
def target
Comment.all
end
end
+
+ class QueryingMethodsDelegationTest < ActiveRecord::TestCase
+ QUERYING_METHODS =
+ ActiveRecord::Batches.public_instance_methods(false) +
+ ActiveRecord::Calculations.public_instance_methods(false) +
+ ActiveRecord::FinderMethods.public_instance_methods(false) - [:raise_record_not_found_exception!] +
+ ActiveRecord::SpawnMethods.public_instance_methods(false) - [:spawn, :merge!] +
+ ActiveRecord::QueryMethods.public_instance_methods(false).reject { |method|
+ method.to_s.end_with?("=", "!", "value", "values", "clause")
+ } - [:reverse_order, :arel, :extensions] + [
+ :any?, :many?, :none?, :one?,
+ :first_or_create, :first_or_create!, :first_or_initialize,
+ :find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
+ :create_or_find_by, :create_or_find_by!,
+ :destroy_all, :delete_all, :update_all, :delete_by, :destroy_by
+ ]
+
+ def test_delegate_querying_methods
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "posts"
+ end
+
+ assert_equal QUERYING_METHODS.sort, ActiveRecord::Querying::QUERYING_METHODS.sort
+
+ 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..d1c13fa1b5
--- /dev/null
+++ b/activerecord/test/cases/relation/delete_all_test.rb
@@ -0,0 +1,102 @@
+# 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
+
+ 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
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
index 60a806c05a..5c5e760e34 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
@@ -107,14 +117,40 @@ class RelationMergingTest < ActiveRecord::TestCase
def test_merging_with_from_clause
relation = Post.all
- assert relation.from_clause.empty?
+ assert_empty relation.from_clause
relation = relation.merge(Post.from("posts"))
- refute relation.from_clause.empty?
+ assert_not_empty relation.from_clause
+ end
+
+ 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
+
+ def test_merging_annotations_respects_merge_order
+ assert_sql(%r{/\* foo \*/ /\* bar \*/}) do
+ Post.annotate("foo").merge(Post.annotate("bar")).first
+ end
+ assert_sql(%r{/\* bar \*/ /\* foo \*/}) do
+ Post.annotate("bar").merge(Post.annotate("foo")).first
+ end
+ assert_sql(%r{/\* foo \*/ /\* bar \*/ /\* baz \*/ /\* qux \*/}) do
+ Post.annotate("foo").annotate("bar").merge(Post.annotate("baz").annotate("qux")).first
+ end
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).
@@ -143,7 +179,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 ffb2da7a26..96249b8d51 100644
--- a/activerecord/test/cases/relation/mutation_test.rb
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -1,42 +1,11 @@
-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
-
- def sanitize_sql_for_order(sql)
- sql
- end
-
- def arel_attribute(name, table)
- table[name]
- end
- end
-
- def relation
- @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table, Post.predicate_builder
- end
+require "cases/helper"
+require "models/post"
- (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select, :left_joins]).each do |method|
+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)
assert_equal [:foo], relation.public_send("#{method}_values")
@@ -44,24 +13,24 @@ 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_equal :name, node.expr.name
+ 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
assert_not_called(obj, :=~) do
assert relation.order!(obj)
@@ -69,12 +38,12 @@ module ActiveRecord
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)
@@ -85,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_equal :name, node.expr.name
+ 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 ce8c5ca489..8623867864 100644
--- a/activerecord/test/cases/relation/or_test.rb
+++ b/activerecord/test/cases/relation/or_test.rb
@@ -1,32 +1,42 @@
+# 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_large_number
+ expected = Post.where("id = 1 or id = 9223372036854775808").to_a
+ assert_equal expected, Post.where(id: 1).or(Post.where(id: 9223372036854775808)).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,56 +46,96 @@ 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
error = assert_raises ArgumentError do
- Post.order('body asc').where('id = 1').or(Post.order('id desc').where(:id => [2, 3])).to_a
+ 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')
+ 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
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 53daf436e5..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
- original_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 = original_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..586aaadd0a
--- /dev/null
+++ b/activerecord/test/cases/relation/select_test.rb
@@ -0,0 +1,27 @@
+# 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
+
+ def test_reselect
+ expected = Post.select(:title).to_sql
+ assert_equal expected, Post.select(:title, :body).reselect(:title).to_sql
+ end
+
+ def test_reselect_with_default_scope_select
+ expected = Post.select(:title).to_sql
+ actual = PostWithDefaultSelect.reselect(:title).to_sql
+
+ assert_equal expected, actual
+ 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..0500574f28
--- /dev/null
+++ b/activerecord/test/cases/relation/update_all_test.rb
@@ -0,0 +1,292 @@
+# 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_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
+
+ def test_update_all_cares_about_optimistic_locking
+ david = people(:david)
+
+ travel 5.seconds do
+ now = Time.now.utc
+ assert_not_equal now, david.updated_at
+
+ people = Person.where(id: people(:michael, :david, :susan))
+ expected = people.pluck(:lock_version)
+ expected.map! { |version| version + 1 }
+ people.update_all(updated_at: now)
+
+ assert_equal [now] * 3, people.pluck(:updated_at)
+ assert_equal expected, people.pluck(:lock_version)
+
+ assert_raises(ActiveRecord::StaleObjectError) do
+ david.touch(time: now)
+ end
+ end
+ end
+
+ def test_update_counters_cares_about_optimistic_locking
+ david = people(:david)
+
+ travel 5.seconds do
+ now = Time.now.utc
+ assert_not_equal now, david.updated_at
+
+ people = Person.where(id: people(:michael, :david, :susan))
+ expected = people.pluck(:lock_version)
+ expected.map! { |version| version + 1 }
+ people.update_counters(touch: [time: now])
+
+ assert_equal [now] * 3, people.pluck(:updated_at)
+ assert_equal expected, people.pluck(:lock_version)
+
+ assert_raises(ActiveRecord::StaleObjectError) do
+ david.touch(time: now)
+ end
+ end
+ end
+
+ def test_touch_all_cares_about_optimistic_locking
+ david = people(:david)
+
+ travel 5.seconds do
+ now = Time.now.utc
+ assert_not_equal now, david.updated_at
+
+ people = Person.where(id: people(:michael, :david, :susan))
+ expected = people.pluck(:lock_version)
+ expected.map! { |version| version + 1 }
+ people.touch_all(time: now)
+
+ assert_equal [now] * 3, people.pluck(:updated_at)
+ assert_equal expected, people.pluck(:lock_version)
+
+ assert_raises(ActiveRecord::StaleObjectError) do
+ david.touch(time: now)
+ end
+ 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|
+ Author.order(order).update_all("id = id + 1")
+ rescue ActiveRecord::ActiveRecordError
+ false
+ 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 56a2b5b8c6..b045184d7d 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -1,25 +1,28 @@
+# 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"
require "models/vertex"
+require "support/stubs/strong_parameters"
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
- fixtures :posts, :edges, :authors, :binaries, :essays, :cars, :treasures, :price_estimates
+ fixtures :posts, :edges, :authors, :author_addresses, :binaries, :essays, :cars, :treasures, :price_estimates, :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
@@ -48,8 +51,17 @@ module ActiveRecord
assert_equal [chef], chefs.to_a
end
+ def test_where_with_invalid_value
+ topics(:first).update!(parent_id: 0, written_on: nil, bonus_time: nil, last_read: nil)
+ assert_empty Topic.where(parent_id: Object.new)
+ assert_empty Topic.where(parent_id: "not-a-number")
+ assert_empty Topic.where(written_on: "")
+ assert_empty Topic.where(bonus_time: "")
+ assert_empty Topic.where(last_read: "")
+ 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
@@ -64,12 +76,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
@@ -87,7 +99,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
@@ -97,38 +109,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
+ 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
@@ -137,7 +168,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
@@ -147,7 +178,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
@@ -157,7 +188,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
@@ -182,40 +213,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
@@ -225,32 +256,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
@@ -289,32 +320,42 @@ module ActiveRecord
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 test_where_with_relation_on_has_many_association
+ essay = essays(:david_modest_proposal)
+ author = Author.where(essays: Essay.where(id: essay.id)).first
- def initialize(parameters)
- @parameters = parameters
- @permitted = false
- end
+ assert_equal authors(:david), author
+ end
- def to_h
- @parameters
- 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 permit!
- @permitted = true
- self
- end
- 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
author = authors(:david)
- params = protected_params.new(name: author.name)
+ params = ProtectedParams.new(name: author.name)
assert_raises(ActiveModel::ForbiddenAttributesError) { Author.where(params) }
assert_equal author, Author.where(params.permit!).first
end
+ def test_where_with_large_number
+ assert_equal [authors(:bob)], Author.where(id: [3, 9223372036854775808])
+ assert_equal [authors(:bob)], Author.where(id: 3..9223372036854775808)
+ end
+
+ def test_to_sql_with_large_number
+ assert_equal [authors(:bob)], Author.find_by_sql(Author.where(id: [3, 9223372036854775808]).to_sql)
+ assert_equal [authors(:bob)], Author.find_by_sql(Author.where(id: 3..9223372036854775808).to_sql)
+ end
+
def test_where_with_unsupported_arguments
assert_raises(ArgumentError) { Author.where(42) }
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 03583344a8..00a7b3841f 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -1,45 +1,30 @@
+# 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
-
- def self.sanitize_sql_for_order(sql)
- sql
- 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
@@ -49,7 +34,7 @@ module ActiveRecord
end
def test_multi_value_initialize
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
Relation::MULTI_VALUE_METHODS.each do |method|
values = relation.send("#{method}_values")
assert_equal [], values, method.to_s
@@ -58,70 +43,64 @@ module ActiveRecord
end
def test_extensions
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal [], relation.extensions
end
def test_empty_where_values_hash
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal({}, relation.where_values_hash)
end
def test_has_values
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
- relation.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)
+ 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)
- 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)
- 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
@@ -131,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(Post, Post.arel_table, Post.predicate_builder)
- relation = relation.merge where: {name: :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({"name"=>:lol}, relation.where_clause.to_h)
+ 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(Post, Post.arel_table, Post.predicate_builder).where!(name: :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
@@ -223,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
@@ -238,17 +260,17 @@ module ActiveRecord
def test_merge_raises_with_invalid_argument
assert_raises ArgumentError do
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
relation.merge(true)
end
end
def test_respond_to_for_non_selected_element
post = Post.select(:title).first
- assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception"
+ assert_not_respond_to post, :body, "post should not respond_to?(:body) since invoking it raises exception"
silence_warnings { post = Post.select("'title' as post_title").first }
- assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception"
+ assert_not_respond_to post, :title, "post should not respond_to?(:body) since invoking it raises exception"
end
def test_select_quotes_when_using_from_clause
@@ -273,7 +295,68 @@ module ActiveRecord
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
+
+ def test_relation_with_annotation_includes_comment_in_to_sql
+ post_with_annotation = Post.where(id: 1).annotate("foo")
+ assert_match %r{= 1 /\* foo \*/}, post_with_annotation.to_sql
+ end
+
+ def test_relation_with_annotation_includes_comment_in_sql
+ post_with_annotation = Post.where(id: 1).annotate("foo")
+ assert_sql(%r{/\* foo \*/}) do
+ assert post_with_annotation.first, "record should be found"
+ end
+ end
+
+ def test_relation_with_annotation_chains_sql_comments
+ post_with_annotation = Post.where(id: 1).annotate("foo").annotate("bar")
+ assert_sql(%r{/\* foo \*/ /\* bar \*/}) do
+ assert post_with_annotation.first, "record should be found"
+ end
+ end
+
+ def test_relation_with_annotation_filters_sql_comment_delimiters
+ post_with_annotation = Post.where(id: 1).annotate("**//foo//**")
+ assert_match %r{= 1 /\* foo \*/}, post_with_annotation.to_sql
+ end
+
+ def test_relation_with_annotation_includes_comment_in_count_query
+ post_with_annotation = Post.annotate("foo")
+ all_count = Post.all.to_a.count
+ assert_sql(%r{/\* foo \*/}) do
+ assert_equal all_count, post_with_annotation.count
+ end
+ end
+
+ def test_relation_without_annotation_does_not_include_an_empty_comment
+ log = capture_sql do
+ Post.where(id: 1).first
+ end
+
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+ end
+
+ def test_relation_with_optimizer_hints_filters_sql_comment_delimiters
+ post_with_hint = Post.where(id: 1).optimizer_hints("**//BADHINT//**")
+ assert_match %r{BADHINT}, post_with_hint.to_sql
+ assert_no_match %r{\*/BADHINT}, post_with_hint.to_sql
+ assert_no_match %r{\*//BADHINT}, post_with_hint.to_sql
+ assert_no_match %r{BADHINT/\*}, post_with_hint.to_sql
+ assert_no_match %r{BADHINT//\*}, post_with_hint.to_sql
+ post_with_hint = Post.where(id: 1).optimizer_hints("/*+ BADHINT */")
+ assert_match %r{/\*\+ BADHINT \*/}, post_with_hint.to_sql
end
class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value
@@ -281,19 +364,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
@@ -304,25 +392,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 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
+ 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
- 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
+ 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 95e4230a58..131e034c66 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -1,45 +1,45 @@
+# 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/contract"
+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
@@ -54,12 +54,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
@@ -97,46 +97,46 @@ 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.to_a # force load
+ topics = Topic.all.order("id ASC")
+ topics.load # force load
assert_no_queries do
assert_equal "The First Topic", topics.first.title
end
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
def test_loaded_first_with_limit
- topics = Topic.all.order('id ASC')
- topics.to_a # force load
+ 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')
+ topics = Topic.all.order("id ASC")
unloaded_first = topics.first(10)
- topics.to_a # force load
+ topics.load # force load
assert_no_queries do
loaded_first = topics.first(10)
@@ -151,28 +151,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
@@ -182,26 +182,87 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ def test_select_with_from_includes_original_table_name
+ relation = Comment.joins(:post).select(:id).order(:id)
+ subquery = Comment.from("#{Comment.table_name} /*! USE INDEX (PRIMARY) */").joins(:post).select(:id).order(:id)
+ assert_equal relation.map(&:id), subquery.map(&:id)
+ end
+
+ def test_pluck_with_from_includes_original_table_name
+ relation = Comment.joins(:post).order(:id)
+ subquery = Comment.from("#{Comment.table_name} /*! USE INDEX (PRIMARY) */").joins(:post).order(:id)
+ assert_equal relation.pluck(:id), subquery.pluck(:id)
+ end
+
+ def test_select_with_from_includes_quoted_original_table_name
+ relation = Comment.joins(:post).select(:id).order(:id)
+ subquery = Comment.from("#{Comment.quoted_table_name} /*! USE INDEX (PRIMARY) */").joins(:post).select(:id).order(:id)
+ assert_equal relation.map(&:id), subquery.map(&:id)
+ end
+
+ def test_pluck_with_from_includes_quoted_original_table_name
+ relation = Comment.joins(:post).order(:id)
+ subquery = Comment.from("#{Comment.quoted_table_name} /*! USE INDEX (PRIMARY) */").joins(:post).order(:id)
+ assert_equal relation.pluck(:id), subquery.pluck(:id)
+ end
+
+ def test_select_with_subquery_in_from_uses_original_table_name
+ relation = Comment.joins(:post).select(:id).order(:id)
+ # Avoid subquery flattening by adding distinct to work with SQLite < 3.20.0.
+ subquery = Comment.from(Comment.all.distinct, Comment.quoted_table_name).joins(:post).select(:id).order(:id)
+ assert_equal relation.map(&:id), subquery.map(&:id)
+ end
+
+ def test_pluck_with_subquery_in_from_uses_original_table_name
+ relation = Comment.joins(:post).order(:id)
+ subquery = Comment.from(Comment.all, Comment.quoted_table_name).joins(:post).order(:id)
+ assert_equal relation.pluck(:id), subquery.pluck(:id)
+ 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, "grouped_#{Comment.table_name}").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, "grouped_#{Comment.table_name}").group("type").average("post_count")
+ assert_equal(relation.map(&:post_count).sort, subquery.values.sort)
+ end
+
+ def test_select_with_subquery_string_in_from_does_not_use_original_table_name
+ relation = Comment.group(:type).select("COUNT(post_id) AS post_count, type")
+ subquery = Comment.from("(#{relation.to_sql}) #{Comment.table_name}_grouped").select("type", "post_count")
+ assert_equal(relation.map(&:post_count).sort, subquery.map(&:post_count).sort)
+ end
+
+ def test_group_with_subquery_string_in_from_does_not_use_original_table_name
+ relation = Comment.group(:type).select("COUNT(post_id) AS post_count,type")
+ subquery = Comment.from("(#{relation.to_sql}) #{Comment.table_name}_grouped").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
@@ -213,41 +274,82 @@ 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("length(title)").reverse_order
+ 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("author_name, length(title), id").reverse_order
+ topics = Topic.order(Arel.sql("author_name, length(title), id")).reverse_order
assert_equal topics(:second).title, topics.first.title
- topics = Topic.order("length(author_name), id, length(title)").reverse_order
+ topics = Topic.order(Arel.sql("length(author_name), id, length(title)")).reverse_order
assert_equal topics(:fifth).title, topics.first.title
end
def test_reverse_order_with_multiargument_function
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("concat(author_name, title)").reverse_order
+ Topic.order(Arel.sql("concat(author_name, title)")).reverse_order
+ end
+ assert_raises(ActiveRecord::IrreversibleOrderError) do
+ Topic.order(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("title NULLS FIRST").reverse_order
+ Topic.order(Arel.sql("title NULLS FIRST")).reverse_order
+ end
+ 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
+ assert_raises(ActiveRecord::IrreversibleOrderError) do
+ Topic.order(Arel.sql("title NULLS FIRST, author_name")).reverse_order
end
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("title nulls last").reverse_order
+ Topic.order(Arel.sql("author_name, title nulls last")).reverse_order
end
end
@@ -258,7 +360,7 @@ class RelationTest < ActiveRecord::TestCase
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
@@ -268,7 +370,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
@@ -282,7 +384,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
@@ -296,7 +398,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
@@ -314,19 +416,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
@@ -340,29 +442,29 @@ class RelationTest < ActiveRecord::TestCase
def test_finding_with_cross_table_order_and_limit
tags = Tag.includes(:taggings).
- order("tags.name asc", "taggings.taggable_id asc", "REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").
+ order("tags.name asc", "taggings.taggable_id asc", Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).
limit(1).to_a
assert_equal 1, tags.length
end
def test_finding_with_complex_order_and_limit
- tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").limit(1).to_a
+ tags = Tag.includes(:taggings).references(:taggings).order(Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).limit(1).to_a
assert_equal 1, tags.length
end
def test_finding_with_complex_order
- tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a
+ tags = Tag.includes(:taggings).references(:taggings).order(Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).to_a
assert_equal 3, tags.length
end
def test_finding_with_sanitized_order
- query = Tag.order(["field(id, ?)", [1,3,2]]).to_sql
+ query = Tag.order([Arel.sql("field(id, ?)"), [1, 3, 2]]).to_sql
assert_match(/field\(id, 1,3,2\)/, query)
- query = Tag.order(["field(id, ?)", []]).to_sql
+ query = Tag.order([Arel.sql("field(id, ?)"), []]).to_sql
assert_match(/field\(id, NULL\)/, query)
- query = Tag.order(["field(id, ?)", nil]).to_sql
+ query = Tag.order([Arel.sql("field(id, ?)"), nil]).to_sql
assert_match(/field\(id, NULL\)/, query)
end
@@ -384,149 +486,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
@@ -554,53 +539,26 @@ 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
- relation = Topic.all
- fake_arel = Struct.new(:responds) {
- def respond_to? method, access = false
- responds << [method, access]
- end
- }.new []
-
- relation.extend(Module.new { attr_accessor :arel })
- relation.arel = fake_arel
-
- relation.respond_to?(:matching_attributes)
- assert_equal [:matching_attributes, false], fake_arel.responds.first
-
- 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
@@ -611,27 +569,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
@@ -644,60 +602,57 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ def test_extracted_association
+ relation_authors = assert_queries(2) { Post.all.extract_associated(:author) }
+ root_authors = assert_queries(2) { Post.extract_associated(:author) }
+ assert_equal relation_authors, root_authors
+ assert_equal Post.all.collect(&:author), relation_authors
+ end
+
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
@@ -706,7 +661,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
@@ -717,17 +672,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
@@ -739,11 +694,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 }
@@ -752,9 +707,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
@@ -765,7 +720,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
@@ -776,7 +731,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
@@ -789,40 +743,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
@@ -832,15 +786,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])
@@ -850,25 +795,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
@@ -884,20 +829,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
@@ -912,22 +855,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
@@ -935,10 +876,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
}
@@ -948,112 +889,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_any_with_scope_on_hash_includes
- post = authors(:david).posts.first
- categories = Categorization.includes(author: :posts).where(posts: { id: post.id })
- assert categories.exists?
- end
-
def test_last
authors = Author.all
assert_equal authors(:bob), authors.last
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_destroy_all_with_conditions_is_deprecated
- assert_deprecated do
- assert_difference('Author.count', -1) { Author.destroy_all(name: 'David') }
- end
- end
-
- def test_delete_all
- davids = Author.where(:name => 'David')
-
- assert_difference('Author.count', -1) { davids.delete_all }
- assert ! davids.loaded?
- end
-
- def test_delete_all_with_conditions_is_deprecated
- assert_deprecated do
- assert_difference('Author.count', -1) { Author.delete_all(name: 'David') }
- end
- end
-
- def test_delete_all_loaded
- davids = Author.where(:name => 'David')
-
- # 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
@@ -1082,8 +943,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
@@ -1094,42 +960,59 @@ 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_size_with_eager_loading_and_manual_distinct_select_and_custom_order
+ accounts = Account.select("DISTINCT accounts.firm_id").order("accounts.firm_id")
+
+ assert_queries(1) { assert_equal 5, accounts.size }
+ assert_queries(1) { assert_equal 5, accounts.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
@@ -1138,31 +1021,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
@@ -1170,13 +1053,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
@@ -1184,14 +1067,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
@@ -1199,11 +1082,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
@@ -1215,17 +1098,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
@@ -1233,50 +1116,50 @@ 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
@@ -1297,11 +1180,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
@@ -1309,11 +1192,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
@@ -1321,201 +1204,336 @@ 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_after_initialize
+ Bird.create!(color: "yellow", name: "canary")
+ parrot = assert_deprecated do
+ Bird.where(color: "green").first_or_create do |bird|
+ bird.name = "parrot"
+ bird.enable_count = true
+ end
+ end
+ assert_equal 0, parrot.total_count
end
def test_first_or_create_with_block
- parrot = Bird.where(:color => 'green').first_or_create { |bird| bird.name = 'parrot' }
+ Bird.create!(color: "yellow", name: "canary")
+ parrot = Bird.where(color: "green").first_or_create do |bird|
+ bird.name = "parrot"
+ assert_deprecated { assert_equal 0, Bird.count }
+ end
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_after_initialize
+ Bird.create!(color: "yellow", name: "canary")
+ parrot = assert_deprecated do
+ Bird.where(color: "green").first_or_create! do |bird|
+ bird.name = "parrot"
+ bird.enable_count = true
+ end
+ end
+ assert_equal 0, parrot.total_count
end
def test_first_or_create_bang_with_valid_block
- parrot = Bird.where(:color => 'green').first_or_create! { |bird| bird.name = 'parrot' }
+ Bird.create!(color: "yellow", name: "canary")
+ parrot = Bird.where(color: "green").first_or_create! do |bird|
+ bird.name = "parrot"
+ assert_deprecated { assert_equal 0, Bird.count }
+ end
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_after_initialize
+ Bird.create!(color: "yellow", name: "canary")
+ parrot = assert_deprecated do
+ Bird.where(color: "green").first_or_initialize do |bird|
+ bird.name = "parrot"
+ bird.enable_count = true
+ end
+ end
+ assert_equal 0, parrot.total_count
end
def test_first_or_initialize_with_block
- parrot = Bird.where(:color => 'green').first_or_initialize { |bird| bird.name = 'parrot' }
+ Bird.create!(color: "yellow", name: "canary")
+ parrot = Bird.where(color: "green").first_or_initialize do |bird|
+ bird.name = "parrot"
+ assert_deprecated { assert_equal 0, Bird.count }
+ end
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_should_not_raise_due_to_validation_errors
+ assert_nothing_raised do
+ bird = Bird.create_or_find_by(color: "green")
+ assert_predicate bird, :invalid?
+ end
+ 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_create_or_find_by_with_bang
+ 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_bang_should_raise_due_to_validation_errors
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.create_or_find_by!(color: "green") }
+ end
+
+ def test_create_or_find_by_with_bang_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_with_bang_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
+ hens = hens.create_with(name: "cock")
+ assert_equal "cock", hens.new.name
+ end
+
+ 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).sort_by(&:id), author_posts.sort_by(&:id)
+ assert_equal author_posts.sort_by(&:id), relation.scoping { Post.except(:order, :limit).sort_by(&:id) }
all_posts = relation.except(:where, :order, :limit)
- assert_equal Post.all, all_posts
+ assert_equal Post.all.sort_by(&:id), all_posts.sort_by(&:id)
+ assert_equal all_posts.sort_by(&:id), relation.scoping { Post.except(:where, :order, :limit).sort_by(&:id) }
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).sort_by(&:id), author_posts.sort_by(&:id)
+ assert_equal author_posts.sort_by(&:id), relation.scoping { Post.only(:where).sort_by(&:id) }
- all_posts = relation.only(:limit)
- assert_equal Post.limit(1).to_a.first, all_posts.first
+ all_posts = relation.only(:order)
+ assert_equal Post.order("id ASC").to_a, all_posts.to_a
+ assert_equal all_posts.to_a, relation.scoping { Post.only(:order).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
@@ -1524,7 +1542,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
@@ -1534,29 +1552,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
@@ -1568,99 +1586,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
- end
-
- def test_update_on_relation_passing_active_record_object_is_deprecated
- topic = Topic.create!(title: 'Foo', author_name: nil)
- assert_deprecated(/update/) do
- Topic.where(id: topic.id).update(topic, title: 'Bar')
- end
+ 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
@@ -1686,72 +1636,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('comments.body', 'yaks.body')
- assert_equal ['comments', 'yaks'], 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
# 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
@@ -1776,7 +1740,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 }
@@ -1786,7 +1750,25 @@ class RelationTest < ActiveRecord::TestCase
# count always trigger the COUNT query.
assert_queries(1) { topics.count }
- assert topics.loaded?
+ assert_predicate topics, :loaded?
+ end
+
+ def test_delete_by
+ david = authors(:david)
+
+ assert_difference("Post.count", -3) { david.posts.delete_by(body: "hello") }
+
+ deleted = Author.delete_by(id: david.id)
+ assert_equal 1, deleted
+ end
+
+ def test_destroy_by
+ david = authors(:david)
+
+ assert_difference("Post.count", -3) { david.posts.destroy_by(body: "hello") }
+
+ destroyed = Author.destroy_by(id: david.id)
+ assert_equal [david], destroyed
end
test "find_by with hash conditions returns the first matching record" do
@@ -1798,11 +1780,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
@@ -1822,7 +1804,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
@@ -1844,7 +1826,7 @@ class RelationTest < ActiveRecord::TestCase
relation.to_a
assert_raises(ActiveRecord::ImmutableRelation) do
- relation.where! 'foo'
+ relation.where! "foo"
end
end
@@ -1862,7 +1844,7 @@ class RelationTest < ActiveRecord::TestCase
relation.to_a
assert_raises(ActiveRecord::ImmutableRelation) do
- relation.merge! where: 'foo'
+ relation.merge! where: "foo"
end
end
@@ -1877,7 +1859,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") }
@@ -1893,6 +1875,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
@@ -1904,19 +1892,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)
+
+ 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)
- 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.joins(:author).where!(title: post.title).take
+ end
+
+ test "arel_attribute respects a custom table" do
+ assert_equal [posts(:sti_comments)], custom_post_relation.ranked_by_comments.limit_by(1).to_a
+ end
- node = relation.arel.constraints.first.grep(Arel::Attributes::Attribute).first
- assert_equal table_alias, node.relation
+ test "alias_tracker respects a custom table" do
+ assert_equal posts(:welcome), custom_post_relation("categories_posts").joins(:categories).first
end
- test '#load' do
+ test "#load" do
relation = Post.all
assert_queries(1) do
assert_equal relation, relation.load
@@ -1924,9 +1920,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|
@@ -1944,49 +1940,154 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [1, 1, 1], posts.map(&:author_address_id)
end
+ test "joins with select custom attribute" do
+ contract = Company.create!(name: "test").contracts.create!
+ company = Company.joins(:contracts).select(:id, :metadata).find(contract.company_id)
+ assert_equal contract.metadata, company.metadata
+ end
+
+ test "joins with order by custom attribute" do
+ companies = Company.create!([{ name: "test1" }, { name: "test2" }])
+ companies.each { |company| company.contracts.create! }
+ assert_equal companies, Company.joins(:contracts).order(:metadata)
+ assert_equal companies.reverse, Company.joins(:contracts).order(metadata: :desc)
+ end
+
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_removes_binds
- left = Post.where(id: Arel::Nodes::BindParam.new)
- column = Post.columns_hash['id']
- left.bind_values += [[column, 20]]
+ def test_unscope_with_subquery
+ p1 = Post.where(id: 1)
+ p2 = Post.where(id: 2)
- relation = left.unscope(where: :id)
- assert_equal [], relation.bind_values
+ 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_merging_removes_rhs_bind_parameters
- left = Post.where(id: 20)
- right = Post.where(id: [1,2,3,4])
+ def test_unscope_with_merge
+ p0 = Post.where(author_id: 0)
+ p1 = Post.where(author_id: 1, comments_count: 1)
+
+ assert_equal [posts(:authorless)], p0
+ assert_equal [posts(:thinking)], p1
- merged = left.merge(right)
- assert_equal [], merged.bind_values
+ comments = Comment.merge(p0).unscope(where: :author_id).where(post: p1)
+
+ assert_not_equal p0.first.comments, comments
+ assert_equal p1.first.comments, comments
end
- def test_merging_keeps_lhs_bind_parameters
- binds = [ActiveRecord::Relation::QueryAttribute.new("id", 20, Post.type_for_attribute("id"))]
+ def test_unscope_with_unknown_column
+ comment = comments(:greetings)
+ comment.update!(comments: 1)
- right = Post.where(id: 20)
- left = Post.where(id: 10)
+ comments = Comment.where(comments: 1).unscope(where: :unknown_column)
+ assert_equal [comment], comments
- merged = left.merge(right)
- assert_equal binds, merged.bound_attributes
+ comments = Comment.where(comments: 1).unscope(where: { comments: :unknown_column })
+ assert_equal [comment], comments
end
- def test_merging_reorders_bind_params
- post = Post.first
- right = Post.where(id: post.id)
- left = Post.where(title: post.title)
+ def test_unscope_specific_where_value
+ posts = Post.where(title: "Welcome to the weblog", body: "Such a lovely day")
- merged = left.merge(right)
- assert_equal post, merged.first
+ assert_equal 1, posts.count
+ assert_equal 1, posts.unscope(where: :title).count
+ assert_equal 1, posts.unscope(where: :body).count
+ end
+
+ def test_locked_should_not_build_arel
+ posts = Post.locked
+ assert_predicate posts, :locked?
+ assert_nothing_raised { posts.lock!(false) }
end
def test_relation_join_method
- assert_equal 'Thank you for the welcome,Thank you again for the welcome', Post.first.comments.join(",")
+ assert_equal "Thank you for the welcome,Thank you again for the welcome", Post.first.comments.join(",")
+ end
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ assert_queries(4) do
+ Post.preload(:comments).skip_query_cache!.load
+ Post.preload(:comments).skip_query_cache!.load
+ end
+ end
end
+
+ test "#where with set" do
+ david = authors(:david)
+ mary = authors(:mary)
+
+ authors = Author.where(name: ["David", "Mary"].to_set)
+ assert_equal [david, mary], authors
+ end
+
+ test "#where with empty set" do
+ authors = Author.where(name: Set.new)
+ assert_empty authors
+ end
+
+ private
+ def custom_post_relation(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 239f63d27b..6c884b4f45 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,97 +12,105 @@ class SanitizeTest < ActiveRecord::TestCase
def test_sanitize_sql_array_handles_string_interpolation
quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi")
- assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi"])
- assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi".mb_chars])
+ assert_equal "name='#{quoted_bambi}'", Binary.sanitize_sql_array(["name='%s'", "Bambi"])
+ assert_equal "name='#{quoted_bambi}'", Binary.sanitize_sql_array(["name='%s'", "Bambi".mb_chars])
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper")
- assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper"])
- assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper".mb_chars])
+ assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.sanitize_sql_array(["name='%s'", "Bambi\nand\nThumper"])
+ assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.sanitize_sql_array(["name='%s'", "Bambi\nand\nThumper".mb_chars])
end
def test_sanitize_sql_array_handles_bind_variables
quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi"])
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi".mb_chars])
+ assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=?", "Bambi"])
+ assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=?", "Bambi".mb_chars])
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper")
- assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"])
- assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars])
+ assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=?", "Bambi\nand\nThumper"])
+ assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=?", "Bambi\nand\nThumper".mb_chars])
end
def test_sanitize_sql_array_handles_named_bind_variables
quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi"])
- assert_equal "name=#{quoted_bambi} AND id=1", Binary.send(:sanitize_sql_array, ["name=:name AND id=:id", name: "Bambi", id: 1])
+ assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=:name", name: "Bambi"])
+ assert_equal "name=#{quoted_bambi} AND id=1", Binary.sanitize_sql_array(["name=:name AND id=:id", name: "Bambi", id: 1])
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper")
- assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi\nand\nThumper"])
- assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name AND name2=:name", name: "Bambi\nand\nThumper"])
+ assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=:name", name: "Bambi\nand\nThumper"])
+ assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=:name AND name2=:name", name: "Bambi\nand\nThumper"])
end
def test_sanitize_sql_array_handles_relations
- 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("20% _reduction_!").to_a
+ searchable_post.search_as_method("20% _reduction_!").to_a
+ end
+
+ assert_sql(/LIKE '20!% !_reduction!_!!'/) do
+ 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_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 }
+ 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_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_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 } }
+ 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
@@ -117,50 +128,55 @@ class SanitizeTest < ActiveRecord::TestCase
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("?", [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(":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("?", 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))) # '
+ 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 (?)', [])
+ 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_range
+ quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')})
+ assert_equal "0", bind("?", 0..0)
+ assert_equal "1,2,3", bind("?", 1..3)
+ assert_equal quoted_abc, bind("?", "a"..."d")
+ end
+
+ def test_bind_empty_range
+ quoted_nil = ActiveRecord::Base.connection.quote(nil)
+ assert_equal quoted_nil, bind("?", 0...0)
+ assert_equal quoted_nil, bind("?", "a"..."a")
end
def test_bind_empty_string
- quoted_empty = ActiveRecord::Base.connection.quote('')
- assert_equal quoted_empty, bind('?', '')
+ 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)
+ 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') }
+ 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
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 12c8a1d5ba..49e9be9565 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,21 +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
@@ -56,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
@@ -104,44 +106,29 @@ 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
+ 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_without_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.*}, 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
@@ -155,7 +142,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
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
@@ -171,24 +158,44 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
def test_schema_dumps_index_columns_in_right_order
- index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip
- if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
- assert_equal 'add_index "companies", ["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 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition
+ 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(/add_index.*company_partial_index/).first.strip
- if current_adapter?(:PostgreSQLAdapter)
- assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition
- elsif current_adapter?(:Mysql2Adapter)
- assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition
- elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index?
- assert_equal 'add_index "companies", ["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 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition
+ 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
@@ -200,43 +207,76 @@ 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?(: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
+ output = dump_table_schema "binary_fields"
assert_match %r{t\.binary\s+"var_binary",\s+limit: 255$}, output
assert_match %r{t\.binary\s+"var_binary_large",\s+limit: 4095$}, output
end
def test_schema_dump_includes_length_for_mysql_blob_and_text_fields
- output = standard_dump
- assert_match %r{t\.blob\s+"tiny_blob",\s+limit: 255$}, output
- assert_match %r{t\.binary\s+"normal_blob",\s+limit: 65535$}, 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+"medium_text",\s+limit: 16777215$}, output
- assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output
+ output = dump_table_schema "binary_fields"
+ assert_match %r{t\.binary\s+"tiny_blob",\s+size: :tiny$}, output
+ assert_match %r{t\.binary\s+"normal_blob"$}, output
+ assert_match %r{t\.binary\s+"medium_blob",\s+size: :medium$}, output
+ assert_match %r{t\.binary\s+"long_blob",\s+size: :long$}, output
+ assert_match %r{t\.text\s+"tiny_text",\s+size: :tiny$}, output
+ assert_match %r{t\.text\s+"normal_text"$}, output
+ assert_match %r{t\.text\s+"medium_text",\s+size: :medium$}, output
+ assert_match %r{t\.text\s+"long_text",\s+size: :long$}, output
+ assert_match %r{t\.binary\s+"tiny_blob_2",\s+size: :tiny$}, output
+ assert_match %r{t\.binary\s+"medium_blob_2",\s+size: :medium$}, output
+ assert_match %r{t\.binary\s+"long_blob_2",\s+size: :long$}, output
+ assert_match %r{t\.text\s+"tiny_text_2",\s+size: :tiny$}, output
+ assert_match %r{t\.text\s+"medium_text_2",\s+size: :medium$}, output
+ assert_match %r{t\.text\s+"long_text_2",\s+size: :long$}, output
end
def test_schema_does_not_include_limit_for_emulated_mysql_boolean_fields
- output = standard_dump
+ output = dump_table_schema "booleans"
assert_no_match %r{t\.boolean\s+"has_fun",.+limit: 1}, output
end
def test_schema_dumps_index_type
- output = standard_dump
- assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output
- assert_match %r{add_index "key_tests", \["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
@@ -247,35 +287,62 @@ class SchemaDumperTest < ActiveRecord::TestCase
if current_adapter?(:PostgreSQLAdapter)
def test_schema_dump_includes_bigint_default
- output = standard_dump
+ output = dump_table_schema "defaults"
assert_match %r{t\.bigint\s+"bigint_default",\s+default: 0}, output
end
def test_schema_dump_includes_limit_on_array_type
- output = standard_dump
+ output = dump_table_schema "bigint_array"
assert_match %r{t\.bigint\s+"big_int_data_points\",\s+array: true}, output
end
def test_schema_dump_allows_array_of_decimal_defaults
- output = standard_dump
+ output = dump_table_schema "bigint_array"
assert_match %r{t\.decimal\s+"decimal_array_default",\s+default: \["1.23", "3.45"\],\s+array: true}, output
end
- 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
- connection.stubs(:extensions).returns(['hstore'])
+ def test_schema_dump_includes_extensions
+ connection = ActiveRecord::Base.connection
+
+ 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
@@ -300,7 +367,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?
@@ -322,10 +389,10 @@ class SchemaDumperTest < ActiveRecord::TestCase
create_table("dogs") do |t|
t.column :name, :string
- t.column :owner_id, :integer
+ t.references :owner
+ t.index [:name]
+ t.foreign_key :dog_owners, column: "owner_id"
end
- add_index "dogs", [:name]
- add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys?
end
def down
drop_table("dogs")
@@ -335,8 +402,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)
@@ -354,7 +421,32 @@ 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
@@ -372,7 +464,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
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::Base.table_name_prefix = "omg_"
ActiveRecord::SchemaDumper.ignore_tables = ["cats"]
migration = create_cat_migration.new
migration.migrate(:up)
@@ -381,7 +473,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string
assert_match %r{create_table "omg_cats"}, output
- refute_match %r{create_table "cats"}, output
+ assert_no_match %r{create_table "cats"}, output
ensure
migration.migrate(:down)
ActiveRecord::Base.table_name_prefix = original_table_name_prefix
@@ -396,25 +488,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 dcd09b6973..e7bdab58c6 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -1,16 +1,20 @@
-require 'cases/helper'
-require 'models/post'
-require 'models/comment'
-require 'models/developer'
-require 'models/computer'
-require 'models/vehicle'
-require 'models/cat'
+# 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
@@ -49,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
@@ -112,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)
@@ -228,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
@@ -249,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
@@ -264,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
@@ -298,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
@@ -341,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
@@ -382,9 +407,37 @@ class DefaultScopingTest < ActiveRecord::TestCase
Comment.joins(:post).count
end
+ def test_joins_not_affected_by_scope_other_than_default_or_unscoped
+ without_scope_on_post = Comment.joins(:post).sort_by(&:id)
+ with_scope_on_post = nil
+ Post.where(id: [1, 5, 6]).scoping do
+ with_scope_on_post = Comment.joins(:post).sort_by(&:id)
+ end
+
+ assert_equal without_scope_on_post, with_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
+ assert_equal Comment.joins(:post).sort_by(&:id),
+ SpecialPostWithDefaultScope.unscoped { Comment.joins(:special_post_with_default_scope).sort_by(&:id) }
+ 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
@@ -409,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
@@ -432,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
@@ -465,7 +500,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
test "a scope can remove the condition from the default scope" do
scope = DeveloperCalledJamis.david2
assert_equal 1, scope.where_clause.ast.children.length
- assert_equal Developer.where(name: "David"), scope
+ assert_equal Developer.where(name: "David").map(&:id), scope.map(&:id)
end
def test_with_abstract_class_where_clause_should_not_be_duplicated
@@ -474,9 +509,9 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_sti_conditions_are_not_carried_in_default_scope
- ConditionalStiPost.create! body: ''
- SubConditionalStiPost.create! body: ''
- SubConditionalStiPost.create! title: 'Hello world', body: ''
+ 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
@@ -490,6 +525,8 @@ class DefaultScopingTest < ActiveRecord::TestCase
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
@@ -498,3 +535,41 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_match gender_pattern, Lion.female.to_sql
end
end
+
+class DefaultScopingWithThreadTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ def test_default_scoping_with_threads
+ 2.times do
+ Thread.new {
+ assert_includes DeveloperOrderedBySalary.all.to_sql, "salary DESC"
+ DeveloperOrderedBySalary.connection.close
+ }.join
+ end
+ end
+
+ def test_default_scope_is_threadsafe
+ 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 96c94eefa0..3488442cab 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.unscoped.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
@@ -196,21 +223,21 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_any_should_not_fire_query_if_scope_loaded
topics = Topic.base
- topics.collect # force load
+ topics.load # force load
assert_no_queries { assert topics.any? }
end
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
@@ -226,28 +253,28 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_many_should_not_fire_query_if_scope_loaded
topics = Topic.base
- topics.collect # force load
+ topics.load # force load
assert_no_queries { assert topics.many? }
end
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
@@ -273,7 +300,7 @@ 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_reserved_scope_names
@@ -301,7 +328,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,
@@ -320,12 +347,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
@@ -333,12 +360,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
@@ -350,7 +377,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 '% %'")
@@ -365,7 +392,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
@@ -377,7 +404,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
@@ -396,16 +423,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
@@ -420,16 +447,27 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq
end
+ def test_class_method_in_scope
+ assert_deprecated do
+ assert_equal [topics(:second)], topics(:first).approved_replies.ordered
+ end
+ end
+
+ def test_nested_scoping
+ expected = Reply.approved
+ assert_equal expected.to_a, Topic.rejected.nested_scoping(expected)
+ end
+
def test_scopes_batch_finders
assert_equal 4, Topic.approved.count
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
@@ -454,14 +492,15 @@ class NamedScopingTest < ActiveRecord::TestCase
[:public_method, :protected_method, :private_method].each do |reserved_method|
assert Topic.respond_to?(reserved_method, true)
- ActiveRecord::Base.logger.expects(:warn)
- silence_warnings { Topic.scope(reserved_method, -> { }) }
+ assert_called(ActiveRecord::Base.logger, :warn) do
+ silence_warnings { Topic.scope(reserved_method, -> { }) }
+ end
end
end
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
@@ -469,9 +508,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
@@ -510,7 +549,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
@@ -533,35 +572,44 @@ 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_empty
- assert !Topic.empty?
- Topic.delete_all
- assert Topic.empty?
+ 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 !Topic.none?
+ assert_not_predicate Topic, :none?
Topic.delete_all
- assert Topic.none?
+ assert_predicate Topic, :none?
end
def test_model_class_should_respond_to_one
- assert !Topic.one?
+ assert_not_predicate Topic, :one?
Topic.delete_all
- assert !Topic.one?
+ assert_not_predicate Topic, :one?
Topic.create!
- assert Topic.one?
+ assert_predicate Topic, :one?
end
+ def test_scope_with_annotation
+ Topic.class_eval do
+ scope :including_annotate_in_scope, Proc.new { annotate("from-scope") }
+ end
+
+ assert_sql(%r{/\* from-scope \*/}) do
+ assert Topic.including_annotate_in_scope.to_a, Topic.all.to_a
+ end
+ end
end
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index c15d57460b..a95ab0f429 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,58 +124,96 @@ 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
end
+ def test_scoped_find_with_annotation
+ Developer.annotate("scoped").scoping do
+ developer = nil
+ assert_sql(%r{/\* scoped \*/}) do
+ developer = Developer.where("name = 'David'").first
+ end
+ assert_equal "David", developer.name
+ end
+ end
+
+ def test_find_with_annotation_unscoped
+ Developer.annotate("scoped").unscoped do
+ developer = nil
+ log = capture_sql do
+ developer = Developer.where("name = 'David'").first
+ end
+
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\* scoped \*/}) }, :empty?
+
+ assert_equal "David", developer.name
+ end
+ end
+
+ def test_find_with_annotation_unscope
+ developer = nil
+ log = capture_sql do
+ developer = Developer.annotate("unscope").
+ where("name = 'David'").
+ unscope(:annotate).first
+ end
+
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\* unscope \*/}) }, :empty?
+
+ assert_equal "David", developer.name
+ end
+
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 +233,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
@@ -211,35 +251,82 @@ class RelationScopingTest < ActiveRecord::TestCase
def test_current_scope_does_not_pollute_sibling_subclasses
Comment.none.scoping do
- assert_not SpecialComment.all.any?
- assert_not VerySpecialComment.all.any?
- assert_not SubSpecialComment.all.any?
+ assert_not_predicate SpecialComment.all, :any?
+ assert_not_predicate VerySpecialComment.all, :any?
+ assert_not_predicate SubSpecialComment.all, :any?
end
SpecialComment.none.scoping do
- assert Comment.all.any?
- assert VerySpecialComment.all.any?
- assert_not SubSpecialComment.all.any?
+ assert_predicate Comment.all, :any?
+ assert_predicate VerySpecialComment.all, :any?
+ assert_not_predicate SubSpecialComment.all, :any?
end
SubSpecialComment.none.scoping do
- assert Comment.all.any?
- assert VerySpecialComment.all.any?
- assert SpecialComment.all.any?
+ assert_predicate Comment.all, :any?
+ assert_predicate VerySpecialComment.all, :any?
+ assert_predicate SpecialComment.all, :any?
+ end
+ end
+
+ 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_with_klass_method_works_in_the_scope_block
+ expected = SpecialPostWithDefaultScope.unscoped.to_a
+ assert_equal expected, SpecialPostWithDefaultScope.unscoped_all
+ end
+
+ def test_scoping_with_query_method_works_in_the_scope_block
+ expected = SpecialPostWithDefaultScope.unscoped.where(author_id: 0).to_a
+ assert_equal expected, SpecialPostWithDefaultScope.authorless
+ 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
@@ -253,39 +340,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
@@ -293,15 +380,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
@@ -313,24 +400,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
@@ -345,7 +432,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
@@ -359,19 +446,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 14b80f4df4..932780bfef 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -1,9 +1,11 @@
+# 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
@@ -12,14 +14,14 @@ class SerializationTest < ActiveRecord::TestCase
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..ecf81b2042 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -1,16 +1,23 @@
-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/person"
+require "models/traffic_light"
+require "models/post"
class SerializedAttributeTest < ActiveRecord::TestCase
fixtures :topics, :posts
MyObject = Struct.new :attribute1, :attribute2
+ class Topic < ActiveRecord::Base
+ serialize :content
+ end
+
+ class ImportantTopic < Topic
+ serialize :important, Hash
+ end
+
teardown do
Topic.serialize("content")
end
@@ -25,7 +32,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 +43,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)
@@ -47,10 +54,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase
def test_serialized_attributes_from_database_on_subclass
Topic.serialize :content, Hash
- t = Reply.new(content: { foo: :bar })
+ t = ImportantTopic.new(content: { foo: :bar })
assert_equal({ foo: :bar }, t.content)
t.save!
- t = Reply.last
+ t = ImportantTopic.last
assert_equal({ foo: :bar }, t.content)
end
@@ -97,7 +104,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 +114,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 +131,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 +159,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 +203,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 +228,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 +248,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 +261,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 +298,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 +327,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 +337,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 = Class.new(Topic)
+
+ topic = model.create!
+ 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 104226010a..6a6d73dc38 100644
--- a/activerecord/test/cases/statement_cache_test.rb
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -1,8 +1,11 @@
-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/numeric_data"
+require "models/electron"
module ActiveRecord
class StatementCacheTest < ActiveRecord::TestCase
@@ -10,22 +13,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 +35,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 +51,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,23 +58,28 @@ 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
+ def test_statement_cache_with_strictly_cast_attribute
+ row = NumericData.create(temperature: 1.5)
+ assert_equal row, NumericData.find_by(temperature: 1.5)
+ end
+
def test_statement_cache_values_differ
cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
Book.where(name: "my book")
@@ -85,13 +89,13 @@ 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
@@ -104,7 +108,33 @@ module ActiveRecord
Book.find_by(name: "my other book")
end
- refute_equal book, other_book
+ assert_not_equal book, other_book
+ end
+
+ def test_find_by_does_not_use_statement_cache_if_table_name_is_changed
+ 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/statement_invalid_test.rb b/activerecord/test/cases/statement_invalid_test.rb
new file mode 100644
index 0000000000..16ea69c1bd
--- /dev/null
+++ b/activerecord/test/cases/statement_invalid_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/book"
+
+module ActiveRecord
+ class StatementInvalidTest < ActiveRecord::TestCase
+ fixtures :books
+
+ class MockDatabaseError < StandardError
+ def result
+ 0
+ end
+
+ def error_number
+ 0
+ end
+ end
+
+ test "message contains no sql" do
+ sql = Book.where(author_id: 96, cover: "hard").to_sql
+ error = assert_raises(ActiveRecord::StatementInvalid) do
+ Book.connection.send(:log, sql, Book.name) do
+ raise MockDatabaseError
+ end
+ end
+ assert_not error.message.include?("SELECT")
+ end
+
+ test "statement and binds are set on select" do
+ sql = Book.where(author_id: 96, cover: "hard").to_sql
+ binds = [Minitest::Mock.new, Minitest::Mock.new]
+ error = assert_raises(ActiveRecord::StatementInvalid) do
+ Book.connection.send(:log, sql, Book.name, binds) do
+ raise MockDatabaseError
+ end
+ end
+ assert_equal error.sql, sql
+ assert_equal error.binds, binds
+ end
+ end
+end
diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb
index bce86875e1..91c0e959f4 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -1,60 +1,150 @@
-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 "updating the store will mark accessor as changed" do
+ @john.color = "red"
+ assert @john.color_changed?
+ end
+
+ test "new record and no accessors changes" do
+ user = Admin::User.new
+ assert_not user.color_changed?
+ assert_nil user.color_was
+ assert_nil user.color_change
+
+ user.color = "red"
+ assert user.color_changed?
+ assert_nil user.color_was
+ assert_equal "red", user.color_change[1]
+ end
+
+ test "updating the store won't mark accessor as changed if the whole store was updated" do
+ @john.settings = { color: @john.color, some: "thing" }
+ assert @john.settings_changed?
+ assert_not @john.color_changed?
+ end
+
+ test "updating the store populates the accessor changed array correctly" do
+ @john.color = "red"
+ assert_equal "black", @john.color_was
+ assert_equal "black", @john.color_change[0]
+ assert_equal "red", @john.color_change[1]
+ end
+
+ test "updating the store won't mark accessor as changed if the value isn't changed" do
+ @john.color = @john.color
+ assert_not @john.color_changed?
+ end
+
+ test "nullifying the store mark accessor as changed" do
+ color = @john.color
+ @john.settings = nil
+ assert @john.color_changed?
+ assert_equal color, @john.color_was
+ assert_equal [color, nil], @john.color_change
+ end
+
+ test "dirty methods for suffixed accessors" do
+ @john.configs[:two_factor_auth] = true
+ assert @john.two_factor_auth_configs_changed?
+ assert_nil @john.two_factor_auth_configs_was
+ assert_equal [nil, true], @john.two_factor_auth_configs_change
+ end
+
+ test "dirty methods for prefixed accessors" do
+ @john.spouse[:name] = "Lena"
+ assert @john.partner_name_changed?
+ assert_equal "Dallas", @john.partner_name_was
+ assert_equal ["Dallas", "Lena"], @john.partner_name_change
+ end
+
+ test "saved changes tracking for accessors" do
+ @john.spouse[:name] = "Lena"
+ assert @john.partner_name_changed?
+
+ @john.save!
+ assert_not @john.partner_name_change
+ assert @john.saved_change_to_partner_name?
+ assert_equal ["Dallas", "Lena"], @john.saved_change_to_partner_name
+ assert_equal "Dallas", @john.partner_name_before_last_save
end
test "object initialization with not nullable column" do
@@ -67,75 +157,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 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
@@ -192,4 +282,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 7d44e36419..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
@@ -60,4 +62,16 @@ class SuppressorTest < ActiveRecord::TestCase
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 0aac5bad31..06f11108f9 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -1,13 +1,41 @@
-require 'cases/helper'
-require 'active_record/tasks/database_tasks'
+# frozen_string_literal: true
+
+require "cases/helper"
+require "active_record/tasks/database_tasks"
+require "models/author"
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
@@ -17,30 +45,64 @@ module ActiveRecord
sqlite3: :sqlite_tasks
}
- class DatabaseTasksUtilsTask< ActiveRecord::TestCase
+ class DatabaseTasksUtilsTask < ActiveRecord::TestCase
def test_raises_an_error_when_called_with_protected_environment
- ActiveRecord::Migrator.stubs(:current_version).returns(1)
+ 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!
- protected_environments = ActiveRecord::Base.protected_environments.dup
- current_env = ActiveRecord::Migrator.current_environment
- assert !protected_environments.include?(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
- ActiveRecord::Base.protected_environments << current_env
- assert_raise(ActiveRecord::ProtectedEnvironmentError) do
+ 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.stubs(:table_exists?).returns(false)
- ActiveRecord::Migrator.stubs(:current_version).returns(1)
-
- assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do
- ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+ 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
@@ -53,16 +115,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
@@ -72,125 +135,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)
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).never
+ @configurations["development"]["host"] = "my.server.tld"
- 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')
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
+ @configurations["development"]["host"] = "localhost"
- 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)
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
+ @configurations["development"]["host"] = nil
- 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')
-
- 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_test_and_development_databases_when_rails_env_is_development
- old_env = ENV['RAILS_ENV']
- ENV['RAILS_ENV'] = 'development'
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with('database' => 'dev-db')
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with('database' => 'test-db')
-
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new('development')
- )
+ 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
+ ENV["RAILS_ENV"] = old_env
end
- def test_establishes_connection_for_the_given_environment
- ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true
+ 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
- ActiveRecord::Base.expects(:establish_connection).with(:development)
+ private
+ def with_stubbed_configurations_establish_connection
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new('development')
- )
+ ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do
+ yield
+ end
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
+ end
+
+ class DatabaseTasksCreateCurrentThreeTierTest < ActiveRecord::TestCase
+ def setup
+ @configurations = {
+ "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } },
+ "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } },
+ "production" => { "primary" => { "url" => "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
@@ -198,140 +460,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')
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
+ @configurations[:development]["host"] = "localhost"
- 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" }
}
-
- ActiveRecord::Base.stubs(:configurations).returns(@configurations)
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"]) 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"]) 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')
-
- ActiveRecord::Tasks::DatabaseTasks.drop_current(
- ActiveSupport::StringInquirer.new('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
end
def test_drops_testand_development_databases_when_rails_env_is_development
- old_env = ENV['RAILS_ENV']
- ENV['RAILS_ENV'] = 'development'
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with('database' => 'dev-db')
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with('database' => 'test-db')
-
- ActiveRecord::Tasks::DatabaseTasks.drop_current(
- ActiveSupport::StringInquirer.new('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
+ 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
- class DatabaseTasksMigrateTest < ActiveRecord::TestCase
+ class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase
def setup
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths = 'custom/path'
+ @configurations = {
+ "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } },
+ "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } },
+ "production" => { "primary" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } }
+ }
end
- def teardown
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil
+ def test_drops_current_environment_database
+ 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
- def test_migrate_receives_correct_env_vars
- verbose, version = ENV['VERBOSE'], ENV['VERSION']
+ 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
- ENV['VERBOSE'] = 'false'
- ENV['VERSION'] = '4'
+ 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" => "secondary-dev-db"],
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+ end
- ActiveRecord::Migrator.expects(:migrate).with('custom/path', 4)
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ 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['VERBOSE'], ENV['VERSION'] = verbose, version
+ 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
+
+ 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 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["VERSION"] = "0 "
+ 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_"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1_name"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_migrate_raise_error_on_failed_check_target_version
+ ActiveRecord::Tasks::DatabaseTasks.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
- ActiveRecord::Base.expects(:clear_cache!)
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ assert_called(ActiveRecord::Base, :clear_cache!) do
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
end
end
@@ -340,48 +893,193 @@ 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)
+ 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
+
+ unless in_memory_db?
+ class DatabaseTasksTruncateAllTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
- ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
- with('database' => 'my-db')
+ fixtures :authors, :author_addresses
- ActiveRecord::Tasks::DatabaseTasks.purge_all
+ def teardown
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
+
+ def test_truncate_tables
+ assert_operator Author.count, :>, 0
+ assert_operator AuthorAddress.count, :>, 0
+
+ old_configurations = ActiveRecord::Base.configurations
+ configurations = { development: ActiveRecord::Base.configurations["arunit"] }
+ ActiveRecord::Base.configurations = configurations
+
+ ActiveRecord::Tasks::DatabaseTasks.stub(:root, nil) do
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+
+ assert_equal 0, Author.count
+ assert_equal 0, AuthorAddress.count
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
end
end
+ class DatabaseTasksTruncateAllWithMultipleDatabasesTest < 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_truncate_all_databases_for_environment
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :truncate_tables,
+ [
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+ end
+ end
+
+ def test_truncate_all_databases_with_url_for_environment
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :truncate_tables,
+ [
+ ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"],
+ ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
+ end
+ end
+
+ def test_truncate_all_development_databases_when_env_is_not_specified
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :truncate_tables,
+ [
+ ["database" => "dev-db"],
+ ["database" => "secondary-dev-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+ end
+
+ def test_truncate_all_development_databases_when_env_is_development
+ old_env = ENV["RAILS_ENV"]
+ ENV["RAILS_ENV"] = "development"
+
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :truncate_tables,
+ [
+ ["database" => "dev-db"],
+ ["database" => "secondary-dev-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all(
+ 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
+
class DatabaseTasksCharsetTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
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
@@ -391,19 +1089,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
@@ -413,31 +1222,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 1632f04854..552e623fd4 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -1,312 +1,409 @@
-require 'cases/helper'
+# frozen_string_literal: true
-if current_adapter?(:Mysql2Adapter)
-module ActiveRecord
- class MysqlDBCreateTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:create_database => true)
- @configuration = {
- 'adapter' => 'mysql2',
- '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' => 'mysql2', 'database' => nil)
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_creates_database_with_no_default_options
- @connection.expects(:create_database).
- with('my-app-db', {})
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_creates_database_with_given_encoding
- @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
- @connection.expects(:create_database).
- with('my-app-db', collation: 'latin1_swedish_ci')
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge('collation' => 'latin1_swedish_ci')
- end
-
- def test_establishes_connection_to_database
- ActiveRecord::Base.expects(:establish_connection).with(@configuration)
-
- 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
-
- class MysqlDBCreateAsRootTest < ActiveRecord::TestCase
- def setup
- @connection = stub("Connection", create_database: true)
- @error = Mysql2::Error.new("Invalid permissions")
- @configuration = {
- 'adapter' => 'mysql2',
- 'database' => 'my-app-db',
- 'username' => 'pat',
- 'password' => 'wossname'
- }
-
- $stdin.stubs(:gets).returns("secret\n")
- $stdout.stubs(:print).returns(nil)
- @error.stubs(:errno).returns(1045)
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).
- raises(@error).
- then.returns(true)
- end
-
- def test_root_password_is_requested
- assert_permissions_granted_for("pat")
- $stdin.expects(:gets).returns("secret\n")
+require "cases/helper"
+require "active_record/tasks/database_tasks"
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+if current_adapter?(:Mysql2Adapter)
+ module ActiveRecord
+ class MysqlDBCreateTest < ActiveRecord::TestCase
+ def setup
+ @connection = Class.new { def create_database(*); end }.new
+ @configuration = {
+ "adapter" => "mysql2",
+ "database" => "my-app-db"
+ }
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
- def test_connection_established_as_root
- assert_permissions_granted_for("pat")
- ActiveRecord::Base.expects(:establish_connection).with(
- 'adapter' => 'mysql2',
- 'database' => nil,
- 'username' => 'root',
- 'password' => 'secret'
- )
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ 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_database_created_by_root
- assert_permissions_granted_for("pat")
- @connection.expects(:create_database).
- with('my-app-db', {})
+ 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
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ 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
- end
+ 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
- end
+ 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' => 'mysql2',
- '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
end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ 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
- def test_sends_output_to_stderr_when_other_errors
- @error.stubs(:errno).returns(42)
+ private
+
+ def with_stubbed_connection_establish_connection
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ ActiveRecord::Base.stub(:connection, @connection) do
+ yield
+ end
+ end
+ end
+ 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
- $stderr.expects(:puts).at_least_once.returns(nil)
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @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
- private
-
- def assert_permissions_granted_for(db_user)
- db_name = @configuration['database']
- db_password = @configuration['password']
- @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;")
- end
- end
-
- class MySQLDBDropTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:drop_database => true)
- @configuration = {
- 'adapter' => 'mysql2',
- 'database' => '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::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_mysql_database
- ActiveRecord::Base.expects(:establish_connection).with @configuration
+ 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::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
+ assert_equal "Dropped database 'my-app-db'\n", $stdout.string
+ end
+ end
- class MySQLPurgeTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:recreate_database => true)
- @configuration = {
- 'adapter' => 'mysql2',
- 'database' => 'test-db'
- }
+ private
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
+ 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_establishes_connection_to_the_appropriate_database
- ActiveRecord::Base.expects(:establish_connection).with(@configuration)
+ 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
- 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
- def test_recreates_database_with_no_default_options
- @connection.expects(:recreate_database).
- with('test-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::Tasks::DatabaseTasks.purge @configuration
- 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
- def test_recreates_database_with_the_given_options
- @connection.expects(:recreate_database).
- with('test-db', charset: 'latin', collation: 'latin1_swedish_ci')
+ private
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge(
- 'encoding' => 'latin', 'collation' => 'latin1_swedish_ci')
+ 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 MysqlDBCharsetTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:create_database => true)
- @configuration = {
- 'adapter' => 'mysql2',
- 'database' => 'my-app-db'
- }
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- end
+ class MysqlDBCharsetTest < ActiveRecord::TestCase
+ def setup
+ @connection = Class.new { def charset; end }.new
+ @configuration = {
+ "adapter" => "mysql2",
+ "database" => "my-app-db"
+ }
+ end
- def test_db_retrieves_charset
- @connection.expects(:charset)
- ActiveRecord::Tasks::DatabaseTasks.charset @configuration
+ 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
- end
-
- class MysqlDBCollationTest < ActiveRecord::TestCase
- def setup
- @connection = stub(:create_database => true)
- @configuration = {
- 'adapter' => 'mysql2',
- 'database' => 'my-app-db'
- }
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
- end
+ class MysqlDBCollationTest < ActiveRecord::TestCase
+ def setup
+ @connection = Class.new { def collation; end }.new
+ @configuration = {
+ "adapter" => "mysql2",
+ "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 MySQLStructureDumpTest < ActiveRecord::TestCase
- def setup
- @configuration = {
- 'adapter' => 'mysql2',
- 'database' => 'test-db'
- }
- end
+ class MySQLStructureDumpTest < ActiveRecord::TestCase
+ def setup
+ @configuration = {
+ "adapter" => "mysql2",
+ "database" => "test-db"
+ }
+ end
- def test_structure_dump
- filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true)
+ 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
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- end
+ 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"]
- def test_warn_when_external_structure_dump_command_execution_fails
- filename = "awesome-file.sql"
- Kernel.expects(:system)
- .with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db")
- .returns(false)
+ 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
- e = assert_raise(RuntimeError) {
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- }
- assert_match(/^failed to execute: `mysqldump`$/, e.message)
- 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)
- end
- end
+ private
+ def with_structure_dump_flags(flags)
+ old = ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = flags
+ yield
+ ensure
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = old
+ end
+ end
+
+ class MySQLStructureLoadTest < ActiveRecord::TestCase
+ def setup
+ @configuration = {
+ "adapter" => "mysql2",
+ "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"
+ expected_command = ["mysql", "--noop", "--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db"]
- def test_structure_load
- filename = "awesome-file.sql"
- Kernel.expects(:system).with('mysql', '--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db")
- .returns(true)
+ 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)
+ 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 ba53f340ae..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
- assert_raises(Exception) { 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', '-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', '-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", '-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", '-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', '-q', '-f', filename, @configuration['database']).returns(true)
+ 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', '-q', '-f', filename, @configuration['database']).returns(true)
+ 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 0aea0c3b38..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
- assert_raises(Exception) { 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 87299c0dab..5b25432dc0 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
@@ -89,16 +105,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]
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im, /^\s*select .* from all_sequences/im]
mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im]
- postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i, /^\s*SELECT\b.*::regtype::oid\b/im]
sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im]
[oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
@@ -112,16 +128,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 1970fe82d0..4411410eda 100644
--- a/activerecord/test/cases/test_fixtures_test.rb
+++ b/activerecord/test/cases/test_fixtures_test.rb
@@ -1,30 +1,14 @@
-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
diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb
index 3b6e4dcc2b..2f534ea110 100644
--- a/activerecord/test/cases/time_precision_test.rb
+++ b/activerecord/test/cases/time_precision_test.rb
@@ -1,84 +1,123 @@
-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
- Foo.reset_column_information
- 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_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
+ teardown do
+ @connection.drop_table :foos, if_exists: true
+ 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
+ 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
- assert_nil Foo.columns_hash['start'].limit
- assert_nil Foo.columns_hash['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
+ 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
+
+ unless current_adapter?(:Mysql2Adapter)
+ def test_no_time_precision_isnt_truncated_on_assignment
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :start, :time
+ @connection.add_column :foos, :finish, :time, precision: 6
+
+ time = ::Time.now.change(nsec: 123)
+ foo = Foo.new(start: time, finish: time)
+
+ assert_equal 123, foo.start.nsec
+ assert_equal 0, foo.finish.nsec
+
+ foo.save!
+ foo.reload
+
+ assert_equal 0, foo.start.nsec
+ assert_equal 0, foo.finish.nsec
end
end
- 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
+ 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
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
+ assert_match %r{t\.time\s+"start",\s+precision: 4$}, output
+ assert_match %r{t\.time\s+"finish",\s+precision: 6$}, output
end
- end
-end
+ 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
+ end
end
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index 937b84bccc..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,12 +76,12 @@ 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
@@ -88,12 +90,22 @@ class TimestampTest < ActiveRecord::TestCase
@developer.touch(:created_at)
end
- assert !@developer.created_at_changed? , 'created_at should not be changed'
- assert !@developer.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
@@ -106,9 +118,9 @@ class TimestampTest < ActiveRecord::TestCase
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
@@ -137,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
@@ -160,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
@@ -228,20 +240,20 @@ class TimestampTest < ActiveRecord::TestCase
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
@@ -256,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
@@ -275,8 +287,8 @@ class TimestampTest < ActiveRecord::TestCase
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
@@ -293,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
@@ -315,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
@@ -341,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)
@@ -368,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)
@@ -399,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
@@ -419,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
@@ -488,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 b47769eed7..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
- assert_not 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
- assert_not 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
@@ -89,7 +100,7 @@ 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
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index 8a7f19293d..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
@@ -68,17 +70,17 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
def do_before_commit(on)
blocks = @before_commit[on] if defined?(@before_commit)
- blocks.each{|b| b.call(self)} if blocks
+ 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
@@ -88,7 +90,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
# 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!' }
+ @first.before_commit_block { raise "better pop this txn from the stack!" }
original_txn = @first.class.connection.current_transaction
@@ -101,8 +103,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
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
@@ -123,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!
@@ -131,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
@@ -155,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!
@@ -213,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
@@ -243,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
@@ -269,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
@@ -292,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!
@@ -303,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
@@ -322,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!
@@ -336,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
@@ -355,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
@@ -392,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
@@ -449,9 +510,52 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
end
end
+class CallbacksOnDestroyUpdateActionRaceTest < ActiveRecord::TestCase
+ class TopicWithHistory < ActiveRecord::Base
+ self.table_name = :topics
-class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
+ def self.clear_history
+ @@history = []
+ end
+
+ def self.history
+ @@history ||= []
+ end
+ end
+
+ class TopicWithCallbacksOnDestroy < TopicWithHistory
+ after_commit(on: :destroy) { |record| record.class.history << :destroy }
+ end
+
+ class TopicWithCallbacksOnUpdate < TopicWithHistory
+ after_commit(on: :update) { |record| record.class.history << :update }
+ end
+
+ def test_trigger_once_on_multiple_deletions
+ TopicWithCallbacksOnDestroy.clear_history
+ topic = TopicWithCallbacksOnDestroy.new
+ topic.save
+ topic_clone = TopicWithCallbacksOnDestroy.find(topic.id)
+ topic.destroy
+ topic_clone.destroy
+ assert_equal [:destroy], TopicWithCallbacksOnDestroy.history
+ end
+
+ def test_trigger_on_update_where_row_was_deleted
+ TopicWithCallbacksOnUpdate.clear_history
+ topic = TopicWithCallbacksOnUpdate.new
+ topic.save
+ topic_clone = TopicWithCallbacksOnUpdate.find(topic.id)
+ topic.destroy
+ topic_clone.author_name = "Test Author"
+ topic_clone.save
+
+ assert_equal [], TopicWithCallbacksOnUpdate.history
+ end
+end
+
+class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
class TopicWithoutTransactionalEnrollmentCallbacks < ActiveRecord::Base
self.table_name = :topics
@@ -470,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)
@@ -487,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)
@@ -508,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 791b895d02..1009dd0f99 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
@@ -78,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
@@ -98,7 +100,7 @@ class TransactionTest < ActiveRecord::TestCase
end
Topic.transaction do
- @first.approved = true
+ @first.approved = true
@first.save!
end
@@ -119,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
@@ -136,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
@@ -181,10 +183,10 @@ class TransactionTest < ActiveRecord::TestCase
end
def test_transaction_state_is_cleared_when_record_is_persisted
- author = Author.create! name: 'foo'
+ author = Author.create! name: "foo"
author.name = nil
assert_not author.save
- assert_not author.new_record?
+ assert_not_predicate author, :new_record?
end
def test_update_should_rollback_on_failure
@@ -192,7 +194,7 @@ class TransactionTest < ActiveRecord::TestCase
posts_count = author.posts.size
assert posts_count > 0
status = author.update(name: nil, post_ids: [])
- assert !status
+ assert_not status
assert_equal posts_count, author.posts.reload.size
end
@@ -206,21 +208,11 @@ class TransactionTest < ActiveRecord::TestCase
assert_equal posts_count, author.posts.reload.size
end
- def test_cancellation_from_returning_false_in_before_filter
- def @first.before_save_for_transaction
- false
- end
-
- assert_deprecated do
- @first.save
- end
- end
-
def test_cancellation_from_before_destroy_rollbacks_in_destroy
add_cancelling_before_destroy_with_db_side_effect_to_topic @first
nbooks_before_destroy = Book.count
status = @first.destroy
- assert !status
+ assert_not status
@first.reload
assert_equal nbooks_before_destroy, Book.count
end
@@ -230,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
@@ -241,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!
@@ -256,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)
@@ -279,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
@@ -291,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
@@ -307,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
@@ -321,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
@@ -342,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
@@ -351,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
@@ -363,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
@@ -372,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
@@ -393,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
@@ -403,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
@@ -443,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?
@@ -493,30 +571,30 @@ class TransactionTest < ActiveRecord::TestCase
def test_rollback_when_commit_raises
assert_called(Topic.connection, :begin_db_transaction) do
- Topic.connection.stub(:commit_db_transaction, ->{ raise('OH NOES') }) 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
- # do nothing
+ Topic.connection.materialize_transactions
end
end
- assert_equal 'OH NOES', e.message
+ 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(FrozenError) { 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
@@ -541,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
@@ -562,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
@@ -596,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
@@ -606,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
@@ -615,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
@@ -629,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
@@ -660,38 +860,65 @@ 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_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 !transaction.state.rolledback?
- assert transaction.state.committed?
+ 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
@@ -702,20 +929,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
- %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)
+ 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
+
+ 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
@@ -732,7 +1029,7 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
raise
end
rescue
- assert !@first.reload.approved?
+ assert_not_predicate @first.reload, :approved?
end
end
@@ -753,31 +1050,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
index bc4900e1c2..c9558e25b5 100644
--- a/activerecord/test/cases/type/date_time_test.rb
+++ b/activerecord/test/cases/type/date_time_test.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/task"
module ActiveRecord
module Type
- class IntegerTest < ActiveRecord::TestCase
+ 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)
diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb
index c0932d5357..15d1a675a1 100644
--- a/activerecord/test/cases/type/integer_test.rb
+++ b/activerecord/test/cases/type/integer_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/company"
@@ -6,13 +8,13 @@ module ActiveRecord
class IntegerTest < ActiveRecord::TestCase
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 "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 6fe6d46711..9e7810a6a5 100644
--- a/activerecord/test/cases/type/string_test.rb
+++ b/activerecord/test/cases/type/string_test.rb
@@ -1,22 +1,24 @@
-require 'cases/helper'
+# frozen_string_literal: true
+
+require "cases/helper"
module ActiveRecord
class StringTypeTest < ActiveRecord::TestCase
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/time_test.rb b/activerecord/test/cases/type/time_test.rb
new file mode 100644
index 0000000000..1a2c47479f
--- /dev/null
+++ b/activerecord/test/cases/type/time_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+
+module ActiveRecord
+ module Type
+ class TimeTest < ActiveRecord::TestCase
+ def test_default_year_is_correct
+ expected_time = ::Time.utc(2000, 1, 1, 10, 30, 0)
+ topic = Topic.new(bonus_time: { 4 => 10, 5 => 30 })
+
+ assert_equal expected_time, topic.bonus_time
+
+ topic.save!
+ topic.reload
+
+ assert_equal expected_time, topic.bonus_time
+ end
+ 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
new file mode 100644
index 0000000000..dd05cf3fff
--- /dev/null
+++ b/activerecord/test/cases/type/unsigned_integer_test.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+module ActiveRecord
+ module Type
+ class UnsignedIntegerTest < ActiveRecord::TestCase
+ test "unsigned int max value is in range" do
+ assert_equal(4294967295, UnsignedInteger.new.serialize(4294967295))
+ end
+
+ test "minus value is out of range" do
+ assert_raises(ActiveModel::RangeError) do
+ UnsignedInteger.new.serialize(-1)
+ end
+ end
+ end
+ 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 81fcf04a27..3f7fb0a604 100644
--- a/activerecord/test/cases/types_test.rb
+++ b/activerecord/test/cases/types_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
module ActiveRecord
@@ -9,7 +11,7 @@ module ActiveRecord
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..49746996bc 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
@@ -9,6 +11,12 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase
def setup
@underlying = ActiveRecord::Base.connection
@specification = ActiveRecord::Base.remove_connection
+
+ # Clear out connection info from other pids (like a fork parent) too
+ pool_map = ActiveRecord::Base.connection_handler.instance_variable_get(:@owner_to_pool)
+ (pool_map.keys - [Process.pid]).each do |other_pid|
+ pool_map.delete(other_pid)
+ end
end
teardown do
@@ -27,7 +35,15 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase
end
end
+ def test_error_message_when_connection_not_established
+ error = assert_raise(ActiveRecord::ConnectionNotEstablished) do
+ TestRecord.find(1)
+ end
+
+ assert_equal "No connection pool with 'primary' found.", error.message
+ end
+
def test_underlying_adapter_no_longer_active
- assert !@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 c0b3750bcc..1982734f02 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,22 +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_validates_absence_of_virtual_attribute_on_model
repair_validations(Interest) do
- Interest.send(:attr_accessor, :token)
+ Interest.attr_accessor(:token)
Interest.validates_absence_of(:token)
- interest = Interest.create!(topic: 'Thought Leadering')
- assert interest.valid?
+ interest = Interest.create!(topic: "Thought Leadering")
+ assert_predicate interest, :valid?
- interest.token = 'tl'
+ interest.token = "tl"
- assert interest.invalid?
+ assert_predicate interest, :invalid?
end
end
end
diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb
index 584a3dc0d8..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,9 +42,9 @@ class AssociationValidationTest < ActiveRecord::TestCase
Reply.validates_presence_of(:content)
t = Topic.new
t.replies << Reply.new
- assert t.invalid?
+ assert_predicate t, :invalid?
t.replies.first.mark_for_destruction
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_associated_without_marked_for_destruction
@@ -54,11 +56,11 @@ class AssociationValidationTest < ActiveRecord::TestCase
Topic.validates_associated(:replies)
t = Topic.new
t.define_singleton_method(:replies) { [reply.new] }
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_associated_with_custom_message_using_quotes
- 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")
@@ -69,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
@@ -89,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..993c201f03 100644
--- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -1,17 +1,23 @@
+# frozen_string_literal: true
+
require "cases/helper"
-require 'models/topic'
+require "models/topic"
class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
+ class Backend < I18n::Backend::Simple
+ include I18n::Backend::Fallbacks
+ end
+
def setup
Topic.clear_validators!
@topic = Topic.new
- I18n.backend = I18n::Backend::Simple.new
+ I18n.backend = Backend.new
end
def reset_i18n_load_path
@old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
I18n.load_path.clear
- I18n.backend = I18n::Backend::Simple.new
+ I18n.backend = Backend.new
yield
ensure
I18n.load_path.replace @old_load_path
@@ -20,20 +26,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 +53,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 +62,41 @@ 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
+
+ test "activerecord attributes scope falls back to parent locale before it falls back to the :errors namespace" do
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", activerecord: { errors: { models: { topic: { attributes: { title: { taken: "custom en message" } } } } } }
+ I18n.backend.store_translations "en-US", errors: { messages: { taken: "generic en-US fallback" } }
+
+ I18n.with_locale "en-US" do
+ assert_equal "custom en message", @topic.errors.generate_message(:title, :taken, value: "title")
+ assert_equal "generic en-US fallback", @topic.errors.generate_message(:heading, :taken, value: "heading")
+ end
end
end
end
diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb
index b8307d6665..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,22 +38,20 @@ 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] }, {}]
]
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
- assert_called_with(@topic.errors, :generate_message, [:title, :taken, generate_message_options.merge(:value => 'unique!')]) do
+ assert_called_with(@topic.errors, :generate_message, [:title, :taken, generate_message_options.merge(value: "unique!")]) do
@topic.valid?
end
end
@@ -60,27 +60,26 @@ class I18nValidationTest < ActiveRecord::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_associated on generated message #{name}" do
Topic.validates_associated :replies, validation_options
- assert_called_with(replied_topic.errors, :generate_message, [:replies, :invalid, generate_message_options.merge(:value => replied_topic.replies)]) do
+ 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
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 78263fd955..a7cb718043 100644
--- a/activerecord/test/cases/validations/length_validation_test.rb
+++ b/activerecord/test/cases/validations/length_validation_test.rb
@@ -1,79 +1,80 @@
+# 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_validates_length_of_virtual_attribute_on_model
repair_validations(Pet) do
- Pet.send(:attr_accessor, :nickname)
+ Pet.attr_accessor(:nickname)
Pet.validates_length_of(:name, minimum: 1)
Pet.validates_length_of(:nickname, minimum: 1)
- pet = Pet.create!(name: 'Fancy Pants', nickname: 'Fancy')
+ pet = Pet.create!(name: "Fancy Pants", nickname: "Fancy")
- assert pet.valid?
+ assert_predicate pet, :valid?
- pet.nickname = ''
+ pet.nickname = ""
- assert pet.invalid?
+ assert_predicate pet, :invalid?
end
end
end
diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb
index 868d111b8c..4b9cbe9098 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
@@ -67,16 +69,16 @@ class PresenceValidationTest < ActiveRecord::TestCase
def test_validates_presence_of_virtual_attribute_on_model
repair_validations(Interest) do
- Interest.send(:attr_accessor, :abbreviation)
+ Interest.attr_accessor(:abbreviation)
Interest.validates_presence_of(:topic)
Interest.validates_presence_of(:abbreviation)
- interest = Interest.create!(topic: 'Thought Leadering', abbreviation: 'tl')
- assert interest.valid?
+ interest = Interest.create!(topic: "Thought Leadering", abbreviation: "tl")
+ assert_predicate interest, :valid?
- interest.abbreviation = ''
+ interest.abbreviation = ""
- assert interest.invalid?
+ assert_predicate interest, :invalid?
end
end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 4c14d93c66..76163e3093 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -1,11 +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/dashboard'
-require 'models/uuid_item'
+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
@@ -26,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
@@ -38,13 +43,13 @@ 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
@@ -57,14 +62,14 @@ class TopicWithAfterCreate < Topic
after_create :set_author
def set_author
- update_attributes!(:author_name => "#{title} #{id}")
+ 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)
@@ -78,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"
@@ -90,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
@@ -101,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!")
@@ -141,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"
@@ -151,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!")
@@ -160,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
@@ -168,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
@@ -180,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
@@ -190,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
@@ -199,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!")
@@ -207,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"
@@ -232,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"
@@ -256,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"
@@ -277,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"
@@ -289,8 +314,53 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t3.save, "Should save t3 as unique"
end
+ if current_adapter?(:Mysql2Adapter)
+ def test_deprecate_validate_uniqueness_mismatched_collation
+ Topic.validates_uniqueness_of(:author_email_address)
+
+ topic1 = Topic.new(author_email_address: "david@loudthinking.com")
+ topic2 = Topic.new(author_email_address: "David@loudthinking.com")
+
+ assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
+
+ assert_deprecated do
+ assert_not topic1.valid?
+ assert_not topic1.save
+ assert topic2.valid?
+ assert topic2.save
+ end
+
+ assert_equal 2, Topic.where(author_email_address: "david@loudthinking.com").count
+ assert_equal 2, Topic.where(author_email_address: "David@loudthinking.com").count
+ end
+ end
+
+ def test_validate_case_sensitive_uniqueness_by_default
+ Topic.validates_uniqueness_of(:author_email_address)
+
+ topic1 = Topic.new(author_email_address: "david@loudthinking.com")
+ topic2 = Topic.new(author_email_address: "David@loudthinking.com")
+
+ assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
+
+ ActiveSupport::Deprecation.silence do
+ assert_not topic1.valid?
+ assert_not topic1.save
+ assert topic2.valid?
+ assert topic2.save
+ end
+
+ if current_adapter?(:Mysql2Adapter)
+ assert_equal 2, Topic.where(author_email_address: "david@loudthinking.com").count
+ assert_equal 2, Topic.where(author_email_address: "David@loudthinking.com").count
+ else
+ assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
+ assert_equal 1, Topic.where(author_email_address: "David@loudthinking.com").count
+ end
+ 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"
@@ -301,41 +371,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
@@ -349,46 +419,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
@@ -399,7 +491,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"
@@ -413,16 +505,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase
def test_validate_uniqueness_on_existing_relation
event = Event.create
- assert TopicWithUniqEvent.create(event: event).valid?
+ assert_predicate TopicWithUniqEvent.create(event: event), :valid?
topic = TopicWithUniqEvent.new(event: event)
- assert_not topic.valid?
- assert_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
@@ -441,7 +533,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
key2 = klass.create!(key_number: 11)
key2.key_number = 10
- assert_not key2.valid?
+ assert_not_predicate key2, :valid?
end
def test_validate_uniqueness_without_primary_key
@@ -454,8 +546,8 @@ class UniquenessValidationTest < ActiveRecord::TestCase
end
abc = klass.create!(dashboard_id: "abc")
- assert klass.new(dashboard_id: "xyz").valid?
- assert_not klass.new(dashboard_id: "abc").valid?
+ assert_predicate klass.new(dashboard_id: "xyz"), :valid?
+ assert_not_predicate klass.new(dashboard_id: "abc"), :valid?
abc.dashboard_id = "def"
@@ -463,7 +555,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
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)
+ assert_match(/Cannot validate uniqueness for persisted record without primary key.\z/, e.message)
end
def test_validate_uniqueness_ignores_itself_when_primary_key_changed
@@ -479,31 +571,31 @@ class UniquenessValidationTest < ActiveRecord::TestCase
def test_validate_uniqueness_with_after_create_performing_save
TopicWithAfterCreate.validates_uniqueness_of(:title)
- topic = TopicWithAfterCreate.create!(:title => "Title1")
+ topic = TopicWithAfterCreate.create!(title: "Title1")
assert topic.author_name.start_with?("Title1")
- topic2 = TopicWithAfterCreate.new(:title => "Title1")
- refute topic2.valid?
+ 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')
+ 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')
+ item2 = UuidValidatingItem.create!(uuid: SecureRandom.uuid, title: "item2")
+ item2.update(title: "item2-title2")
assert_empty item2.errors
end
def test_validate_uniqueness_regular_id
- item = CoolTopic.create!(title: 'MyItem')
+ item = CoolTopic.create!(title: "MyItem")
assert_empty item.errors
- item2 = CoolTopic.new(id: item.id, title: 'MyItem2')
- refute item2.valid?
+ item2 = CoolTopic.new(id: item.id, title: "MyItem2")
+ assert_not_predicate item2, :valid?
assert_equal(["has already been taken"], item2.errors[:id])
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 85e33d2218..9a70934b7e 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"
@@ -53,7 +55,7 @@ class ValidationsTest < ActiveRecord::TestCase
end
def test_invalid_using_multiple_contexts
- r = WrongReply.new(:title => 'Wrong Create')
+ 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
@@ -95,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
@@ -107,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
@@ -123,8 +125,8 @@ 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
@@ -137,11 +139,18 @@ class ValidationsTest < ActiveRecord::TestCase
def test_throw_away_typing
d = Developer.new("name" => "David", "salary" => "100,000")
- assert !d.valid?
+ assert_not_predicate d, :valid?
assert_equal 100, d.salary
assert_equal "100,000", d.salary_before_type_cast
end
+ def test_validates_acceptance_of_with_undefined_attribute_methods
+ Topic.validates_acceptance_of(:approved)
+ topic = Topic.new(approved: true)
+ Topic.undefine_attribute_methods
+ assert topic.approved
+ end
+
def test_validates_acceptance_of_as_database_column
Topic.validates_acceptance_of(:approved)
topic = Topic.create("approved" => true)
@@ -156,22 +165,50 @@ 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_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 topic.valid?
- ensure
- Topic.reset_column_information
+ 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'
+ self.table_name = "posts"
end
klass.reset_column_information
diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb
index f3c2d2f30e..36b9df7ba5 100644
--- a/activerecord/test/cases/view_test.rb
+++ b/activerecord/test/cases/view_test.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "cases/helper"
require "models/book"
require "support/schema_dumping_helper"
@@ -11,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
@@ -44,8 +47,7 @@ module ViewBehavior
def test_table_exists
view_name = Ebook.table_name
- # TODO: switch this assertion around once we changed #tables to not return views.
- ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" }
+ assert_not @connection.table_exists?(view_name), "'#{view_name}' table should not exist"
end
def test_views_ara_valid_data_sources
@@ -60,157 +62,161 @@ 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
+ schema = dump_table_schema "ebooks'"
+ assert_no_match %r{create_table "ebooks'"}, schema
end
-end
-
-if ActiveRecord::Base.connection.supports_views?
-class ViewWithPrimaryKeyTest < ActiveRecord::TestCase
- include ViewBehavior
private
- def create_view(name, query)
- @connection.execute "CREATE VIEW #{name} AS #{query}"
- end
-
- def drop_view(name)
- @connection.execute "DROP VIEW #{name}" if @connection.view_exists? name
- end
+ def quote_table_name(name)
+ @connection.quote_table_name(name)
+ end
end
-class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
- include SchemaDumpingHelper
- fixtures :books
-
- 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
-
- 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
+if ActiveRecord::Base.connection.supports_views?
+ class ViewWithPrimaryKeyTest < ActiveRecord::TestCase
+ include ViewBehavior
- def test_views
- assert_equal [Paperback.table_name], @connection.views
- end
+ private
+ def create_view(name, query)
+ @connection.execute "CREATE VIEW #{quote_table_name(name)} AS #{query}"
+ end
- def test_view_exists
- view_name = Paperback.table_name
- assert @connection.view_exists?(view_name), "'#{view_name}' view should exist"
+ def drop_view(name)
+ @connection.execute "DROP VIEW #{quote_table_name(name)}" if @connection.view_exists? name
+ end
end
- def test_table_exists
- view_name = Paperback.table_name
- # TODO: switch this assertion around once we changed #tables to not return views.
- ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" }
- end
+ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ fixtures :books
- def test_column_definitions
- assert_equal([["name", :string],
- ["status", :integer]], Paperback.columns.map { |c| [c.name, c.type] })
- end
+ class Paperback < ActiveRecord::Base; end
- def test_attributes
- assert_equal({"name" => "Agile Web Development with Rails", "status" => 2},
- Paperback.first.attributes)
- end
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute <<~SQL
+ CREATE VIEW paperbacks
+ AS SELECT name, status FROM books WHERE format = 'paperback'
+ SQL
+ end
- def test_does_not_have_a_primary_key
- assert_nil Paperback.primary_key
- end
+ teardown do
+ @connection.execute "DROP VIEW paperbacks" if @connection.view_exists? "paperbacks"
+ 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_reading
+ books = Paperback.all
+ assert_equal ["Agile Web Development with Rails"], books.map(&:name)
+ end
-# sqlite dose not support CREATE, INSERT, and DELETE for VIEW
-if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
-class UpdateableViewTest < ActiveRecord::TestCase
- self.use_transactional_tests = false
- fixtures :books
+ def test_views
+ assert_equal [Paperback.table_name], @connection.views
+ end
- class PrintedBook < ActiveRecord::Base
- self.primary_key = "id"
- end
+ def test_view_exists
+ view_name = Paperback.table_name
+ assert @connection.view_exists?(view_name), "'#{view_name}' view should exist"
+ 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
+ def test_table_exists
+ view_name = Paperback.table_name
+ assert_not @connection.table_exists?(view_name), "'#{view_name}' table should not exist"
+ end
- teardown do
- @connection.execute "DROP VIEW printed_books" if @connection.view_exists? "printed_books"
- end
+ def test_column_definitions
+ assert_equal([["name", :string],
+ ["status", :integer]], Paperback.columns.map { |c| [c.name, c.type] })
+ end
- def test_update_record
- book = PrintedBook.first
- book.name = "AWDwR"
- book.save!
- book.reload
- assert_equal "AWDwR", book.name
- end
+ def test_attributes
+ assert_equal({ "name" => "Agile Web Development with Rails", "status" => 2 },
+ Paperback.first.attributes)
+ end
- def test_insert_record
- PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback"
+ def test_does_not_have_a_primary_key
+ assert_nil Paperback.primary_key
+ end
- new_book = PrintedBook.last
- assert_equal "Rails in Action", new_book.name
+ 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_update_record_to_fail_view_conditions
- book = PrintedBook.first
- book.format = "ebook"
- book.save!
-
- assert_raises ActiveRecord::RecordNotFound do
- book.reload
+ # sqlite dose not support CREATE, INSERT, and DELETE for VIEW
+ if current_adapter?(:Mysql2Adapter, :SQLServerAdapter, :PostgreSQLAdapter)
+
+ 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
-end # end fo `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)`
-end # end fo `if ActiveRecord::Base.connection.supports_views?`
+ end # end of `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)`
+end # end of `if ActiveRecord::Base.connection.supports_views?`
-if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) &&
- ActiveRecord::Base.connection.supports_materialized_views?
-class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase
- include ViewBehavior
+if 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
+ 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 #{name}" if @connection.view_exists? name
+ 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/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 58e2d45748..f5e3ac3c19 100644
--- a/activerecord/test/config.example.yml
+++ b/activerecord/test/config.example.yml
@@ -1,7 +1,5 @@
default_connection: <%= defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3' %>
-with_manual_interventions: false
-
connections:
jdbcderby:
arunit: activerecord_unittest
@@ -54,11 +52,18 @@ connections:
mysql2:
arunit:
username: rails
- encoding: utf8
- collation: utf8_unicode_ci
+ encoding: utf8mb4
+ collation: utf8mb4_unicode_ci
+<% if ENV['MYSQL_HOST'] %>
+ host: <%= ENV['MYSQL_HOST'] %>
+<% end %>
arunit2:
username: rails
- encoding: utf8
+ encoding: utf8mb4
+ collation: utf8mb4_general_ci
+<% if ENV['MYSQL_HOST'] %>
+ host: <%= ENV['MYSQL_HOST'] %>
+<% end %>
oracle:
arunit:
@@ -77,6 +82,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/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/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
index 3e10331105..76f66e01ae 100644
--- a/activerecord/test/fixtures/naked/yml/parrots.yml
+++ b/activerecord/test/fixtures/naked/yml/parrots.yml
@@ -1,2 +1,3 @@
george:
arrr: "Curious George"
+ foobar: Foobar
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
index 39ff763547..3e11a33802 100644
--- a/activerecord/test/fixtures/other_posts.yml
+++ b/activerecord/test/fixtures/other_posts.yml
@@ -5,3 +5,4 @@ second_welcome:
author_id: 1
title: Welcome to the another weblog
body: It's really nice today
+ comments_count: 1
diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml
index 86d46f753a..8d7e1e0ae7 100644
--- a/activerecord/test/fixtures/posts.yml
+++ b/activerecord/test/fixtures/posts.yml
@@ -28,6 +28,7 @@ sti_comments:
author_id: 1
title: sti comments
body: hello
+ comments_count: 5
type: Post
sti_post_and_comments:
@@ -35,6 +36,7 @@ sti_post_and_comments:
author_id: 1
title: sti me
body: hello
+ comments_count: 2
type: StiPost
sti_habtm:
@@ -50,6 +52,8 @@ eager_other:
title: eager loading with OR'd conditions
body: hello
type: Post
+ comments_count: 1
+ tags_count: 3
misc_by_bob:
id: 8
@@ -57,6 +61,7 @@ misc_by_bob:
title: misc post by bob
body: hello
type: Post
+ tags_count: 1
misc_by_mary:
id: 9
@@ -64,6 +69,7 @@ misc_by_mary:
title: misc post by mary
body: hello
type: Post
+ tags_count: 1
other_by_bob:
id: 10
@@ -71,6 +77,7 @@ other_by_bob:
title: other post by bob
body: hello
type: Post
+ tags_count: 1
other_by_mary:
id: 11
@@ -78,3 +85,4 @@ other_by_mary:
title: other post by mary
body: hello
type: Post
+ tags_count: 1
diff --git a/activerecord/test/fixtures/reserved_words/values.yml b/activerecord/test/fixtures/reserved_words/values.yml
index 7d109609ab..9ed9e5edc5 100644
--- a/activerecord/test/fixtures/reserved_words/values.yml
+++ b/activerecord/test/fixtures/reserved_words/values.yml
@@ -1,7 +1,7 @@
values1:
- id: 1
+ as: 1
group_id: 2
values2:
- id: 2
+ as: 2
group_id: 1
diff --git a/activerecord/test/fixtures/sponsors.yml b/activerecord/test/fixtures/sponsors.yml
index 2da541c539..02ddb8dd38 100644
--- a/activerecord/test/fixtures/sponsors.yml
+++ b/activerecord/test/fixtures/sponsors.yml
@@ -10,3 +10,6 @@ crazy_club_sponsor_for_groucho:
sponsor_club: crazy_club
sponsorable_id: 3
sponsorable_type: Member
+sponsor_for_author_david:
+ sponsorable_id: 1
+ sponsorable_type: Author
diff --git a/activerecord/test/fixtures/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 e908c9eabc..4b0d5fb6fa 100644
--- a/activerecord/test/migrations/10_urban/9_add_expressions.rb
+++ b/activerecord/test/migrations/10_urban/9_add_expressions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AddExpressions < ActiveRecord::Migration::Current
def self.up
create_table("expressions") do |t|
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 549647de86..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 @@
+# 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/activerecord/test/migrations/empty/.keep b/activerecord/test/migrations/empty/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ 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 53b263bf55..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::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 e046944e31..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,3 +1,5 @@
+# frozen_string_literal: true
+
class PeopleHaveMiddleNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "middle_name", :string
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 50fe2a9c8e..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,3 +1,5 @@
+# frozen_string_literal: true
+
class PeopleHaveLastNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "last_name", :string
diff --git a/activerecord/test/migrations/missing/3_we_need_reminders.rb b/activerecord/test/migrations/missing/3_we_need_reminders.rb
index d7c63ac892..4647268c6e 100644
--- a/activerecord/test/migrations/missing/3_we_need_reminders.rb
+++ b/activerecord/test/migrations/missing/3_we_need_reminders.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class WeNeedReminders < ActiveRecord::Migration::Current
def self.up
create_table("reminders") do |t|
diff --git a/activerecord/test/migrations/missing/4_innocent_jointable.rb b/activerecord/test/migrations/missing/4_innocent_jointable.rb
index 20fe183777..8063bc0558 100644
--- a/activerecord/test/migrations/missing/4_innocent_jointable.rb
+++ b/activerecord/test/migrations/missing/4_innocent_jointable.rb
@@ -1,6 +1,8 @@
+# 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 9dce01acfd..8e71a1d996 100644
--- a/activerecord/test/migrations/rename/1_we_need_things.rb
+++ b/activerecord/test/migrations/rename/1_we_need_things.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class WeNeedThings < ActiveRecord::Migration::Current
def self.up
create_table("things") do |t|
diff --git a/activerecord/test/migrations/rename/2_rename_things.rb b/activerecord/test/migrations/rename/2_rename_things.rb
index cb8484e7dc..110fe3f0fa 100644
--- a/activerecord/test/migrations/rename/2_rename_things.rb
+++ b/activerecord/test/migrations/rename/2_rename_things.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RenameThings < ActiveRecord::Migration::Current
def self.up
rename_table "things", "awesome_things"
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 607113b091..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::Current
+# 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 d4cbddab50..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::Current
+# 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 2e9f5ec6bc..85c166b319 100644
--- a/activerecord/test/migrations/to_copy2/1_create_articles.rb
+++ b/activerecord/test/migrations/to_copy2/1_create_articles.rb
@@ -1,3 +1,5 @@
+# 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 2e9f5ec6bc..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::Current
+# 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 8f81805fe1..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::Current
+# 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 607113b091..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::Current
+# 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 d4cbddab50..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::Current
+# 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 2e9f5ec6bc..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,3 +1,5 @@
+# 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 d361847d4b..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,3 +1,5 @@
+# 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 c450211d8c..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,3 +1,5 @@
+# frozen_string_literal: true
+
class ValidPeopleHaveLastNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "last_name", :string
diff --git a/activerecord/test/migrations/valid/2_we_need_reminders.rb b/activerecord/test/migrations/valid/2_we_need_reminders.rb
index d7c63ac892..4647268c6e 100644
--- a/activerecord/test/migrations/valid/2_we_need_reminders.rb
+++ b/activerecord/test/migrations/valid/2_we_need_reminders.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class WeNeedReminders < ActiveRecord::Migration::Current
def self.up
create_table("reminders") do |t|
diff --git a/activerecord/test/migrations/valid/3_innocent_jointable.rb b/activerecord/test/migrations/valid/3_innocent_jointable.rb
index 20fe183777..8063bc0558 100644
--- a/activerecord/test/migrations/valid/3_innocent_jointable.rb
+++ b/activerecord/test/migrations/valid/3_innocent_jointable.rb
@@ -1,6 +1,8 @@
+# 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 c450211d8c..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,3 +1,5 @@
+# frozen_string_literal: true
+
class ValidPeopleHaveLastNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "last_name", :string
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 d7c63ac892..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,3 +1,5 @@
+# frozen_string_literal: true
+
class WeNeedReminders < ActiveRecord::Migration::Current
def self.up
create_table("reminders") do |t|
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 20fe183777..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 @@
+# 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 9fd27593f0..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,3 +1,5 @@
+# frozen_string_literal: true
+
class ValidWithTimestampsPeopleHaveLastNames < ActiveRecord::Migration::Current
def self.up
add_column "people", "last_name", :string
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 4a59921136..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,3 +1,5 @@
+# frozen_string_literal: true
+
class ValidWithTimestampsWeNeedReminders < ActiveRecord::Migration::Current
def self.up
create_table("reminders") do |t|
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 bf934576c9..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 @@
+# 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 6f314c881c..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,3 +1,5 @@
+# frozen_string_literal: true
+
class MigrationVersionCheck < ActiveRecord::Migration::Current
def self.up
raise "incorrect migration version" unless version == 20131219224947
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 c4404a8094..4fdea46cf7 100644
--- a/activerecord/test/models/aircraft.rb
+++ b/activerecord/test/models/aircraft.rb
@@ -1,5 +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 38b983eda0..67be59a1fe 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -1,146 +1,156 @@
+# 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 :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_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"
@@ -152,6 +162,9 @@ class Author < ActiveRecord::Base
def extension_method; end
end
+ has_many :top_posts, -> { order(id: :asc) }, class_name: "Post"
+ has_many :other_top_posts, -> { order(id: :asc) }, class_name: "Post"
+
attr_accessor :post_log
after_initialize :set_post_log
@@ -205,5 +218,14 @@ end
class AuthorFavorite < ActiveRecord::Base
belongs_to :author
- belongs_to :favorite_author, :class_name => "Author"
+ belongs_to :favorite_author, class_name: "Author"
+end
+
+class AuthorFavoriteWithScope < ActiveRecord::Base
+ self.table_name = "author_favorites"
+
+ default_scope { order(id: :asc) }
+
+ belongs_to :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..20af7c6122 100644
--- a/activerecord/test/models/bird.rb
+++ b/activerecord/test/models/bird.rb
@@ -1,12 +1,24 @@
+# frozen_string_literal: true
+
class Bird < ActiveRecord::Base
belongs_to :pirate
validates_presence_of :name
accepts_nested_attributes_for :pirate
+ before_save do
+ # force materialize_transactions
+ self.class.connection.materialize_transactions
+ end
+
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
+
+ attr_accessor :total_count, :enable_count
+ after_initialize do
+ self.total_count = Bird.count if enable_count
+ end
end
diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb
index e43e5c3901..afdda1a81e 100644
--- a/activerecord/test/models/book.rb
+++ b/activerecord/test/models/book.rb
@@ -1,20 +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], _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 cover: { hard: 'hard', soft: 'soft' }
+ 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 dc0296305a..ab92f7025d 100644
--- a/activerecord/test/models/bulb.rb
+++ b/activerecord/test/models/bulb.rb
@@ -1,6 +1,8 @@
+# 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
@@ -35,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
@@ -50,9 +52,3 @@ class FailedBulb < Bulb
throw(:abort)
end
end
-
-class TrickyBulb < Bulb
- after_create do |record|
- record.car.bulbs.to_a
- 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 0f37e9a289..8614926626 100644
--- a/activerecord/test/models/car.rb
+++ b/activerecord/test/models/car.rb
@@ -1,29 +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
+ 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
index 230be118c3..995a9d3bef 100644
--- a/activerecord/test/models/carrier.rb
+++ b/activerecord/test/models/carrier.rb
@@ -1,2 +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
index dfdde18641..43013964b6 100644
--- a/activerecord/test/models/cat.rb
+++ b/activerecord/test/models/cat.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Cat < ActiveRecord::Base
self.abstract_class = true
diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb
index 4cd67c970a..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, counter_cache: true
- belongs_to :named_category, :class_name => 'Category', :foreign_key => :named_category_name, :primary_key => :name
+ 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..8c86879dc6 100644
--- a/activerecord/test/models/category.rb
+++ b/activerecord/test/models/category.rb
@@ -1,34 +1,46 @@
+# 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 :ordered_post_comments, -> { order(id: :desc) }, 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 9d3dd01016..ff528644bc 100644
--- a/activerecord/test/models/chef.rb
+++ b/activerecord/test/models/chef.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Chef < ActiveRecord::Base
belongs_to :employable, polymorphic: true
has_many :recipes
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..13e72e9c50 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" }).unscope(:limit) }
+
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 dcc5c5a310..f0f0576709 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -1,26 +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
+ 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 }
+
+ 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)
@@ -38,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
@@ -50,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 1dcd9fc21e..a0f48d23f1 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,13 +9,15 @@ 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 :developers, through: :contracts
+
+ attribute :metadata, :json
scope :of_first_firm, lambda {
- joins(:account => :firm).
- where('firms.id' => 1)
+ joins(account: :firm).
+ where("firms.id" => 1)
}
def arbitrary_method
@@ -22,12 +26,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
@@ -35,7 +39,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
@@ -45,45 +49,47 @@ 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 :association_with_references, -> { references(:foo) }, :class_name => 'Client'
+ has_many :developers_with_select, -> { select("id, name, first_name") }, class_name: "Developer"
has_one :lead_developer, class_name: "Developer"
has_many :projects
@@ -103,32 +109,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
@@ -141,6 +153,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
@@ -151,7 +178,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|
@@ -170,20 +197,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
@@ -192,39 +212,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 9f2f69e1ee..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.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
index 140e1dfc78..14bbee53d8 100644
--- a/activerecord/test/models/content.rb
+++ b/activerecord/test/models/content.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
class Content < ActiveRecord::Base
- self.table_name = 'content'
+ self.table_name = "content"
has_one :content_position, dependent: :destroy
def self.destroyed_ids
@@ -12,8 +14,8 @@ class Content < ActiveRecord::Base
end
class ContentWhichRequiresTwoDestroyCalls < ActiveRecord::Base
- self.table_name = 'content'
- has_one :content_position, foreign_key: 'content_id', dependent: :destroy
+ self.table_name = "content"
+ has_one :content_position, foreign_key: "content_id", dependent: :destroy
after_initialize do
@destroy_count = 0
diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb
index cdf7b267b5..89719775c4 100644
--- a/activerecord/test/models/contract.rb
+++ b/activerecord/test/models/contract.rb
@@ -1,9 +1,13 @@
+# 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"
+
+ attribute :metadata, :json
- before_save :hi
+ before_save :hi, :update_metadata
after_save :bye
attr_accessor :hi_count, :bye_count
@@ -17,4 +21,12 @@ class Contract < ActiveRecord::Base
@bye_count ||= 0
@bye_count += 1
end
+
+ def update_metadata
+ self.metadata = { company_id: company_id, developer_id: developer_id }
+ end
+end
+
+class NewContract < Contract
+ 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
index 37186903ff..6cb9d5239d 100644
--- a/activerecord/test/models/customer_carrier.rb
+++ b/activerecord/test/models/customer_carrier.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class CustomerCarrier < ActiveRecord::Base
cattr_accessor :current_customer
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 9a907273f8..c6574cf6e7 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
@@ -23,35 +25,35 @@ class Developer < ActiveRecord::Base
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
@@ -59,20 +61,20 @@ class Developer < ActiveRecord::Base
belongs_to :firm
has_many :contracted_projects, class_name: "Project"
- scope :jamises, -> { where(:name => 'Jamis') }
+ scope :jamises, -> { where(name: "Jamis") }
- validates_inclusion_of :salary, :in => 50000..200000
- validates_length_of :name, :within => 3..20
+ 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'
+ define_attribute_method "last_name"
def log=(message)
- audit_logs.build :message => message
+ audit_logs.build message: message
end
after_find :track_instance_count
@@ -83,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!
@@ -102,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
@@ -166,61 +178,62 @@ 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 { }
+ 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)
@@ -228,31 +241,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
@@ -261,3 +274,23 @@ 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
+
+class DeveloperName < ActiveRecord::Type::String
+ def deserialize(value)
+ "Developer: #{value}"
+ end
+end
+
+class AttributedDeveloper < ActiveRecord::Base
+ self.table_name = "developers"
+
+ attribute :name, DeveloperName.new
+
+ self.ignored_columns += ["name"]
+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..8258408f35 100644
--- a/activerecord/test/models/drink_designer.rb
+++ b/activerecord/test/models/drink_designer.rb
@@ -1,3 +1,14 @@
+# frozen_string_literal: true
+
class DrinkDesigner < ActiveRecord::Base
has_one :chef, as: :employable
end
+
+class DrinkDesignerWithPolymorphicDependentNullifyChef < ActiveRecord::Base
+ self.table_name = "drink_designers"
+
+ has_one :chef, as: :employable, dependent: :nullify
+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 af76fea52c..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
+ 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
+ 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
index cd068ff53d..649b998665 100644
--- a/activerecord/test/models/guitar.rb
+++ b/activerecord/test/models/guitar.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Guitar < ActiveRecord::Base
has_many :tuning_pegs, index_errors: true
accepts_nested_attributes_for :tuning_pegs
diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb
index 9c90ffcff4..1a433c3cab 100644
--- a/activerecord/test/models/hotel.rb
+++ b/activerecord/test/models/hotel.rb
@@ -1,11 +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 :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 7693c6e515..6e33ac0a6d 100644
--- a/activerecord/test/models/member.rb
+++ b/activerecord/test/models/member.rb
@@ -1,35 +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 :club_category, through: :club, source: :category
+ has_one :general_club, -> { general }, through: :current_membership, source: :club
- has_many :current_memberships, -> { where :favourite => true }
- has_many :clubs, :through => :current_memberships
+ has_many :super_memberships
+ has_many :favourite_memberships, -> { where(favourite: true) }, class_name: "Membership"
+ has_many :clubs, through: :favourite_memberships
has_many :tenant_memberships
- has_many :tenant_clubs, through: :tenant_memberships, class_name: 'Club', source: :club
+ 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
@@ -37,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 157130986c..e121a849d0 100644
--- a/activerecord/test/models/member_detail.rb
+++ b/activerecord/test/models/member_detail.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
class MemberDetail < ActiveRecord::Base
belongs_to :member, inverse_of: false
belongs_to :organization
has_one :member_type, through: :member
has_one :membership, through: :member
+ has_one :admittable, through: :member, source_type: "Member"
has_many :organization_member_details, through: :organization, source: :member_details
end
diff --git a/activerecord/test/models/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 e181ba1f11..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
diff --git a/activerecord/test/models/mentor.rb b/activerecord/test/models/mentor.rb
index 11f1e4bff8..2fbb62c435 100644
--- a/activerecord/test/models/mentor.rb
+++ b/activerecord/test/models/mentor.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Mentor < ActiveRecord::Base
has_many :developers
-end \ No newline at end of file
+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/mocktail_designer.rb b/activerecord/test/models/mocktail_designer.rb
deleted file mode 100644
index 77b44651a3..0000000000
--- a/activerecord/test/models/mocktail_designer.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-class MocktailDesigner < DrinkDesigner
-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 82edc64b68..3f8728af5e 100644
--- a/activerecord/test/models/notification.rb
+++ b/activerecord/test/models/notification.rb
@@ -1,3 +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 ddc9dcaf29..3bb5316eca 100644
--- a/activerecord/test/models/parrot.rb
+++ b/activerecord/test/models/parrot.rb
@@ -1,23 +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
+ 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 a4a9c6b0d4..c3d15a571a 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -1,73 +1,79 @@
+# 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 :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 PersonWithPolymorphicDependentNullifyComments < ActiveRecord::Base
+ self.table_name = "people"
+ has_many :comments, as: :author, 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
@@ -75,11 +81,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
@@ -87,56 +93,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..8733398697 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,27 @@ 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
+
+class SpacePirate < ActiveRecord::Base
+ self.table_name = "pirates"
+
+ belongs_to :parrot
+ belongs_to :parrot_with_annotation, -> { annotate("that tells jokes") }, class_name: :Parrot, foreign_key: :parrot_id
+ has_and_belongs_to_many :parrots, foreign_key: :pirate_id
+ has_and_belongs_to_many :parrots_with_annotation, -> { annotate("that are very colorful") }, class_name: :Parrot, foreign_key: :pirate_id
+ has_one :ship, foreign_key: :pirate_id
+ has_one :ship_with_annotation, -> { annotate("that is a rocket") }, class_name: :Ship, foreign_key: :pirate_id
+ has_many :birds, foreign_key: :pirate_id
+ has_many :birds_with_annotation, -> { annotate("that are also parrots") }, class_name: :Bird, foreign_key: :pirate_id
+ has_many :treasures, as: :looter
+ has_many :treasure_estimates, through: :treasures, source: :price_estimates
+ has_many :treasure_estimates_with_annotation, -> { annotate("yarrr") }, through: :treasures, source: :price_estimates
+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 bf3079a1df..61e5f14100 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,39 @@ 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
+ belongs_to :author_with_select, -> { select(:id) }, 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 +51,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 +63,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 +77,97 @@ 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_favorites_with_scope, through: :author, class_name: "AuthorFavoriteWithScope", source: "author_favorites"
+ 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, 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 :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, 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 :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 +181,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,66 +190,84 @@ 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.inheritance_column = :disabled
- self.table_name = 'posts'
- default_scope { where(:id => 1) }
+ self.table_name = "posts"
+ default_scope { where(id: 1) }
- has_many :comments, :foreign_key => :post_id
- has_one :comment, :foreign_key => :post_id
+ has_many :comments, foreign_key: :post_id
+ has_one :comment, foreign_key: :post_id
+end
+
+class PostWithDefaultSelect < ActiveRecord::Base
+ self.table_name = "posts"
+
+ default_scope { select(:author_id) }
+end
+
+class TaggedPost < Post
+ has_many :taggings, -> { rewhere(taggable_type: "TaggedPost") }, as: :taggable
+ has_many :tags, through: :taggings
end
class PostWithDefaultInclude < ActiveRecord::Base
self.inheritance_column = :disabled
- self.table_name = 'posts'
+ 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.inheritance_column = :disabled
- self.table_name = 'posts'
+ 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.inheritance_column = :disabled
- self.table_name = 'posts'
- default_scope { where(:id => [1, 5,6]) }
+ self.table_name = "posts"
+ default_scope { where(id: [1, 5, 6]) }
+ scope :unscoped_all, -> { unscoped { all } }
+ scope :authorless, -> { unscoped { where(author_id: 0) } }
end
class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base
self.inheritance_column = :disabled
- self.table_name = 'posts'
+ self.table_name = "posts"
has_many :comments, class_name: "CommentThatAutomaticallyAltersPostBody", foreign_key: :post_id
after_save do |post|
@@ -245,7 +277,7 @@ end
class PostWithAfterCreateCallback < ActiveRecord::Base
self.inheritance_column = :disabled
- self.table_name = 'posts'
+ self.table_name = "posts"
has_many :comments, foreign_key: :post_id
after_create do |post|
@@ -255,7 +287,7 @@ end
class PostWithCommentWithDefaultScopeReferencesAssociation < ActiveRecord::Base
self.inheritance_column = :disabled
- self.table_name = 'posts'
+ 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
@@ -265,8 +297,60 @@ class SerializedPost < ActiveRecord::Base
end
class ConditionalStiPost < Post
- default_scope { where(title: 'Untitled') }
+ 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 columns_hash
+ { "name" => nil }
+ 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
index 7654eda0ef..abc23f40ff 100644
--- a/activerecord/test/models/professor.rb
+++ b/activerecord/test/models/professor.rb
@@ -1,4 +1,6 @@
-require_dependency 'models/arunit2_model'
+# frozen_string_literal: true
+
+require_dependency "models/arunit2_model"
class Professor < ARUnit2Model
has_and_belongs_to_many :courses
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index efa8246f1e..846cef625b 100644
--- a/activerecord/test/models/project.rb
+++ b/activerecord/test/models/project.rb
@@ -1,17 +1,19 @@
+# frozen_string_literal: true
+
class Project < ActiveRecord::Base
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_payed_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, :class_name => "Developer"
+ 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
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..49aa38285f 100644
--- a/activerecord/test/models/rating.rb
+++ b/activerecord/test/models/rating.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
class Rating < ActiveRecord::Base
belongs_to :comment
- has_many :taggings, :as => :taggable
+ has_many :taggings, as: :taggable
+ has_many :taggings_without_tag, -> { left_joins(:tag).where("tags.id": nil) }, as: :taggable, class_name: "Tagging"
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..82185040d6 100644
--- a/activerecord/test/models/reference.rb
+++ b/activerecord/test/models/reference.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
class Reference < ActiveRecord::Base
belongs_to :person
belongs_to :job
- has_many :agents_posts_authors, :through => :person
+ has_many :ideal_jobs, class_name: "Job", foreign_key: :ideal_reference_id
+ has_many :agents_posts_authors, through: :person
class << self; attr_accessor :make_comments; end
self.make_comments = false
@@ -17,6 +20,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..b35623a344 100644
--- a/activerecord/test/models/reply.rb
+++ b/activerecord/test/models/reply.rb
@@ -1,27 +1,37 @@
-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"
+
+ scope :ordered, -> { Reply.order(: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 +60,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 e333b964ab..7973219a79 100644
--- a/activerecord/test/models/ship.rb
+++ b/activerecord/test/models/ship.rb
@@ -1,20 +1,22 @@
+# frozen_string_literal: true
+
class Ship < ActiveRecord::Base
self.record_timestamps = false
belongs_to :pirate
- belongs_to :update_only_pirate, :class_name => 'Pirate'
+ belongs_to :update_only_pirate, class_name: "Pirate"
belongs_to :developer, dependent: :destroy
- has_many :parts, :class_name => 'ShipPart'
+ 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
@@ -33,7 +35,7 @@ class Prisoner < ActiveRecord::Base
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
index 1580e8b20c..97fb058331 100644
--- a/activerecord/test/models/shop_account.rb
+++ b/activerecord/test/models/shop_account.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class ShopAccount < ActiveRecord::Base
belongs_to :customer
belongs_to :customer_carrier
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..f87315fcd1 100644
--- a/activerecord/test/models/subscription.rb
+++ b/activerecord/test/models/subscription.rb
@@ -1,4 +1,8 @@
+# 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
+
+ validates_presence_of :subscriber_id, :book_id
end
diff --git a/activerecord/test/models/tag.rb b/activerecord/test/models/tag.rb
index b48b9a2155..c1a8890a8a 100644
--- a/activerecord/test/models/tag.rb
+++ b/activerecord/test/models/tag.rb
@@ -1,13 +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 :taggings, -> { order('taggings.id DESC') }, foreign_key: 'tag_id'
+ has_many :ordered_taggings, -> { order("taggings.id DESC") }, foreign_key: "tag_id", class_name: "Tagging"
+ has_many :tagged_posts, through: :ordered_taggings, source: "taggable", source_type: "Post"
end
diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb
index 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 176bc79dc7..77101090f2 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -1,20 +1,22 @@
+# 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_lifo, -> { where(author_name: "lifo") }
+ scope :replied, -> { where "replies_count > 0" }
- scope 'approved_as_string', -> { where(:approved => true) }
- scope :anonymous_extension, -> { all } do
+ scope "approved_as_string", -> { where(approved: true) }
+ scope :anonymous_extension, -> { } do
def one
1
end
@@ -22,7 +24,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)
@@ -33,22 +35,16 @@ class Topic < ActiveRecord::Base
end
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 :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 +65,46 @@ 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
+ def self.nested_scoping(scope)
+ scope.base
+ end
+
+ private
def default_written_on
self.written_on = Time.now unless attribute_present?("written_on")
end
def destroy_children
- self.class.where("parent_id = #{id}").delete_all
+ self.class.delete_by(parent_id: id)
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,10 +113,10 @@ class Topic < ActiveRecord::Base
def before_destroy_for_transaction; end
def after_save_for_transaction; end
def after_create_for_transaction; end
-end
-class ImportantTopic < Topic
- serialize :important, Hash
+ def change_approved_callback
+ self.approved = change_approved_before_save unless change_approved_before_save.nil?
+ end
end
class DefaultRejectedTopic < Topic
@@ -117,8 +130,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 63ff0c23ec..b51db56c37 100644
--- a/activerecord/test/models/treasure.rb
+++ b/activerecord/test/models/treasure.rb
@@ -1,11 +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
index 1252d6dc1d..6e052e1d0f 100644
--- a/activerecord/test/models/tuning_peg.rb
+++ b/activerecord/test/models/tuning_peg.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class TuningPeg < ActiveRecord::Base
belongs_to :guitar
validates_numericality_of :pitch
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
index 2353e40213..41f68c4c18 100644
--- a/activerecord/test/models/uuid_item.rb
+++ b/activerecord/test/models/uuid_item.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class UuidItem < ActiveRecord::Base
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
index ef26170f1f..c9b3338522 100644
--- a/activerecord/test/models/vehicle.rb
+++ b/activerecord/test/models/vehicle.rb
@@ -1,7 +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 \ No newline at end of file
+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 5a49b38457..b143035213 100644
--- a/activerecord/test/schema/mysql2_specific_schema.rb
+++ b/activerecord/test/schema/mysql2_specific_schema.rb
@@ -1,14 +1,33 @@
-ActiveRecord::Schema.define do
+# frozen_string_literal: true
- if ActiveRecord::Base.connection.version >= '5.6.0'
+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 :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.tinyblob :tiny_blob
t.blob :normal_blob
t.mediumblob :medium_blob
@@ -18,51 +37,50 @@ ActiveRecord::Schema.define do
t.mediumtext :medium_text
t.longtext :long_text
+ t.binary :tiny_blob_2, size: :tiny
+ t.binary :medium_blob_2, size: :medium
+ t.binary :long_blob_2, size: :long
+ t.text :tiny_text_2, size: :tiny
+ t.text :medium_text_2, size: :medium
+ t.text :long_text_2, size: :long
+
t.index :var_binary
end
- 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'
+ 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
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
-DROP PROCEDURE IF EXISTS ten;
-SQL
-
- ActiveRecord::Base.connection.execute <<-SQL
-CREATE PROCEDURE ten() SQL SECURITY INVOKER
-BEGIN
- select 10;
-END
-SQL
+ create_table :enum_tests, id: false, force: true do |t|
+ t.column :enum_column, "ENUM('text','blob','tiny','medium','long','unsigned','bigint')"
+ end
- ActiveRecord::Base.connection.execute <<-SQL
-DROP PROCEDURE IF EXISTS topics;
-SQL
+ execute "DROP PROCEDURE IF EXISTS ten"
- ActiveRecord::Base.connection.execute <<-SQL
-CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER
-BEGIN
- select * from topics limit num;
-END
-SQL
+ execute <<~SQL
+ CREATE PROCEDURE ten() SQL SECURITY INVOKER
+ BEGIN
+ SELECT 10;
+ END
+ SQL
- ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true
+ execute "DROP PROCEDURE IF EXISTS topics"
- ActiveRecord::Base.connection.execute <<-SQL
-CREATE TABLE enum_tests (
- enum_column ENUM('text','blob','tiny','medium','long','unsigned','bigint')
-)
-SQL
+ execute <<~SQL
+ CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER
+ BEGIN
+ SELECT * FROM topics LIMIT num;
+ END
+ SQL
end
diff --git a/activerecord/test/schema/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb
index 264d9b8910..08c6e24555 100644
--- a/activerecord/test/schema/oracle_specific_schema.rb
+++ b/activerecord/test/schema/oracle_specific_schema.rb
@@ -1,28 +1,27 @@
-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
execute "drop table defaults" rescue nil
execute "drop sequence defaults_seq" rescue nil
- execute <<-SQL
-create table test_oracle_defaults (
- id integer not null primary key,
- test_char char(1) default 'X' not null,
- test_string varchar2(20) default 'hello' not null,
- test_int integer default 3 not null
-)
+ execute <<~SQL
+ create table test_oracle_defaults (
+ id integer not null primary key,
+ test_char char(1) default 'X' not null,
+ test_string varchar2(20) default 'hello' not null,
+ test_int integer default 3 not null
+ )
SQL
- execute <<-SQL
-create sequence test_oracle_defaults_seq minvalue 10000
- SQL
+ execute "create sequence test_oracle_defaults_seq minvalue 10000"
execute "create sequence companies_nonstd_seq minvalue 10000"
- execute <<-SQL
- CREATE TABLE defaults (
+ execute <<~SQL
+ CREATE TABLE defaults (
id integer not null,
modified_date date default sysdate,
modified_date_function date default sysdate,
@@ -33,8 +32,7 @@ create sequence test_oracle_defaults_seq minvalue 10000
char1 varchar2(1) default 'Y',
char2 varchar2(50) default 'a varchar field',
char3 clob default 'a text field'
- )
+ )
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 24713f722a..975824ed51 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/activerecord/test/schema/postgresql_specific_schema.rb
@@ -1,64 +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
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: '--- []
-
-'
+ 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
- %w(postgresql_times postgresql_oids postgresql_timestamp_with_zones
- postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name|
- drop_table table_name, if_exists: true
+ create_table :postgresql_oids, force: true do |t|
+ t.oid :obj_id
end
- execute 'DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE'
- execute 'CREATE SEQUENCE companies_nonstd_seq START 101 OWNED BY companies.id'
+ 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 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
@@ -88,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
@@ -108,7 +105,7 @@ _SQL
end
create_table :uuid_items, force: true, id: false do |t|
- t.uuid :uuid, primary_key: true
+ 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 2bcdc8729e..ead4de2a13 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
ActiveRecord::Schema.define do
# ------------------------------------------------------------------- #
# #
@@ -6,8 +8,15 @@ ActiveRecord::Schema.define do
# #
# ------------------------------------------------------------------- #
+ case_sensitive_options =
+ if current_adapter?(:Mysql2Adapter)
+ { collation: "utf8mb4_bin" }
+ else
+ {}
+ end
+
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
@@ -19,9 +28,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
@@ -31,6 +43,7 @@ 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|
@@ -54,8 +67,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
@@ -87,18 +100,24 @@ 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|
+ default_zero = { default: 0 }
+ t.references :author
t.string :format
t.column :name, :string
- t.column :status, :integer, default: 0
- t.column :read_status, :integer, default: 0
+ t.column :status, :integer, **default_zero
+ t.column :read_status, :integer, **default_zero
t.column :nullable_status, :integer
- t.column :language, :integer, default: 0
- t.column :author_visibility, :integer, default: 0
- t.column :illustrator_visibility, :integer, default: 0
- t.column :font_size, :integer, default: 0
- t.column :cover, :string, default: 'hard'
+ t.column :language, :integer, **default_zero
+ t.column :author_visibility, :integer, **default_zero
+ t.column :illustrator_visibility, :integer, **default_zero
+ t.column :font_size, :integer, **default_zero
+ t.column :difficulty, :integer, **default_zero
+ t.column :cover, :string, default: "hard"
+ t.string :isbn
+ t.datetime :published_on
+ t.index [:author_id, :name], unique: true
+ t.index :isbn, where: "published_on IS NOT NULL", unique: true
end
create_table :booleans, force: true do |t|
@@ -106,7 +125,7 @@ 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, default: false
@@ -120,11 +139,15 @@ 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|
@@ -147,8 +170,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|
@@ -185,20 +209,26 @@ 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 [:firm_id, :type, :rating], name: "company_index"
- t.index [:firm_id, :type], name: "company_partial_index", where: "rating > 10"
- t.index :name, name: 'company_name_index', using: :btree
+ 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
create_table :content, force: true do |t|
@@ -227,8 +257,9 @@ 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
+ t.string :metadata
end
create_table :customers, force: true do |t|
@@ -246,7 +277,7 @@ ActiveRecord::Schema.define do
end
create_table :dashboards, force: true, id: false do |t|
- t.string :dashboard_id
+ t.string :dashboard_id, **case_sensitive_options
t.string :name
end
@@ -254,7 +285,7 @@ ActiveRecord::Schema.define do
t.string :name
t.string :first_name
t.integer :salary, default: 70000
- t.integer :firm_id
+ t.references :firm, index: false
t.integer :mentor_id
if subsecond_precision_supported?
t.datetime :created_at, precision: 6
@@ -297,11 +328,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'
+ t.index [:source_id, :sink_id], unique: true, name: "unique_edge_index"
end
create_table :engines, force: true do |t|
- t.integer :car_id
+ t.references :car, index: false
end
create_table :entrants, force: true do |t|
@@ -310,7 +341,7 @@ ActiveRecord::Schema.define do
end
create_table :essays, force: true do |t|
- t.string :name
+ t.string :name, **case_sensitive_options
t.string :writer_id
t.string :writer_type
t.string :category_id
@@ -318,12 +349,25 @@ ActiveRecord::Schema.define do
end
create_table :events, force: true do |t|
- t.string :title, limit: 5
+ t.string :title, limit: 5, **case_sensitive_options
end
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
@@ -347,7 +391,7 @@ ActiveRecord::Schema.define do
end
create_table :guids, force: true do |t|
- t.column :key, :string
+ t.column :key, :string, **case_sensitive_options
end
create_table :guitars, force: true do |t|
@@ -355,8 +399,8 @@ ActiveRecord::Schema.define do
end
create_table :inept_wizards, force: true do |t|
- t.column :name, :string, null: false
- t.column :city, :string, null: false
+ t.column :name, :string, null: false, **case_sensitive_options
+ t.column :city, :string, null: false, **case_sensitive_options
t.column :type, :string
end
@@ -389,11 +433,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
@@ -408,6 +460,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|
@@ -421,11 +481,15 @@ ActiveRecord::Schema.define do
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|
@@ -439,7 +503,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|
@@ -457,7 +522,7 @@ 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|
@@ -506,7 +571,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
@@ -549,32 +614,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
- if subsecond_precision_supported?
- t.column :created_at, :datetime, precision: 0
- t.column :created_on, :datetime, precision: 0
- t.column :updated_at, :datetime, precision: 0
- t.column :updated_on, :datetime, precision: 0
- else
- t.column :created_at, :datetime
- t.column :created_on, :datetime
- t.column :updated_at, :datetime
- t.column :updated_on, :datetime
+ 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
- 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|
@@ -607,29 +695,18 @@ ActiveRecord::Schema.define do
create_table :pets, primary_key: :pet_id, force: true do |t|
t.string :name
t.integer :owner_id, :integer
- if subsecond_precision_supported?
- t.timestamps null: false, precision: 6
- else
- t.timestamps null: false
- end
+ t.timestamps
end
- create_table :pirates, force: true do |t|
- t.column :catchphrase, :string
- t.column :parrot_id, :integer
- t.integer :non_validated_parrot_id
- if subsecond_precision_supported?
- t.column :created_on, :datetime, precision: 6
- t.column :updated_on, :datetime, precision: 6
- else
- t.column :created_on, :datetime
- t.column :updated_on, :datetime
- end
+ create_table :pets_treasures, force: true do |t|
+ t.column :treasure_id, :integer
+ t.column :pet_id, :integer
+ t.column :rainbow_color, :string
end
create_table :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)
@@ -642,6 +719,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
@@ -675,7 +753,7 @@ ActiveRecord::Schema.define do
create_table :projects, force: true do |t|
t.string :name
t.string :type
- t.integer :firm_id
+ t.references :firm, index: false
t.integer :mentor_id
end
@@ -747,6 +825,10 @@ ActiveRecord::Schema.define do
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
@@ -760,26 +842,23 @@ 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
@@ -799,6 +878,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|
@@ -807,8 +887,8 @@ ActiveRecord::Schema.define do
end
create_table :topics, force: true do |t|
- t.string :title, limit: 250
- t.string :author_name
+ t.string :title, limit: 250, **case_sensitive_options
+ t.string :author_name, **case_sensitive_options
t.string :author_email_address
if subsecond_precision_supported?
t.datetime :written_on, precision: 6
@@ -820,10 +900,10 @@ ActiveRecord::Schema.define do
# 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)
- t.string :content, limit: 4000
+ t.string :content, limit: 4000, **case_sensitive_options
t.string :important, limit: 4000
else
- t.text :content
+ t.text :content, **case_sensitive_options
t.text :important
end
t.boolean :approved, default: true
@@ -833,11 +913,7 @@ ActiveRecord::Schema.define do
t.string :parent_title
t.string :type
t.string :group
- if subsecond_precision_supported?
- t.timestamps null: true, precision: 6
- else
- t.timestamps null: true
- end
+ t.timestamps null: true
end
create_table :toys, primary_key: :toy_id, force: true do |t|
@@ -854,14 +930,6 @@ 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
- end
-
create_table :tuning_pegs, force: true do |t|
t.integer :guitar_id
t.float :pitch
@@ -880,7 +948,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
@@ -888,7 +956,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
@@ -902,6 +969,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|
@@ -912,22 +980,25 @@ 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, primary_key: [:country_id, :treaty_id] do |t|
t.string :country_id, null: false
t.string :treaty_id, null: false
@@ -945,9 +1016,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|
@@ -985,24 +1056,21 @@ ActiveRecord::Schema.define do
create_table :records, force: true do |t|
end
- if supports_foreign_keys?
- # fk_test_has_fk should be before fk_test_has_pk
- create_table :fk_test_has_fk, force: true do |t|
- t.integer :fk_id, null: false
+ disable_referential_integrity do
+ create_table :fk_test_has_pk, primary_key: "pk_id", force: :cascade do |t|
end
- create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t|
+ create_table :fk_test_has_fk, force: true do |t|
+ t.references :fk, null: false
+ t.foreign_key :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id"
end
-
- add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id"
- 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|
@@ -1013,6 +1081,10 @@ ActiveRecord::Schema.define do
create_table :test_with_keyword_column_name, force: true do |t|
t.string :desc
end
+
+ create_table :non_primary_keys, force: true, id: false do |t|
+ t.integer :id
+ end
end
Course.connection.create_table :courses, force: true do |t|
@@ -1032,3 +1104,5 @@ Professor.connection.create_table :courses_professors, id: false, force: true do
t.references :course
t.references :professor
end
+
+OtherDog.connection.create_table :dogs, force: true
diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb
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..de0d90a18f 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
@@ -11,33 +13,34 @@ module ARTest
private
- def config_file
- 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
+ def config_file
+ Pathname.new(ENV["ARCONFIG"] || TEST_ROOT + "/config.yml")
end
- erb = Erubis::Eruby.new(config_file.read)
- expand_config(YAML.parse(erb.result(binding)).transform)
- end
+ def read_config
+ unless config_file.exist?
+ FileUtils.cp TEST_ROOT + "/config.example.yml", config_file
+ end
- def expand_config(config)
- config['connections'].each do |adapter, connection|
- dbs = [['arunit', 'activerecord_unittest'], ['arunit2', 'activerecord_unittest2']]
- dbs.each do |name, dbname|
- unless connection[name].is_a?(Hash)
- connection[name] = { 'database' => connection[name] }
- end
+ erb = ERB.new(config_file.read)
+ expand_config(YAML.parse(erb.result(binding)).transform)
+ end
- connection[name]['database'] ||= dbname
- connection[name]['adapter'] ||= adapter
+ def expand_config(config)
+ 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] }
+ end
+
+ connection[name]["database"] ||= dbname
+ connection[name]["adapter"] ||= adapter
+ end
end
- end
- config
- end
+ config
+ end
end
end
diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb
index c5334e8596..367309dd85 100644
--- a/activerecord/test/support/connection.rb
+++ b/activerecord/test/support/connection.rb
@@ -1,20 +1,27 @@
-require 'active_support/logger'
-require 'models/college'
-require 'models/course'
-require 'models/professor'
+# 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
puts "Using #{connection_name}"
ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024)
+ ActiveRecord::Base.connection_handlers = { ActiveRecord::Base.writing_role => ActiveRecord::Base.default_connection_handler }
ActiveRecord::Base.configurations = connection_config
ActiveRecord::Base.establish_connection :arunit
ARUnit2Model.establish_connection :arunit2
diff --git a/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 666c1b6a14..777e6a7c1b 100644
--- a/activerecord/test/support/schema_dumping_helper.rb
+++ b/activerecord/test/support/schema_dumping_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module SchemaDumpingHelper
def dump_table_schema(table, connection = ActiveRecord::Base.connection)
old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
diff --git a/activerecord/test/support/stubs/strong_parameters.rb b/activerecord/test/support/stubs/strong_parameters.rb
new file mode 100644
index 0000000000..da8f9892f9
--- /dev/null
+++ b/activerecord/test/support/stubs/strong_parameters.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/hash/indifferent_access"
+
+class ProtectedParams
+ delegate :keys, :key?, :has_key?, :empty?, to: :@parameters
+
+ def initialize(parameters = {})
+ @parameters = parameters.with_indifferent_access
+ @permitted = false
+ end
+
+ def permitted?
+ @permitted
+ end
+
+ def permit!
+ @permitted = true
+ self
+ end
+
+ def [](key)
+ @parameters[key]
+ end
+
+ def to_h
+ @parameters.to_h
+ end
+ alias to_unsafe_h to_h
+
+ def stringify_keys
+ dup
+ end
+
+ def dup
+ super.tap do |duplicate|
+ duplicate.instance_variable_set :@permitted, @permitted
+ end
+ end
+end
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..54fc949172
--- /dev/null
+++ b/activestorage/CHANGELOG.md
@@ -0,0 +1,175 @@
+## Rails 6.0.0.beta3 (March 11, 2019) ##
+
+* No changes.
+
+
+## Rails 6.0.0.beta2 (February 25, 2019) ##
+
+* No changes.
+
+
+## Rails 6.0.0.beta1 (January 18, 2019) ##
+
+* [Rename npm package](https://github.com/rails/rails/pull/34905) from
+ [`activestorage`](https://www.npmjs.com/package/activestorage) to
+ [`@rails/activestorage`](https://www.npmjs.com/package/@rails/activestorage).
+
+ *Javan Makhmali*
+
+* Replace `config.active_storage.queue` with two options that indicate which
+ queues analysis and purge jobs should use, respectively:
+
+ * `config.active_storage.queues.analysis`
+ * `config.active_storage.queues.purge`
+
+ `config.active_storage.queue` is preferred over the new options when it's
+ set, but it is deprecated and will be removed in Rails 6.1.
+
+ *George Claghorn*
+
+* Permit generating variants of TIFF images.
+
+ *Luciano Sousa*
+
+* Use base36 (all lowercase) for all new Blob keys to prevent
+ collisions and undefined behavior with case-insensitive filesystems and
+ database indices.
+
+ *Julik Tarkhanov*
+
+* It doesn’t include an `X-CSRF-Token` header if a meta tag is not found on
+ the page. It previously included one with a value of `undefined`.
+
+ *Cameron Bothner*
+
+* Fix `ArgumentError` when uploading to amazon s3
+
+ *Hiroki Sanpei*
+
+* Add progressive JPG to default list of variable content types
+
+ *Maurice Kühlborn*
+
+* 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.5.0 or newer.
+
+ *Jeremy Daer*, *Kasper Timm Hansen*
+
+
+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..771376cc7e
--- /dev/null
+++ b/activestorage/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2017-2019 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..7a437d4014
--- /dev/null
+++ b/activestorage/README.md
@@ -0,0 +1,162 @@
+# 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](https://www.rubydoc.info/gems/ruby-vips/Vips/Image) supported transformation.
+
+You can read more about Active Storage in the [Active Storage Overview](https://edgeguides.rubyonrails.org/active_storage_overview.html) guide.
+
+## 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_limit: [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
+ require("@rails/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:
+
+* https://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..0b246564bc
--- /dev/null
+++ b/activestorage/Rakefile
@@ -0,0 +1,25 @@
+# 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
+
+if ENV["encrypted_0fb9444d0374_key"] && ENV["encrypted_0fb9444d0374_iv"]
+ file "test/service/configurations.yml" do
+ system "openssl aes-256-cbc -K $encrypted_0fb9444d0374_key -iv $encrypted_0fb9444d0374_iv -in test/service/configurations.yml.enc -out test/service/configurations.yml -d"
+ end
+
+ task test: "test/service/configurations.yml"
+end
+
+task :package
+
+task default: :test
diff --git a/activestorage/activestorage.gemspec b/activestorage/activestorage.gemspec
new file mode 100644
index 0000000000..0ae2dcdd3e
--- /dev/null
+++ b/activestorage/activestorage.gemspec
@@ -0,0 +1,36 @@
+# 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.5.0"
+
+ s.license = "MIT"
+
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "https://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 "activejob", 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..e2bcb520b9
--- /dev/null
+++ b/activestorage/app/assets/javascripts/activestorage.js
@@ -0,0 +1,942 @@
+(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");
+ var csrfToken = getMetaValue("csrf-token");
+ if (csrfToken != undefined) {
+ this.xhr.setRequestHeader("X-CSRF-Token", csrfToken);
+ }
+ 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.tagName == "BUTTON") && 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], button[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..df3116afd7
--- /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[:key]), content_type: key[:content_type], disposition: key[: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..7fbe315f76
--- /dev/null
+++ b/activestorage/app/javascript/activestorage/blob_record.js
@@ -0,0 +1,73 @@
+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")
+
+ const csrfToken = getMetaValue("csrf-token")
+ if (csrfToken != undefined) {
+ this.xhr.setRequestHeader("X-CSRF-Token", csrfToken)
+ }
+
+ 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..98fcba60fa
--- /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.tagName == "BUTTON") && 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], button[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..35d043d508
--- /dev/null
+++ b/activestorage/app/jobs/active_storage/analyze_job.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later.
+class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob
+ queue_as { ActiveStorage.queues[:analysis] }
+
+ 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..7bc2064dc5
--- /dev/null
+++ b/activestorage/app/jobs/active_storage/base_job.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+class ActiveStorage::BaseJob < ActiveJob::Base
+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..5ceb222005
--- /dev/null
+++ b/activestorage/app/jobs/active_storage/purge_job.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# Provides asynchronous purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later.
+class ActiveStorage::PurgeJob < ActiveStorage::BaseJob
+ queue_as { ActiveStorage.queues[:purge] }
+
+ 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..874ba80ca8
--- /dev/null
+++ b/activestorage/app/models/active_storage/attachment.rb
@@ -0,0 +1,50 @@
+# 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
+
+ActiveSupport.run_load_hooks :active_storage_attachment, ActiveStorage::Attachment
diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb
new file mode 100644
index 0000000000..c9fbafad1f
--- /dev/null
+++ b/activestorage/app/models/active_storage/blob.rb
@@ -0,0 +1,278 @@
+# 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 use 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
+
+ # To prevent problems with case-insensitive filesystems, especially in combination
+ # with databases which treat indices as case-sensitive, all blob keys generated are going
+ # to only contain the base-36 character alphabet and will therefore be lowercase. To maintain
+ # the same or higher amount of entropy as in the base-58 encoding used by `has_secure_token`
+ # the number of bytes used is increased to 28 from the standard 24
+ def generate_unique_secure_token
+ SecureRandom.base36(28)
+ 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 the
+ # secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
+ # 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_for_service_url,
+ disposition: forced_disposition_for_service_url || 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, **service_metadata
+ 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 +tmpdir:+ to create it in a different directory:
+ #
+ # blob.open(tmpdir: "/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(tmpdir: nil, &block)
+ service.open key, checksum: checksum,
+ name: [ "ActiveStorage-#{id}-", filename.extension_with_delimiter ], tmpdir: tmpdir, &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
+
+ def allowed_inline?
+ ActiveStorage.content_types_allowed_inline.include?(content_type)
+ end
+
+ def content_type_for_service_url
+ forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
+ end
+
+ def forced_disposition_for_service_url
+ if forcibly_serve_as_binary? || !allowed_inline?
+ :attachment
+ end
+ end
+
+ def service_metadata
+ if forcibly_serve_as_binary?
+ { content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename }
+ elsif !allowed_inline?
+ { content_type: content_type, disposition: :attachment, filename: filename }
+ else
+ { content_type: content_type }
+ end
+ end
+end
+
+ActiveSupport.run_load_hooks :active_storage_blob, ActiveStorage::Blob
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..924bd06131
--- /dev/null
+++ b/activestorage/app/models/active_storage/blob/identifiable.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module ActiveStorage::Blob::Identifiable
+ def identify
+ unless identified?
+ update! content_type: identify_content_type, identified: true
+ update_service_metadata
+ end
+ 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
+
+ def update_service_metadata
+ service.update_metadata key, service_metadata if service_metadata.any?
+ 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..32e8fcefdf
--- /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_limit: [100, 100]).processed.service_url
+ #
+ # This will create and process a variant of the avatar blob that's constrained to a height and width of 100px.
+ # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
+ #
+ # 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_limit: [100, 100]) %>
+ #
+ # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
+ # can then produce on-demand.
+ #
+ # 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_limit: [100, 100]).processed.service_url
+ #
+ # Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand.
+ # Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s
+ # how to use the built-in version:
+ #
+ # <%= image_tag video.preview(resize_to_limit: [100, 100]) %>
+ #
+ # This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine
+ # whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?.
+ 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_limit: [100, 100]).processed.service_url
+ #
+ # Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call
+ # ActiveStorage::Blob#representable? to determine whether a blob is representable.
+ #
+ # 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..bb9d960443
--- /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_limit: [100, 100]).processed.service_url
+ #
+ # Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview
+ # image is stored with the blob, it is only generated once.
+ 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..bc0058967a
--- /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_limit: [100, 100]) %>
+#
+# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
+# can then produce on-demand.
+#
+# 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_limit: [100, 100]).processed.service_url
+#
+# This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
+# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
+#
+# You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the
+# ImageProcessing gem (such as +resize_to_limit+):
+#
+# avatar.variant(resize_to_limit: [800, 800], monochrome: true, rotate: "-90")
+#
+# Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
+#
+# * {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..41b5a45f53
--- /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_limit: [100, 100], monochrome: true, trim: true, rotate: "-90")
+#
+# The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
+class ActiveStorage::Variation
+ 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.squish
+ 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/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb b/activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb
new file mode 100644
index 0000000000..5472e3c87b
--- /dev/null
+++ b/activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb
@@ -0,0 +1,9 @@
+class AddForeignKeyConstraintToActiveStorageAttachmentsForBlobId < ActiveRecord::Migration[6.0]
+ def up
+ return if foreign_key_exists?(:active_storage_attachments, column: :blob_id)
+
+ if table_exists?(:active_storage_blobs)
+ add_foreign_key :active_storage_attachments, :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..5c5da551ae
--- /dev/null
+++ b/activestorage/lib/active_storage.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+#--
+# Copyright (c) 2017-2019 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_support/core_ext/numeric/time"
+
+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 :queues, default: {}
+ 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 :content_types_allowed_inline, default: []
+ mattr_accessor :binary_content_type, default: "application/octet-stream"
+ 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..26414ffbc2
--- /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 tmpdir: tmpdir, &block
+ end
+
+ def logger #:doc:
+ ActiveStorage.logger
+ end
+
+ def tmpdir #: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..e56c97f4f5
--- /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::Analyzer::VideoAnalyzer.new(blob).metadata
+ # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] }
+ #
+ # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
+ #
+ # 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..89cccfb58a
--- /dev/null
+++ b/activestorage/lib/active_storage/attached/changes/create_one.rb
@@ -0,0 +1,69 @@
+# 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)
+ record.public_send("#{name}_blob=", blob)
+ 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..4d7e832af5
--- /dev/null
+++ b/activestorage/lib/active_storage/downloader.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Downloader #:nodoc:
+ attr_reader :service
+
+ def initialize(service)
+ @service = service
+ end
+
+ def open(key, checksum:, name: "ActiveStorage-", tmpdir: nil)
+ open_tempfile(name, tmpdir) do |file|
+ download key, file
+ verify_integrity_of file, checksum: checksum
+ yield file
+ end
+ end
+
+ private
+ def open_tempfile(name, tmpdir = nil)
+ file = Tempfile.open(name, tmpdir)
+
+ begin
+ yield file
+ ensure
+ file.close!
+ end
+ end
+
+ def download(key, file)
+ file.binmode
+ service.download(key) { |chunk| file.write(chunk) }
+ file.flush
+ file.rewind
+ end
+
+ def verify_integrity_of(file, checksum:)
+ unless Digest::MD5.file(file).base64digest == 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..fc75a8f816
--- /dev/null
+++ b/activestorage/lib/active_storage/engine.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require "rails"
+require "action_controller/railtie"
+require "active_job/railtie"
+require "active_record/railtie"
+
+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.queues = ActiveSupport::OrderedOptions.new
+
+ config.active_storage.variable_content_types = %w(
+ image/png
+ image/gif
+ image/jpg
+ image/jpeg
+ image/pjpeg
+ image/tiff
+ 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
+ application/mathml+xml
+ text/cache-manifest
+ )
+
+ config.active_storage.content_types_allowed_inline = %w(
+ image/png
+ image/gif
+ image/jpg
+ image/jpeg
+ image/tiff
+ image/vnd.adobe.photoshop
+ image/vnd.microsoft.icon
+ application/pdf
+ )
+
+ 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.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
+ ActiveStorage.content_types_allowed_inline = app.config.active_storage.content_types_allowed_inline || []
+ ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
+ 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.queues" do
+ config.after_initialize do |app|
+ if queue = app.config.active_storage.queue
+ ActiveSupport::Deprecation.warn \
+ "config.active_storage.queue is deprecated and will be removed in Rails 6.1. " \
+ "Set config.active_storage.queues.purge and config.active_storage.queues.analysis instead."
+
+ ActiveStorage.queues = { purge: queue, analysis: queue }
+ else
+ ActiveStorage.queues = app.config.active_storage.queues || {}
+ 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..d8f8577cc3
--- /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 = "beta3"
+
+ 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..af6bcadd4c
--- /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 tmpdir: tmpdir, &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 #tmpdir.
+ 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-", tmpdir)
+
+ 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 tmpdir #: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..6bf501a607
--- /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 dimensions 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..aac1e62e7f
--- /dev/null
+++ b/activestorage/lib/active_storage/service.rb
@@ -0,0 +1,141 @@
+# 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, **options)
+ raise NotImplementedError
+ end
+
+ # Update metadata for the file identified by +key+ in the service.
+ # Override in subclasses only if the service needs to store specific
+ # metadata that has to be updated upon identification.
+ def update_metadata(key, **metadata)
+ 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
+
+ def open(*args, &block)
+ ActiveStorage::Downloader.new(self).open(*args, &block)
+ 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..993cc0e5f7
--- /dev/null
+++ b/activestorage/lib/active_storage/service/azure_storage_service.rb
@@ -0,0 +1,165 @@
+# 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:, **options)
+ @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key, **options)
+ @signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
+ @blobs = client.blob_client
+ @container = container
+ 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
+ blobs.delete_blob(container, key)
+ rescue Azure::Core::Http::HTTPError => e
+ raise unless e.type == "BlobNotFound"
+ # Ignore files already deleted
+ 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..67892d43b2
--- /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
+ File.binread path_for(key)
+ rescue Errno::ENOENT
+ raise ActiveStorage::FileNotFoundError
+ end
+ end
+ end
+
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ File.open(path_for(key), "rb") do |file|
+ file.seek range.begin
+ file.read range.size
+ end
+ rescue Errno::ENOENT
+ raise ActiveStorage::FileNotFoundError
+ end
+ end
+
+ def delete(key)
+ instrument :delete, key: key do
+ File.delete path_for(key)
+ rescue Errno::ENOENT
+ # Ignore files already deleted
+ 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|
+ content_disposition = content_disposition_with(type: disposition, filename: filename)
+ verified_key_with_expiration = ActiveStorage.verifier.generate(
+ {
+ key: key,
+ disposition: content_disposition,
+ content_type: content_type
+ },
+ { expires_in: expires_in,
+ purpose: :blob_key }
+ )
+
+ generated_url = url_helpers.rails_disk_service_url(verified_key_with_expiration,
+ host: current_host,
+ disposition: content_disposition,
+ content_type: content_type,
+ filename: filename
+ )
+ 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..09abc613f3
--- /dev/null
+++ b/activestorage/lib/active_storage/service/gcs_service.rb
@@ -0,0 +1,141 @@
+# 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, content_type: nil, disposition: nil, filename: nil)
+ instrument :upload, key: key, checksum: checksum do
+ # GCS's signed URLs don't include params such as response-content-type response-content_disposition
+ # in the signature, which means an attacker can modify them and bypass our effort to force these to
+ # binary and attachment when the file's content type requires it. The only way to force them is to
+ # store them as object's metadata.
+ content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
+ bucket.create_file(io, key, md5: checksum, content_type: content_type, content_disposition: content_disposition)
+ rescue Google::Cloud::InvalidArgumentError
+ raise ActiveStorage::IntegrityError
+ 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
+ file_for(key).download.string
+ rescue Google::Cloud::NotFoundError
+ raise ActiveStorage::FileNotFoundError
+ end
+ end
+ end
+
+ def update_metadata(key, content_type:, disposition: nil, filename: nil)
+ instrument :update_metadata, key: key, content_type: content_type, disposition: disposition do
+ file_for(key).update do |file|
+ file.content_type = content_type
+ file.content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
+ end
+ end
+ end
+
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ file_for(key).download(range: range).string
+ rescue Google::Cloud::NotFoundError
+ raise ActiveStorage::FileNotFoundError
+ end
+ end
+
+ def delete(key)
+ instrument :delete, key: key do
+ file_for(key).delete
+ rescue Google::Cloud::NotFoundError
+ # Ignore files already deleted
+ end
+ end
+
+ def delete_prefixed(prefix)
+ instrument :delete_prefixed, prefix: prefix do
+ bucket.files(prefix: prefix).all do |file|
+ file.delete
+ rescue Google::Cloud::NotFoundError
+ # Ignore concurrently-deleted files
+ 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), skip_lookup: true)
+ 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..aa41df304e
--- /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, :path_for, 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, **options)
+ each_service.collect do |service|
+ service.upload key, io.tap(&:rewind), checksum: checksum, **options
+ 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..bf94f3f49e
--- /dev/null
+++ b/activestorage/lib/active_storage/service/s3_service.rb
@@ -0,0 +1,116 @@
+# 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, content_type: nil, **)
+ instrument :upload, key: key, checksum: checksum do
+ object_for(key).put(upload_options.merge(body: io, content_md5: checksum, content_type: content_type))
+ rescue Aws::S3::Errors::BadDigest
+ raise ActiveStorage::IntegrityError
+ end
+ end
+
+ def download(key, &block)
+ if block_given?
+ instrument :streaming_download, key: key do
+ stream(key, &block)
+ end
+ else
+ instrument :download, key: key do
+ object_for(key).get.body.string.force_encoding(Encoding::BINARY)
+ rescue Aws::S3::Errors::NoSuchKey
+ raise ActiveStorage::FileNotFoundError
+ end
+ end
+ end
+
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body.read.force_encoding(Encoding::BINARY)
+ rescue Aws::S3::Errors::NoSuchKey
+ raise ActiveStorage::FileNotFoundError
+ 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..506150576c
--- /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.squish
+ 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..6b0469636c
--- /dev/null
+++ b/activestorage/lib/tasks/activestorage.rake
@@ -0,0 +1,22 @@
+# 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
+
+ # desc "Copy over the migrations needed to the application upgrading"
+ task update: :environment do
+ ENV["MIGRATIONS_PATH"] = "db/update_migrate"
+
+ Rake::Task["active_storage:install"].invoke
+ end
+end
diff --git a/activestorage/package.json b/activestorage/package.json
new file mode 100644
index 0000000000..c363acebde
--- /dev/null
+++ b/activestorage/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@rails/activestorage",
+ "version": "6.0.0-beta3",
+ "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..9bf2641de6
--- /dev/null
+++ b/activestorage/test/controllers/blobs_controller_test.rb
@@ -0,0 +1,47 @@
+# 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
+
+if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].present?
+ class ActiveStorage::S3BlobsControllerTest < 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 "allow redirection to the different host" do
+ blob = create_file_blob filename: "racecar.jpg"
+
+ assert_nothing_raised { get rails_blob_url(blob) }
+ assert_response :redirect
+ assert_no_match @request.host, @response.headers["Location"]
+ ensure
+ blob.purge
+ end
+ end
+else
+ puts "Skipping S3 redirection tests because no S3 configuration was supplied"
+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..a723b4d56a
--- /dev/null
+++ b/activestorage/test/controllers/disk_controller_test.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "database/setup"
+
+class ActiveStorage::DiskControllerTest < ActionDispatch::IntegrationTest
+ test "showing blob inline" do
+ blob = create_blob(filename: "hello.jpg", content_type: "image/jpg")
+
+ get blob.service_url
+ assert_response :ok
+ assert_equal "inline; filename=\"hello.jpg\"; filename*=UTF-8''hello.jpg", response.headers["Content-Disposition"]
+ assert_equal "image/jpg", 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" do
+ blob = create_blob
+ get blob.service_url, headers: { "Range" => "bytes=5-9" }
+ assert_response :partial_content
+ 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 " worl", response.body
+ end
+
+ test "showing blob that does not exist" do
+ blob = create_blob
+ blob.delete
+
+ get blob.service_url
+ end
+
+ test "showing blob with invalid key" do
+ get rails_disk_service_url(encoded_key: "Invalid key", filename: "hello.txt")
+ 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..4ae0ff877e
--- /dev/null
+++ b/activestorage/test/controllers/representations_controller_test.rb
@@ -0,0 +1,91 @@
+# 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
+
+if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].present?
+ class ActiveStorage::S3RepresentationsControllerWithVariantsTest < 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 "allow redirection to the different host" do
+ blob = create_file_blob filename: "racecar.jpg"
+
+ assert_nothing_raised do
+ get rails_blob_representation_url(
+ filename: blob.filename,
+ signed_blob_id: blob.signed_id,
+ variation_key: ActiveStorage::Variation.encode(resize: "100x100"))
+ end
+ assert_response :redirect
+ assert_no_match @request.host, @response.headers["Location"]
+ ensure
+ blob.purge
+ end
+ end
+else
+ puts "Skipping S3 redirection tests because no S3 configuration was supplied"
+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/activestorage/test/dummy/app/assets/images/.keep b/activestorage/test/dummy/app/assets/images/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/activestorage/test/dummy/app/assets/images/.keep
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js b/activestorage/test/dummy/app/assets/javascripts/application.js
index e54c6461cc..e54c6461cc 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js
+++ b/activestorage/test/dummy/app/assets/javascripts/application.js
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/activestorage/test/dummy/app/controllers/concerns/.keep b/activestorage/test/dummy/app/controllers/concerns/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ 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/activestorage/test/dummy/app/models/concerns/.keep b/activestorage/test/dummy/app/models/concerns/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ 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..d0dd7c27ac
--- /dev/null
+++ b/activestorage/test/dummy/bin/yarn
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+VENDOR_PATH = File.expand_path("..", __dir__)
+Dir.chdir(VENDOR_PATH) do
+ 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
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..151c8ade4b
--- /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 6.0
+
+ 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..4b80d291ca
--- /dev/null
+++ b/activestorage/test/dummy/config/environments/development.rb
@@ -0,0 +1,52 @@
+# 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.action_controller.enable_fragment_cache_logging = 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.tif b/activestorage/test/fixtures/files/racecar.tif
new file mode 100644
index 0000000000..0a11b22896
--- /dev/null
+++ b/activestorage/test/fixtures/files/racecar.tif
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..e826109874
--- /dev/null
+++ b/activestorage/test/models/attached/many_test.rb
@@ -0,0 +1,599 @@
+# 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
+
+ assert_not_empty @user.highlights_attachments
+ assert_equal @user.highlights_blobs.count, 2
+ 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.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..ac08d324bb
--- /dev/null
+++ b/activestorage/test/models/attached/one_test.rb
@@ -0,0 +1,516 @@
+# 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
+
+ assert_not_nil @user.avatar_attachment
+ assert_not_nil @user.avatar_blob
+ 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.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..9fd75a1b4a
--- /dev/null
+++ b/activestorage/test/models/blob_test.rb
@@ -0,0 +1,206 @@
+# 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 "create after upload generates a 28-character base36 key" do
+ assert_match(/^[a-z0-9]{28}$/, create_blob.key)
+ 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 tmpdir" do
+ create_file_blob(filename: "racecar.jpg").open(tmpdir: tmpdir = Dir.mktmpdir) do |file|
+ assert file.binmode?
+ assert_equal 0, file.pos
+ assert_match(/\.jpg\z/, file.path)
+ assert file.path.starts_with?(tmpdir)
+ 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 content_type to binary and 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, content_type: "application/octet-stream"), blob.service_url
+ assert_equal expected_url_for(blob, disposition: :attachment, content_type: "application/octet-stream"), blob.service_url(disposition: :inline)
+ end
+ end
+
+ test "URLs force attachment as content disposition when the content type is not allowed inline" do
+ blob = create_blob(content_type: "application/zip")
+
+ freeze_time do
+ assert_equal expected_url_for(blob, disposition: :attachment, content_type: "application/zip"), blob.service_url
+ assert_equal expected_url_for(blob, disposition: :attachment, content_type: "application/zip"), 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: :attachment,
+ 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: :attachment, filename: nil, content_type: nil)
+ filename ||= blob.filename
+ content_type ||= blob.content_type
+
+ query = { disposition: ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename.sanitized), content_type: content_type }
+ key_params = { key: blob.key }.merge(query)
+
+ "https://example.com/rails/active_storage/disk/#{ActiveStorage.verifier.generate(key_params, expires_in: 5.minutes, purpose: :blob_key)}/#{filename}?#{query.to_param}"
+ 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..d98935eb9f
--- /dev/null
+++ b/activestorage/test/models/variant_test.rb
@@ -0,0 +1,180 @@
+# 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
+ 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
+
+ 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
+ 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
+
+ test "center-weighted crop of JPEG blob using :combine_options" do
+ 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
+
+ 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 "resized variation of TIFF blob" do
+ blob = create_file_blob(filename: "racecar.tif")
+ variant = blob.variant(resize: "50x50").processed
+ assert_match(/racecar\.png/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal "PNG", image.type
+ assert_equal 50, image.width
+ assert_equal 33, 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, :<, 730
+ end
+
+ test "works for vips processor" do
+ 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
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..2b07902d07
--- /dev/null
+++ b/activestorage/test/service/azure_storage_service_test.rb
@@ -0,0 +1,37 @@
+# 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
+ 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
+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..f3c4dd26bd
--- /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..6bca428f50
--- /dev/null
+++ b/activestorage/test/service/gcs_service_test.rb
@@ -0,0 +1,82 @@
+# 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
+ 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
+
+ test "upload with content_type and content_disposition" do
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+
+ @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest(data), disposition: :attachment, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain")
+
+ url = @service.url(key, expires_in: 2.minutes, disposition: :inline, content_type: "text/html", filename: ActiveStorage::Filename.new("test.html"))
+ response = Net::HTTP.get_response(URI(url))
+ assert_equal "text/plain", response.content_type
+ assert_match(/attachment;.*test.txt/, response["Content-Disposition"])
+ ensure
+ @service.delete key
+ end
+
+ test "upload with content_type" do
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+
+ @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest(data), content_type: "text/plain")
+
+ url = @service.url(key, expires_in: 2.minutes, disposition: :inline, content_type: "text/html", filename: ActiveStorage::Filename.new("test.html"))
+ response = Net::HTTP.get_response(URI(url))
+ assert_equal "text/plain", response.content_type
+ assert_match(/inline;.*test.html/, response["Content-Disposition"])
+ ensure
+ @service.delete key
+ end
+
+ test "update metadata" do
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest(data), disposition: :attachment, filename: ActiveStorage::Filename.new("test.html"), content_type: "text/html")
+
+ @service.update_metadata(key, disposition: :inline, filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain")
+ url = @service.url(key, expires_in: 2.minutes, disposition: :attachment, content_type: "text/html", filename: ActiveStorage::Filename.new("test.html"))
+
+ response = Net::HTTP.get_response(URI(url))
+ assert_equal "text/plain", response.content_type
+ assert_match(/inline;.*test.txt/, response["Content-Disposition"])
+ ensure
+ @service.delete key
+ 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
+ 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..249a5652fb
--- /dev/null
+++ b/activestorage/test/service/mirror_service_test.rb
@@ -0,0 +1,68 @@
+# 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
+ 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
+
+ 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
+
+ test "path for file in primary service" do
+ assert_equal @service.primary.path_for(@key), @service.path_for(@key)
+ 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..74c0aa0405
--- /dev/null
+++ b/activestorage/test/service/s3_service_test.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require "service/shared_service_tests"
+require "net/http"
+require "database/setup"
+
+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
+ 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
+
+ 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
+
+ test "upload with content type" do
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ content_type = "text/plain"
+
+ @service.upload(
+ key,
+ StringIO.new(data),
+ checksum: Digest::MD5.base64digest(data),
+ filename: "cool_data.txt",
+ content_type: content_type
+ )
+
+ assert_equal content_type, @service.bucket.object(key).content_type
+ ensure
+ @service.delete key
+ end
+ end
+else
+ puts "Skipping S3 Service tests because no S3 configuration was supplied"
+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..17f3736056
--- /dev/null
+++ b/activestorage/test/service/shared_service_tests.rb
@@ -0,0 +1,140 @@
+# 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
+ 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
+
+ test "uploading without integrity" do
+ 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
+
+ test "uploading with integrity and multiple keys" do
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ @service.upload(
+ key,
+ StringIO.new(data),
+ checksum: Digest::MD5.base64digest(data),
+ filename: "racecar.jpg",
+ content_type: "image/jpg"
+ )
+
+ assert_equal data, @service.download(key)
+ ensure
+ @service.delete key
+ 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
+ @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
diff --git a/activestorage/test/template/image_tag_test.rb b/activestorage/test/template/image_tag_test.rb
new file mode 100644
index 0000000000..258cf702ad
--- /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/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 32c87ecc64..db9c7e96f3 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,616 +1,432 @@
-* Match `String#to_time`'s behaviour to that of ruby's implementation for edge cases.
+* In `:zeitwerk` mode, eager load directories in engines and applications only
+ if present in their respective `config.eager_load_paths`.
- `nil` is now returned instead of the current date if the string provided does
- contain time information, but none that is used to build the `Time` object.
+ A common use case for this is adding `lib` to `config.autoload_paths`, but
+ not to `config.eager_load_paths`. In that configuration, for example, files
+ in the `lib` directory should not be eager loaded.
- Fixes #22958.
+ *Xavier Noria*
- *Siim Liiser*
-
-* Rely on the native DateTime#<=> implementation to handle non-datetime like
- objects instead of returning `nil` ourselves. This restores the ability
- of `DateTime` instances to be compared with a `Numeric` that represents an
- astronomical julian day number.
-
- Fixes #24228.
-
- *Andrew White*
-
-* Add `String#upcase_first` method.
-
- *Glauco Custódio*, *bogdanvlviv*
-
-* Prevent `Marshal.load` from looping infinitely when trying to autoload a constant
- which resolves to a different name.
-
- *Olek Janiszewski*
-
-* Deprecate `Module.local_constants`. Please use `Module.constants(false)` instead.
-
- *Yuichiro Kaneko*
-
-* Publish `ActiveSupport::Executor` and `ActiveSupport::Reloader` APIs to allow
- components and libraries to manage, and participate in, the execution of
- application code, and the application reloading process.
-
- *Matthew Draper*
-
-
-## Rails 5.0.0.beta3 (February 24, 2016) ##
-
-* Deprecate arguments on `assert_nothing_raised`.
-
- `assert_nothing_raised` does not assert the arguments that have been passed
- in (usually a specific exception class) since the method only yields the
- block. So as not to confuse the users that the arguments have meaning, they
- are being deprecated.
-
- *Tara Scherner de la Fuente*
-
-* Make `benchmark('something', silence: true)` actually work.
-
- *DHH*
-
-* Add `#on_weekday?` method to `Date`, `Time`, and `DateTime`.
-
- `#on_weekday?` returns `true` if the receiving date/time does not fall on a Saturday
- or Sunday.
-
- *Vipul A M*
-
-* Add `Array#second_to_last` and `Array#third_to_last` methods.
-
- *Brian Christian*
-
-* Fix regression in `Hash#dig` for HashWithIndifferentAccess.
-
- *Jon Moss*
+* Fix bug in Range comparisons when comparing to an excluded-end Range
+ Before:
-## Rails 5.0.0.beta2 (February 01, 2016) ##
+ (1..10).cover?(1...11) # => false
-* Change `number_to_currency` behavior for checking negativity.
+ After:
- Used `to_f.negative` instead of using `to_f.phase` for checking negativity
- of a number in number_to_currency helper.
- This change works same for all cases except when number is "-0.0".
+ (1..10).cover?(1...11) # => true
- -0.0.to_f.negative? => false
- -0.0.to_f.phase? => 3.14
+ With the same change for `Range#include?` and `Range#===`.
- This change reverts changes from https://github.com/rails/rails/pull/6512.
- But it should be acceptable as we could not find any currency which
- supports negative zeros.
+ *Owen Stephens*
- *Prathamesh Sonpatki*, *Rafael Mendonça França*
+* Use weak references in descendants tracker to allow anonymous subclasses to
+ be garbage collected.
-* Match `HashWithIndifferentAccess#default`'s behaviour with `Hash#default`.
+ *Edgars Beigarts*
- *David Cornu*
+* Update `ActiveSupport::Notifications::Instrumenter#instrument` to make
+ passing a block optional. This will let users use
+ `ActiveSupport::Notifications` messaging features outside of
+ instrumentation.
-* Adds `:exception_object` key to `ActiveSupport::Notifications::Instrumenter`
- payload when an exception is raised.
+ *Ali Ibrahim*
- Adds new key/value pair to payload when an exception is raised:
- e.g. `:exception_object => #<RuntimeError: FAIL>`.
+* Fix `Time#advance` to work with dates before 1001-03-07
- *Ryan T. Hosford*
+ Before:
-* Support extended grapheme clusters and UAX 29.
+ Time.utc(1001, 3, 6).advance(years: -1) # => 1000-03-05 00:00:00 UTC
- *Adam Roben*
+ After
-* Add petabyte and exabyte numeric conversion.
+ Time.utc(1001, 3, 6).advance(years: -1) # => 1000-03-06 00:00:00 UTC
- *Akshay Vishnoi*
+ Note that this doesn't affect `DateTime#advance` as that doesn't use a proleptic calendar.
+ *Andrew White*
-## Rails 5.0.0.beta1 (December 18, 2015) ##
+* In Zeitwerk mode, engines are now managed by the `main` autoloader. Engines may reference application constants, if the application is reloaded and we do not reload engines, they won't use the reloaded application code.
-* Add thread_m/cattr_accessor/reader/writer suite of methods for declaring class and module variables that live per-thread.
- This makes it easy to declare per-thread globals that are encapsulated. Note: This is a sharp edge. A wild proliferation
- of globals is A Bad Thing. But like other sharp tools, when it's right, it's right.
+ *Xavier Noria*
- Here's an example of a simple event tracking system where the object being tracked needs not pass a creator that it
- doesn't need itself along:
+* Add support for supplying `locale` to `transliterate` and `parameterize`.
- module Current
- thread_mattr_accessor :account
- thread_mattr_accessor :user
+ I18n.backend.store_translations(:de, i18n: { transliterate: { rule: { "ü" => "ue" } } })
- def self.reset() self.account = self.user = nil end
- end
+ ActiveSupport::Inflector.transliterate("ü", locale: :de) # => "ue"
+ "Fünf autos".parameterize(locale: :de) # => "fuenf-autos"
+ ActiveSupport::Inflector.parameterize("Fünf autos", locale: :de) # => "fuenf-autos"
- class ApplicationController < ActionController::Base
- before_action :set_current
- after_action { Current.reset }
+ *Kaan Ozkan*, *Sharang Dashputre*
- private
- def set_current
- Current.account = Account.find(params[:account_id])
- Current.user = Current.account.users.find(params[:user_id])
- end
- end
+* Allow `Array#excluding` and `Enumerable#excluding` to deal with a passed array gracefully.
- class MessagesController < ApplicationController
- def create
- @message = Message.create!(message_params)
- end
- end
-
- class Message < ApplicationRecord
- has_many :events
- after_create :track_created
-
- private
- def track_created
- events.create! origin: self, action: :create
- end
- end
-
- class Event < ApplicationRecord
- belongs_to :creator, class_name: 'User'
- before_validation { self.creator ||= Current.user }
- end
+ [ 1, 2, 3, 4, 5 ].excluding([4, 5]) # => [ 1, 2, 3 ]
*DHH*
+* Renamed `Array#without` and `Enumerable#without` to `Array#excluding` and `Enumerable#excluding`, to create parity with
+ `Array#including` and `Enumerable#including`. Retained the old names as aliases.
-* Deprecated `Module#qualified_const_` in favour of the builtin Module#const_
- methods.
+ *DHH*
- *Genadi Samokovarov*
+* Added `Array#including` and `Enumerable#including` to conveniently enlarge a collection with more members using a method rather than an operator:
-* Deprecate passing string to define callback.
+ [ 1, 2, 3 ].including(4, 5) # => [ 1, 2, 3, 4, 5 ]
+ post.authors.including(Current.person) # => All the authors plus the current person!
- *Yuichiro Kaneko*
+ *DHH*
-* `ActiveSupport::Cache::Store#namespaced_key`,
- `ActiveSupport::Cache::MemCachedStore#escape_key`, and
- `ActiveSupport::Cache::FileStore#key_file_path`
- are deprecated and replaced with `normalize_key` that now calls `super`.
- `ActiveSupport::Cache::LocaleCache#set_cache_value` is deprecated and replaced with `write_cache_value`.
+## Rails 6.0.0.beta3 (March 11, 2019) ##
- *Michael Grosser*
+* No changes.
-* Implements an evented file watcher to asynchronously detect changes in the
- application source code, routes, locales, etc.
- This watcher is disabled by default, applications my enable it in the configuration:
+## Rails 6.0.0.beta2 (February 25, 2019) ##
- # config/environments/development.rb
- config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+* New autoloading based on [Zeitwerk](https://github.com/fxn/zeitwerk).
- This feature depends on the [listen](https://github.com/guard/listen) gem:
+ *Xavier Noria*
- group :development do
- gem 'listen', '~> 3.0.5'
- end
+* Revise `ActiveSupport::Notifications.unsubscribe` to correctly handle Regex or other multiple-pattern subscribers.
- *Puneet Agarwal* and *Xavier Noria*
+ *Zach Kemp*
-* Added `Time.days_in_year` to return the number of days in the given year, or the
- current year if no argument is provided.
+* Add `before_reset` callback to `CurrentAttributes` and define `after_reset` as an alias of `resets` for symmetry.
- *Jon Pascoe*
+ *Rosa Gutierrez*
-* Updated `parameterize` to preserve the case of a string, optionally.
+* Remove the `` Kernel#` `` override that suppresses ENOENT and accidentally returns nil on Unix systems.
- Example:
+ *Akinori Musha*
- parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth"
- parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth"
+* Add `ActiveSupport::HashWithIndifferentAccess#assoc`.
- *Swaathi Kakarla*
+ `assoc` can now be called with either a string or a symbol.
-* `HashWithIndifferentAccess.new` respects the default value or proc on objects
- that respond to `#to_hash`. `.new_from_hash_copying_default` simply invokes `.new`.
- All calls to `.new_from_hash_copying_default` are replaced with `.new`.
+ *Stefan Schüßler*
- *Gordon Chan*
+* Add `Hash#deep_transform_values`, and `Hash#deep_transform_values!`.
-* Change Integer#year to return a Fixnum instead of a Float to improve
- consistency.
+ *Guillermo Iguaran*
- Integer#years returned a Float while the rest of the accompanying methods
- (days, weeks, months, etc.) return a Fixnum.
- Before:
+## Rails 6.0.0.beta1 (January 18, 2019) ##
- 1.year # => 31557600.0
+* Remove deprecated `Module#reachable?` method.
- After:
+ *Rafael Mendonça França*
- 1.year # => 31557600
+* Remove deprecated `#acronym_regex` method from `Inflections`.
- *Konstantinos Rousis*
+ *Rafael Mendonça França*
-* Handle invalid UTF-8 strings when HTML escaping.
+* Fix `String#safe_constantize` throwing a `LoadError` for incorrectly cased constant references.
- Use `ActiveSupport::Multibyte::Unicode.tidy_bytes` to handle invalid UTF-8
- strings in `ERB::Util.unwrapped_html_escape` and `ERB::Util.html_escape_once`.
- Prevents user-entered input passed from a querystring into a form field from
- causing invalid byte sequence errors.
+ *Keenan Brock*
- *Grey Baker*
+* Preserve key order passed to `ActiveSupport::CacheStore#fetch_multi`.
-* Update `ActiveSupport::Multibyte::Chars#slice!` to return `nil` if the
- arguments are out of bounds, to mirror the behavior of `String#slice!`
+ `fetch_multi(*names)` now returns its results in the same order as the `*names` requested, rather than returning cache hits followed by cache misses.
- *Gourav Tiwari*
+ *Gannon McGibbon*
-* Fix `number_to_human` so that 999999999 rounds to "1 Billion" instead of
- "1000 Million".
+* If the same block is `included` multiple times for a Concern, an exception is no longer raised.
- *Max Jacobson*
+ *Mark J. Titorenko*, *Vlad Bokov*
-* Fix `ActiveSupport::Deprecation#deprecate_methods` to report using the
- current deprecator instance, where applicable.
+* Fix bug where `#to_options` for `ActiveSupport::HashWithIndifferentAccess`
+ would not act as alias for `#symbolize_keys`.
- *Brandon Dunne*
+ *Nick Weiland*
-* `Cache#fetch` instrumentation marks whether it was a `:hit`.
+* Improve the logic that detects non-autoloaded constants.
- *Robin Clowers*
+ *Jan Habermann*, *Xavier Noria*
-* `assert_difference` and `assert_no_difference` now returns the result of the
- yielded block.
+* 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.
- Example:
+ *Francesco Rodríguez*
- post = assert_difference -> { Post.count }, 1 do
- Post.create
- end
+* Deprecate `ActiveSupport::Multibyte::Chars.consumes?` in favor of `String#is_utf8?`.
- *Lucas Mazza*
+ *Francesco Rodríguez*
-* Short-circuit `blank?` on date and time values since they are never blank.
+* 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.
- Fixes #21657.
+ *Edouard Chin*
- *Andrew White*
+* Deprecate `ActiveSupport::Multibyte::Unicode#normalize` and `ActiveSuppport::Multibyte::Chars#normalize`
+ in favor of `String#unicode_normalize`
-* Replaced deprecated `ThreadSafe::Cache` with its successor `Concurrent::Map` now that
- the thread_safe gem has been merged into concurrent-ruby.
+ *Francesco Rodríguez*
- *Jerry D'Antonio*
+* Deprecate `ActiveSupport::Multibyte::Unicode#downcase/upcase/swapcase` in favor of
+ `String#downcase/upcase/swapcase`.
-* Updated Unicode version to 8.0.0
+ *Francesco Rodríguez*
- *Anshul Sharma*
+* Add `ActiveSupport::ParameterFilter`.
-* `number_to_currency` and `number_with_delimiter` now accept custom `delimiter_pattern` option
- to handle placement of delimiter, to support currency formats like INR
+ *Yoshiyuki Kinjo*
- Example:
+* Rename `Module#parent`, `Module#parents`, and `Module#parent_name` to
+ `module_parent`, `module_parents`, and `module_parent_name`.
- number_to_currency(1230000, delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/, unit: '₹', format: "%u %n")
- # => '₹ 12,30,000.00'
+ *Gannon McGibbon*
- *Vipul A M*
+* Deprecate the use of `LoggerSilence` in favor of `ActiveSupport::LoggerSilence`
-* Deprecate `:prefix` option of `number_to_human_size` with no replacement.
+ *Edouard Chin*
- *Jean Boussier*
+* Deprecate using negative limits in `String#first` and `String#last`.
-* Fix `TimeWithZone#eql?` to properly handle `TimeWithZone` created from `DateTime`:
- twz = DateTime.now.in_time_zone
- twz.eql?(twz.dup) => true
+ *Gannon McGibbon*, *Eric Turner*
- Fixes #14178.
+* Fix bug where `#without` for `ActiveSupport::HashWithIndifferentAccess` would fail
+ with symbol arguments
- *Roque Pinel*
+ *Abraham Chan*
-* ActiveSupport::HashWithIndifferentAccess `select` and `reject` will now return
- enumerator if called without block.
+* 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.
- Fixes #20095.
+ *Janosch Müller*
- *Bernard Potocki*
+* Changed `ActiveSupport::TaggedLogging.new` to return a new logger instance instead
+ of mutating the one received as parameter.
-* Removed `ActiveSupport::Concurrency::Latch`, superseded by `Concurrent::CountDownLatch`
- from the concurrent-ruby gem.
+ *Thierry Joyal*
- *Jerry D'Antonio*
+* Define `unfreeze_time` as an alias of `travel_back` in `ActiveSupport::Testing::TimeHelpers`.
-* Fix not calling `#default` on `HashWithIndifferentAccess#to_hash` when only
- `default_proc` is set, which could raise.
+ The alias is provided for symmetry with `freeze_time`.
- *Simon Eskildsen*
+ *Ryan Davidson*
-* Fix setting `default_proc` on `HashWithIndifferentAccess#dup`.
+* Add support for tracing constant autoloads. Just throw
- *Simon Eskildsen*
+ ActiveSupport::Dependencies.logger = Rails.logger
+ ActiveSupport::Dependencies.verbose = true
-* Fix a range of values for parameters of the Time#change.
+ in an initializer.
- *Nikolay Kondratyev*
+ *Xavier Noria*
-* Add `Enumerable#pluck` to get the same values from arrays as from ActiveRecord
- associations.
+* Maintain `html_safe?` on html_safe strings when sliced.
- Fixes #20339.
+ string = "<div>test</div>".html_safe
+ string[-1..1].html_safe? # => true
- *Kevin Deisz*
+ *Elom Gomez*, *Yumin Wong*
-* Add a bang version to `ActiveSupport::OrderedOptions` get methods which will raise
- an `KeyError` if the value is `.blank?`.
+* Add `Array#extract!`.
- Before:
+ 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.
- if (slack_url = Rails.application.secrets.slack_url).present?
- # Do something worthwhile
- else
- # Raise as important secret password is not specified
- end
+ 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]
- After:
+ *bogdanvlviv*
- slack_url = Rails.application.secrets.slack_url!
+* Support not to cache `nil` for `ActiveSupport::Cache#fetch`.
- *Aditya Sanghi*, *Gaurish Sharma*
+ cache.fetch('bar', skip_nil: true) { nil }
+ cache.exist?('bar') # => false
-* Remove deprecated `Class#superclass_delegating_accessor`.
- Use `Class#class_attribute` instead.
+ *Martin Hong*
- *Akshay Vishnoi*
+* 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:
-* Patch `Delegator` to work with `#try`.
+ ActiveSupport::Notifications.subscribe('wait') do |*args|
+ @event = ActiveSupport::Notifications::Event.new(*args)
+ end
- Fixes #5790.
+ ActiveSupport::Notifications.instrument('wait') do
+ sleep 1
+ end
- *Nate Smith*
+ @event.duration # => 1000.138
-* Add `Integer#positive?` and `Integer#negative?` query methods
- in the vein of `Fixnum#zero?`.
+ 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:
- This makes it nicer to do things like `bunch_of_numbers.select(&:positive?)`.
+ ActiveSupport::Notifications.subscribe('wait') do |event|
+ @event = event
+ end
- *DHH*
+ ActiveSupport::Notifications.instrument('wait') do
+ sleep 1
+ end
-* Encoding `ActiveSupport::TimeWithZone` to YAML now preserves the timezone information.
+ p @event.allocations # => 7
+ p @event.cpu_time # => 0.256
+ p @event.idle_time # => 1003.2399
- Fixes #9183.
+ Now you can enjoy event objects without making them yourself. Neat!
- *Andrew White*
+ *Aaron "t.lo" Patterson*
-* Added `ActiveSupport::TimeZone#strptime` to allow parsing times as if
- from a given timezone.
+* Add cpu_time, idle_time, and allocations to Event.
- *Paul A Jungwirth*
+ *Eileen M. Uchitelle*, *Aaron Patterson*
-* `ActiveSupport::Callbacks#skip_callback` now raises an `ArgumentError` if
- an unrecognized callback is removed.
+* RedisCacheStore: support key expiry in increment/decrement.
- *Iain Beeston*
+ Pass `:expires_in` to `#increment` and `#decrement` to set a Redis EXPIRE on the key.
-* Added `ActiveSupport::ArrayInquirer` and `Array#inquiry`.
+ If the key is already set to expire, RedisCacheStore won't extend its expiry.
- Wrapping an array in an `ArrayInquirer` gives a friendlier way to check its
- contents:
+ Rails.cache.increment("some_key", 1, expires_in: 2.minutes)
- variants = ActiveSupport::ArrayInquirer.new([:phone, :tablet])
+ *Jason Lee*
- variants.phone? # => true
- variants.tablet? # => true
- variants.desktop? # => false
+* Allow `Range#===` and `Range#cover?` on Range.
- variants.any?(:phone, :tablet) # => true
- variants.any?(:phone, :desktop) # => true
- variants.any?(:desktop, :watch) # => false
+ `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.
- `Array#inquiry` is a shortcut for wrapping the receiving array in an
- `ArrayInquirer`.
+ *Requiring active_support/core_ext/range/include_range is now deprecated.*
+ *Use `require "active_support/core_ext/range/compare_range"` instead.*
- *George Claghorn*
+ *utilum*
-* Deprecate `alias_method_chain` in favour of `Module#prepend` introduced in
- Ruby 2.0.
+* Add `index_with` to Enumerable.
- *Kir Shatrov*
+ Allows creating a hash from an enumerable with the value from a passed block
+ or a default argument.
-* Added `#without` on `Enumerable` and `Array` to return a copy of an
- enumerable without the specified elements.
+ %i( title body ).index_with { |attr| post.public_send(attr) }
+ # => { title: "hey", body: "what's up?" }
- *Todd Bealmear*
+ %i( title body ).index_with(nil)
+ # => { title: nil, body: nil }
-* Fixed a problem where `String#truncate_words` would get stuck with a complex
- string.
+ Closely linked with `index_by`, which creates a hash where the keys are extracted from a block.
- *Henrik Nygren*
+ *Kasper Timm Hansen*
-* Fixed a roundtrip problem with `AS::SafeBuffer` where primitive-like strings
- will be dumped as primitives:
+* Fix bug where `ActiveSupport::TimeZone.all` would fail when tzinfo data for
+ any timezone defined in `ActiveSupport::TimeZone::MAPPING` is missing.
- Before:
+ *Dominik Sander*
- 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
+* Redis cache store: `delete_matched` no longer blocks the Redis server.
+ (Switches from evaled Lua to a batched SCAN + DEL loop.)
- After:
+ *Gleb Mazovetskiy*
- 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"
+* 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.
*Godfrey Chan*
-* 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*
-
-* `AS::Testing::TimeHelpers#travel_to` now changes `DateTime.now` as well as
- `Time.now` and `Date.today`.
-
- *Yuki Nishijima*
-
-* Add `file_fixture` to `ActiveSupport::TestCase`.
- It provides a simple mechanism to access sample files in your test cases.
-
- By default file fixtures are stored in `test/fixtures/files`. This can be
- configured per test-case using the `file_fixture_path` class attribute.
-
- *Yves Senn*
+* Fix bug where `URI.unescape` would fail with mixed Unicode/escaped character input:
-* Return value of yielded block in `File.atomic_write`.
+ URI.unescape("\xe3\x83\x90") # => "ãƒ"
+ URI.unescape("%E3%83%90") # => "ãƒ"
+ URI.unescape("\xe3\x83\x90%E3%83%90") # => Encoding::CompatibilityError
- *Ian Ker-Seymer*
+ *Ashe Connor*, *Aaron Patterson*
-* 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`.
+* Add `before?` and `after?` methods to `Date`, `DateTime`,
+ `Time`, and `TimeWithZone`.
- Fixes #18550.
+ *Nick Holden*
- *Aditya Kapoor*
+* `ActiveSupport::Inflector#ordinal` and `ActiveSupport::Inflector#ordinalize` now support
+ translations through I18n.
-* Add missing time zone definitions for Russian Federation and sync them
- with `zone.tab` file from tzdata version 2014j (latest).
+ # locale/fr.rb
- *Andrey Novikov*
+ {
+ fr: {
+ number: {
+ nth: {
+ ordinals: lambda do |_key, number:, **_options|
+ if number.to_i.abs == 1
+ 'er'
+ else
+ 'e'
+ end
+ end,
-* Add `SecureRandom.base58` for generation of random base58 strings.
+ ordinalized: lambda do |_key, number:, **_options|
+ "#{number}#{ActiveSupport::Inflector.ordinal(number)}"
+ end
+ }
+ }
+ }
+ }
- *Matthew Draper*, *Guillermo Iguaran*
-* Add `#prev_day` and `#next_day` counterparts to `#yesterday` and
- `#tomorrow` for `Date`, `Time`, and `DateTime`.
+ *Christian Blais*
- *George Claghorn*
-
-* Add `same_time` option to `#next_week` and `#prev_week` for `Date`, `Time`,
- and `DateTime`.
-
- *George Claghorn*
-
-* Add `#on_weekend?`, `#next_weekday`, `#prev_weekday` methods to `Date`,
- `Time`, and `DateTime`.
-
- `#on_weekend?` returns `true` if the receiving date/time falls on a Saturday
- or Sunday.
-
- `#next_weekday` returns a new date/time representing the next day that does
- not fall on a Saturday or Sunday.
-
- `#prev_weekday` returns a new date/time representing the previous day that
- does not fall on a Saturday or Sunday.
-
- *George Claghorn*
-
-* Change the default test order from `:sorted` to `:random`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`.
-
- *Rafael Mendonça França*
+* Add `:private` option to ActiveSupport's `Module#delegate`
+ in order to delegate methods as private:
-* Remove deprecated methods `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string=`
- and `ActiveSupport::JSON::Encoding.encode_big_decimal_as_string`.
+ class User < ActiveRecord::Base
+ has_one :profile
+ delegate :date_of_birth, to: :profile, private: true
- *Rafael Mendonça França*
-
-* Remove deprecated `ActiveSupport::SafeBuffer#prepend`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated methods at `Kernel`.
-
- `silence_stderr`, `silence_stream`, `capture` and `quietly`.
-
- *Rafael Mendonça França*
-
-* Remove deprecated `active_support/core_ext/big_decimal/yaml_conversions`
- file.
-
- *Rafael Mendonça França*
-
-* Remove deprecated methods `ActiveSupport::Cache::Store.instrument` and
- `ActiveSupport::Cache::Store.instrument=`.
-
- *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, callbacks could only be halted by explicitly providing a
- terminator and by having a callback match the conditions of the terminator.
-
-* Add `ActiveSupport.halt_callback_chains_on_return_false`
-
- Setting `ActiveSupport.halt_callback_chains_on_return_false`
- to `true` will let an app support the deprecated way of halting Active Record,
- and Active Model callback chains by returning `false`.
-
- Setting the value to `false` will tell the app to ignore any `false` value
- returned by those callbacks, and only halt the chain upon `throw(:abort)`.
-
- 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*, *Roque Pinel*
+ # 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.5.0 or newer.
- *Genadi Samokovarov*
+ *Jeremy Daer*, *Kasper Timm Hansen*
-* 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 40235833ba..315a4bc7c4 100644
--- a/activesupport/MIT-LICENSE
+++ b/activesupport/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2005-2016 David Heinemeier Hansson
+Copyright (c) 2005-2019 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc
index 14ce204303..d8d25d86c8 100644
--- a/activesupport/README.rdoc
+++ b/activesupport/README.rdoc
@@ -5,6 +5,7 @@ extensions that were found useful for the Rails framework. These additions
reside in this package so they can be loaded as needed in Ruby projects
outside of Rails.
+You can read more about the extensions in the {Active Support Core Extensions}[https://edgeguides.rubyonrails.org/active_support_core_extensions.html] guide.
== Download and installation
@@ -21,16 +22,16 @@ 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
API documentation is at:
-* http://api.rubyonrails.org
+* https://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/activesupport/Rakefile b/activesupport/Rakefile
index 33ee62aa1b..f10f19be0a 100644
--- a/activesupport/Rakefile
+++ b/activesupport/Rakefile
@@ -1,22 +1,43 @@
-require 'rake/testtask'
+# frozen_string_literal: true
-task :default => :test
+require "rake/testtask"
+
+task default: :test
task :package
-task "package:clean"
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 71fe4d7253..bf0fe0f76d 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -1,27 +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 = '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.5.0"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "https://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://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 'tzinfo', '~> 1.1'
- s.add_dependency 'minitest', '~> 5.1'
- s.add_dependency 'concurrent-ruby', '~> 1.0'
+ 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"
+ s.add_dependency "zeitwerk", "~> 1.4", ">= 1.4.3"
end
diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables
deleted file mode 100755
index 2193533588..0000000000
--- a/activesupport/bin/generate_tables
+++ /dev/null
@@ -1,139 +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.combining_class = Integer($4)
- codepoint.decomp_type = $7
- codepoint.decomp_mapping = ($8=='') ? nil : $8.split.collect(&:hex)
- codepoint.uppercase_mapping = ($16=='') ? 0 : $16.hex
- codepoint.lowercase_mapping = ($17=='') ? 0 : $17.hex
- @ucd.codepoints[codepoint.code] = codepoint
- end
-
- def parse_grapheme_break_property(line)
- if line =~ /^([0-9A-F.]+)\s*;\s*([\w]+)\s*#/
- type = $2.downcase.intern
- @ucd.boundary[type] ||= []
- if $1.include? '..'
- parts = $1.split '..'
- @ucd.boundary[type] << (parts[0].hex..parts[1].hex)
- else
- @ucd.boundary[type] << $1.hex
- end
- end
- end
-
- def parse_composition_exclusion(line)
- if line =~ /^([0-9A-F]+)/i
- @ucd.composition_exclusion << $1.hex
- end
- end
-
- def parse_cp1252(line)
- if line =~ /^([0-9A-Fx]+)\s([0-9A-Fx]+)/i
- @ucd.cp1252[$1.hex] = $2.hex
- end
- end
-
- def create_composition_map
- @ucd.codepoints.each do |_, cp|
- if !cp.nil? 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 72777baecd..5589c71281 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
#--
-# Copyright (c) 2005-2016 David Heinemeier Hansson
+# Copyright (c) 2005-2019 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,18 @@
# 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
@@ -49,6 +53,7 @@ module ActiveSupport
autoload :Callbacks
autoload :Configurable
autoload :Deprecation
+ autoload :Digest
autoload :Gzip
autoload :Inflector
autoload :JSON
@@ -78,12 +83,12 @@ module ActiveSupport
cattr_accessor :test_order # :nodoc:
- def self.halt_callback_chains_on_return_false
- Callbacks.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.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 f59ddf5403..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,7 +22,7 @@ module ActiveSupport
# variants.any?(:phone, :tablet) # => true
# variants.any?('phone', 'desktop') # => true
# variants.any?(:desktop, :watch) # => false
- def any?(*candidates, &block)
+ def any?(*candidates)
if candidates.none?
super
else
@@ -30,11 +34,11 @@ module ActiveSupport
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 3988b147ac..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
@@ -39,7 +41,7 @@ module ActiveSupport
result = nil
ms = Benchmark.ms { result = options[:silence] ? logger.silence { yield } : yield }
- logger.send(options[:level], '%s (%.1fms)' % [ message, ms ])
+ 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 1c63e8a93f..e055135bb4 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -1,29 +1,29 @@
-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'
-require 'active_support/core_ext/string/strip'
+# 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
@@ -73,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}/"
@@ -91,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})"
@@ -146,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
@@ -198,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
+ #
+ # 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</tt> will store a large cache entry set by the call
- # in a compressed format.
+ # Setting <tt>compress: false</tt> disables compression of the cache entry.
#
# Setting <tt>:expires_in</tt> will set an expiration time on the cache.
# All caches support auto-expiring content after a specified number of
@@ -215,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
@@ -243,14 +280,14 @@ 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
@@ -283,6 +320,7 @@ module ActiveSupport
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
@@ -292,26 +330,38 @@ module ActiveSupport
else
save_block_result_to_cache(name, options) { |_name| yield _name }
end
+ elsif options && options[:force]
+ raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block."
else
read(name, options)
end
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 = normalize_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
@@ -323,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.
@@ -333,27 +383,33 @@ module ActiveSupport
options = names.extract_options!
options = merged_options(options)
- results = {}
- names.each do |name|
- key = normalize_key(name, options)
- entry = read_entry(key, options)
- if entry
- if entry.expired?
- delete_entry(key, options)
- else
- results[name] = entry.value
- end
+ instrument :read_multi, names, options do |payload|
+ read_multi_entries(names, options).tap do |results|
+ payload[:hits] = results.keys
+ end
+ 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
- results
end
# Fetches data from the cache, using the given keys. If there is data in
# 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:
#
@@ -364,17 +420,36 @@ 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|
+ reads = read_multi_entries(names, options)
+ writes = {}
+ ordered = names.each_with_object({}) do |name, hash|
+ hash[name] = reads.fetch(name) { writes[name] = yield(name) }
end
+
+ payload[:hits] = reads.keys
+ payload[:super_operation] = :fetch_multi
+
+ write_multi(writes, options)
+
+ ordered
end
end
@@ -385,7 +460,7 @@ module ActiveSupport
options = merged_options(options)
instrument(:write, name, options) do
- entry = Entry.new(value, options)
+ entry = Entry.new(value, options.merge(version: normalize_version(name, options)))
write_entry(normalize_key(name, options), entry, options)
end
end
@@ -409,66 +484,66 @@ module ActiveSupport
instrument(:exist?, name) do
entry = read_entry(normalize_key(name, options), options)
- (entry && !entry.expired?) || false
+ (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.
#
- # All implementations may not support this method.
+ # Some implementations may not support this method.
def delete_matched(matcher, options = nil)
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.
#
- # All implementations may not support this method.
+ # Some implementations may not support this method.
def increment(name, amount = 1, options = nil)
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.
#
- # All implementations may not support this method.
+ # Some implementations may not support this method.
def decrement(name, amount = 1, options = nil)
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.
#
- # All implementations may not support this method.
+ # Some implementations may not support this method.
def cleanup(options = nil)
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.
#
- # All implementations may not support this method.
+ # Some implementations may not support this method.
def clear(options = nil)
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]}"
@@ -479,78 +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 normalize_key(key, options)
- key = expanded_key(key)
- namespace = options[:namespace] if options
- prefix = namespace.is_a?(Proc) ? namespace.call : namespace
- key = "#{prefix}:#{key}" if prefix
- key
+ def normalize_version(key, options = nil)
+ (options && options[:version].try(:to_param)) || expanded_version(key)
end
- def namespaced_key(*args)
- ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
- `namespaced_key` is deprecated and will be removed from Rails 5.1.
- Please use `normalize_key` which will return a fully resolved key.
- MESSAGE
- normalize_key(*args)
+ 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(operation, key, options = nil)
log { "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}" }
- payload = { :key => key }
+ payload = { key: key }
payload.merge!(options) if options.is_a?(Hash)
- ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) }
+ ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) }
end
def log
@@ -565,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
@@ -575,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
@@ -638,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)
@@ -665,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 99c55b1aa4..f43894a1ea 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
@@ -16,20 +18,24 @@ module ActiveSupport
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
- GITKEEP_FILES = ['.gitkeep', '.keep'].freeze
+ GITKEEP_FILES = [".gitkeep", ".keep"].freeze
def initialize(cache_path, options = nil)
super(options)
@cache_path = cache_path.to_s
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 .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 = exclude_from(cache_path, EXCLUDED_DIRS + GITKEEP_FILES)
- FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)})
+ root_dirs = (Dir.children(cache_path) - GITKEEP_FILES)
+ FileUtils.rm_r(root_dirs.collect { |f| File.join(cache_path, f) })
rescue Errno::ENOENT
end
@@ -37,9 +43,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
@@ -66,7 +71,7 @@ module ActiveSupport
end
end
- protected
+ private
def read_entry(key, options)
if File.exist?(key)
@@ -80,7 +85,7 @@ module ActiveSupport
def write_entry(key, entry, options)
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)}
+ File.atomic_write(key, cache_path) { |f| Marshal.dump(entry, f) }
true
end
@@ -98,17 +103,14 @@ module ActiveSupport
end
end
- private
# Lock a file for a block so only one process can modify it at a time.
- def lock_file(file_name, &block) # :nodoc:
+ def lock_file(file_name, &block)
if File.exist?(file_name)
- File.open(file_name, 'r+') do |f|
- begin
- f.flock File::LOCK_EX
- yield
- ensure
- f.flock File::LOCK_UN
- end
+ File.open(file_name, "r+") do |f|
+ f.flock File::LOCK_EX
+ yield
+ ensure
+ f.flock File::LOCK_UN
end
else
yield
@@ -121,29 +123,25 @@ module ActiveSupport
fname = URI.encode_www_form_component(key)
if fname.size > FILEPATH_MAX_SIZE
- fname = Digest::MD5.hexdigest(key)
+ fname = ActiveSupport::Digest.hexdigest(key)
end
hash = Zlib.adler32(fname)
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?
-
- File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths)
- end
+ 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
- def key_file_path(key)
- ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
- `key_file_path` is deprecated and will be removed from Rails 5.1.
- Please use `normalize_key` which will return a fully resolved key or nothing.
- MESSAGE
- key
+ File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, fname_paths)
end
# Translate a file path into a key.
@@ -155,7 +153,7 @@ module ActiveSupport
# Delete empty directories in the cache.
def delete_empty_directories(dir)
return if File.realpath(dir) == File.realpath(cache_path)
- if exclude_from(dir, EXCLUDED_DIRS).empty?
+ if Dir.children(dir).empty?
Dir.delete(dir) rescue nil
delete_empty_directories(File.dirname(dir))
end
@@ -168,8 +166,7 @@ module ActiveSupport
def search_dir(dir, &callback)
return if !File.exist?(dir)
- Dir.foreach(dir) do |d|
- next if EXCLUDED_DIRS.include?(d)
+ Dir.each_child(dir) do |d|
name = File.join(dir, d)
if File.directory?(name)
search_dir(name, &callback)
@@ -194,11 +191,6 @@ 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 2ca4b51efa..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.
#
@@ -26,24 +27,29 @@ module ActiveSupport
class MemCacheStore < Store
# 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)
+ private
+ def read_entry(key, options)
+ entry = super
+ if options[:raw] && local_cache && entry
+ entry = deserialize_entry(entry.value)
+ end
+ entry
end
- entry
- end
- def write_entry(key, entry, options) # :nodoc:
- 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
+ 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
+ end
+
+ # Advertise cache versioning support.
+ def self.supports_cache_versioning?
+ true
end
prepend Strategy::LocalCache
@@ -62,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
@@ -85,36 +98,20 @@ 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
end
- # Reads multiple values from the cache using a single call to the
- # servers for all keys. Options can be passed in the last argument.
- def read_multi(*names)
- options = names.extract_options!
- options = merged_options(options)
-
- keys_to_names = Hash[names.map{|name| [normalize_key(name, options), name]}]
- raw_values = @data.get_multi(keys_to_names.keys, :raw => true)
- values = {}
- raw_values.each do |key, value|
- entry = deserialize_entry(value)
- values[keys_to_names[key]] = entry.value unless entry.expired?
- end
- values
- end
-
# Increment a cached value. This method uses the memcached incr atomic
# 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
+ instrument(:increment, name, amount: amount) do
rescue_error_with nil do
- @data.incr(normalize_key(name, options), amount)
+ @data.with { |c| c.incr(normalize_key(name, options), amount, options[:expires_in]) }
end
end
end
@@ -123,11 +120,11 @@ module ActiveSupport
# 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
+ instrument(:decrement, name, amount: amount) do
rescue_error_with nil do
- @data.decr(normalize_key(name, options), amount)
+ @data.with { |c| c.decr(normalize_key(name, options), amount, options[:expires_in]) }
end
end
end
@@ -135,22 +132,22 @@ module ActiveSupport
# Clear the entire cache on all memcached servers. This method should
# be used with care when shared cache is being used.
def clear(options = nil)
- rescue_error_with(nil) { @data.flush_all }
+ rescue_error_with(nil) { @data.with { |c| c.flush_all } }
end
# Get the statistics from the memcached servers.
def stats
- @data.stats
+ @data.with { |c| c.stats }
end
- protected
+ private
# Read an entry from the cache.
- def read_entry(key, options) # :nodoc:
- rescue_error_with(nil) { deserialize_entry(@data.get(key, options)) }
+ 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
@@ -159,16 +156,32 @@ module ActiveSupport
expires_in += 5.minutes
end
rescue_error_with false do
- @data.send(method, key, value, expires_in, options)
+ @data.with { |c| c.send(method, key, value, expires_in, options) }
end
end
- # Delete an entry from the cache.
- def delete_entry(key, options) # :nodoc:
- rescue_error_with(false) { @data.delete(key) }
+ # 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
@@ -176,16 +189,8 @@ module ActiveSupport
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
- end
-
- def escape_key(key)
- ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
- `escape_key` is deprecated and will be removed from Rails 5.1.
- Please use `normalize_key` which will return a fully resolved key or nothing.
- MESSAGE
+ 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
diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb
index 896c28ad8b..629eb2dd70 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?
@@ -54,13 +62,13 @@ module ActiveSupport
return if pruning?
@pruning = true
begin
- start_time = Time.now
+ start_time = Concurrent.monotonic_time
cleanup
- instrument(:prune, target_size, :from => @cache_size) do
- keys = synchronize{ @key_access.keys.sort{|a,b| @key_access[a].to_f <=> @key_access[b].to_f} }
+ instrument(:prune, target_size, from: @cache_size) do
+ keys = synchronize { @key_access.keys.sort { |a, b| @key_access[a].to_f <=> @key_access[b].to_f } }
keys.each do |key|
delete_entry(key, options)
- return if @cache_size <= target_size || (max_time && Time.now - start_time > max_time)
+ return if @cache_size <= target_size || (max_time && Concurrent.monotonic_time - start_time > max_time)
end
end
ensure
@@ -83,6 +91,7 @@ module ActiveSupport
modify_value(name, -amount, options)
end
+ # Deletes cache entries if the cache key matches a given pattern.
def delete_matched(matcher, options = nil)
options = merged_options(options)
instrument(:delete_matched, matcher.inspect) do
@@ -104,15 +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
@@ -124,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]
@@ -141,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)
@@ -150,8 +159,6 @@ module ActiveSupport
end
end
- private
-
def modify_value(name, amount, options)
synchronize do
options = merged_options(options)
diff --git a/activesupport/lib/active_support/cache/null_store.rb b/activesupport/lib/active_support/cache/null_store.rb
index 0564ce5312..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
@@ -10,6 +12,11 @@ module ActiveSupport
class NullStore < Store
prepend Strategy::LocalCache
+ # Advertise cache versioning support.
+ def self.supports_cache_versioning?
+ true
+ end
+
def clear(options = nil)
end
@@ -25,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 df38dbcf11..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
@@ -70,6 +83,7 @@ module ActiveSupport
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
@@ -86,26 +100,26 @@ module ActiveSupport
def cleanup(options = nil) # :nodoc:
return super unless cache = local_cache
- cache.clear(options)
+ cache.clear
super
end
def increment(name, amount = 1, options = nil) # :nodoc:
return super unless local_cache
- value = bypass_local_cache{super}
+ value = bypass_local_cache { super }
write_cache_value(name, value, options)
value
end
def decrement(name, amount = 1, options = nil) # :nodoc:
return super unless local_cache
- value = bypass_local_cache{super}
+ value = bypass_local_cache { super }
write_cache_value(name, value, options)
value
end
- protected
- def read_entry(key, options) # :nodoc:
+ private
+ def read_entry(key, options)
if cache = local_cache
cache.fetch_entry(key) { super }
else
@@ -113,25 +127,35 @@ module ActiveSupport
end
end
- def write_entry(key, entry, options) # :nodoc:
- local_cache.write_entry(key, entry, options) if local_cache
- super
+ 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 delete_entry(key, options) # :nodoc:
- local_cache.delete_entry(key, options) if local_cache
+ 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 set_cache_value(value, name, amount, options) # :nodoc:
- ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
- `set_cache_value` is deprecated and will be removed from Rails 5.1.
- Please use `write_cache_value`
- MESSAGE
- write_cache_value name, value, options
+ def delete_entry(key, options)
+ local_cache.delete_entry(key, options) if local_cache
+ super
end
- def write_cache_value(name, value, options) # :nodoc:
+ def write_cache_value(name, value, options)
name = normalize_key(name, options)
cache = local_cache
cache.mute do
@@ -143,10 +167,8 @@ module ActiveSupport
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 d878d44d02..d0644a0f7e 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -1,13 +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/module/attribute_accessors'
-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.
@@ -22,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
@@ -63,16 +67,11 @@ module ActiveSupport
included do
extend ActiveSupport::DescendantsTracker
+ class_attribute :__callbacks, instance_writer: false, default: {}
end
CALLBACK_FILTER_TYPES = [:before, :after, :around]
- # If true, Active Record and Active Model callbacks returning +false+ will
- # halt the entire callback chain and display a deprecation message.
- # If false, callback chains will only be halted by calling +throw :abort+.
- # Defaults to +true+.
- mattr_accessor(:halt_and_display_warning_on_return_false, instance_writer: false) { true }
-
# Runs the callbacks for the given event.
#
# Calls the before and around callbacks in the order they were set, yields
@@ -86,706 +85,772 @@ module ActiveSupport
# run_callbacks :save do
# save
# end
- def run_callbacks(kind, &block)
- send "_run_#{kind}_callbacks", &block
- end
-
- private
+ #
+ #--
+ #
+ # 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]
- def __run_callbacks__(callbacks, &block)
if callbacks.empty?
yield if block_given?
else
- runner = callbacks.compile
- e = Filters::Environment.new(self, false, nil, block)
- runner.call(e).value
- end
- end
-
- # 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
+ 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
- module Conditionals # :nodoc:
- class Value
- def initialize(&block)
- @block = block
+ # 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
- def call(target, value); @block.call(value); end
end
end
- module Filters
- Environment = Struct.new(:target, :halted, :value, :run_block)
+ private
- class End
- def call(env)
- block = env.run_block
- env.value = !env.halted && (!block || block.call)
- env
- 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
- ENDING = End.new
-
- class Before
- def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter)
- halted_lambda = chain_config[:terminator]
- 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)
+ module Conditionals # :nodoc:
+ class Value
+ def initialize(&block)
+ @block = block
end
+ def call(target, value); @block.call(value); end
end
+ end
- 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
+ module Filters
+ Environment = Struct.new(:target, :halted, :value)
- 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
+ class Before
+ def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter)
+ halted_lambda = chain_config[:terminator]
+
+ 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
+
+ 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
- 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
- end
- 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)
+ 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
- halting(callback_sequence, user_callback)
+ if user_conditions.any?
+ conditional callback_sequence, user_callback, user_conditions
+ else
+ simple callback_sequence, user_callback
+ end
end
- else
- if user_conditions.any?
- conditional callback_sequence, user_callback, user_conditions
- else
- simple callback_sequence, user_callback
+ end
+
+ 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
+
+ if !halted && user_conditions.all? { |c| c.call(target, value) }
+ user_callback.call target, value
+ end
+
+ 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
- env
+ 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
+ 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
+
+ env
end
+ end
+ private_class_method :simple
+ end
+ end
- env
+ 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
+
+ new chain.name, filter, kind, options, chain.config
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
+ 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
- if user_conditions.all? { |c| c.call(target, value) }
- user_callback.call target, value
- end
+ 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 :conditional
- def self.simple(callback_sequence, user_callback)
- callback_sequence.after do |env|
- user_callback.call env.target, env.value
+ def matches?(_kind, _filter)
+ @kind == _kind && filter == _filter
+ end
- env
+ def duplicates?(other)
+ case @filter
+ when Symbol
+ matches?(other.kind, other.filter)
+ else
+ false
end
end
- private_class_method :simple
- end
- class Around
- def self.build(callback_sequence, user_callback, user_conditions, chain_config)
- if user_conditions.any?
- halting_and_conditional(callback_sequence, user_callback, user_conditions)
- else
- halting(callback_sequence, user_callback)
+ # 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
- 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
+ def current_scopes
+ Array(chain_config[:scope]).map { |s| public_send(s) }
+ end
- if !halted && user_conditions.all? { |c| c.call(target, value) }
- user_callback.call(target, value) {
- run.call.value
- }
- env
- else
- run.call
+ 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_and_conditional
- def self.halting(callback_sequence, user_callback)
- callback_sequence.around do |env, &run|
- target = env.target
- value = env.value
+ conditionals
+ end
- if env.halted
- run.call
+ def compute_identifier(filter)
+ case filter
+ when ::Proc
+ filter.object_id
else
- user_callback.call(target, value) {
- run.call.value
- }
- env
+ filter
end
end
- end
- private_class_method :halting
+
+ def conditions_lambdas
+ @if.map { |c| CallTemplate.build(c, self).make_lambda } +
+ @unless.map { |c| CallTemplate.build(c, self).inverted_lambda }
+ end
end
- end
- class Callback #:nodoc:#
- def self.build(chain, filter, kind, options)
- if filter.is_a?(String)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing string to define callback is deprecated and will be removed
- in Rails 5.1 without replacement.
- MSG
+ # 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
- new chain.name, filter, kind, options, chain.config
- 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
+ }
- 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
+ result.unshift @method_name
+ result.unshift @override_block || block
+ result.unshift @override_target || target
- def filter; @key; end
- def raw_filter; @filter; end
+ # target, block, method, *arguments = result
+ # target.send(method, *arguments, &block)
+ result
+ end
- def merge_conditional_options(chain, if_option:, unless_option:)
- options = {
- :if => @if.dup,
- :unless => @unless.dup
- }
+ # 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
- 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, 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
- self.class.build chain, @filter, @kind, options
- 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 matches?(_kind, _filter)
- @kind == _kind && filter == _filter
+ new(filter, method_to_call, [:target], nil)
+ end
+ end
end
- def duplicates?(other)
- case @filter
- when Symbol, String
- matches?(other.kind, other.filter)
- else
- false
+ # 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
+
+ @before = []
+ @after = []
end
- end
- # Wraps code with filter
- def apply(callback_sequence)
- user_conditions = conditions_lambdas
- user_callback = make_lambda @filter
+ def before(&before)
+ @before.unshift(before)
+ self
+ end
- 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)
+ def after(&after)
+ @after.push(after)
+ self
end
- end
- private
+ def around(call_template, user_conditions)
+ CallbackSequence.new(self, call_template, user_conditions)
+ end
- def invert_lambda(l)
- lambda { |*args, &blk| !l.call(*args, &blk) }
- end
+ def skip?(arg)
+ arg.halted || !@user_conditions.all? { |c| c.call(arg.target, arg.value) }
+ 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("_")
+ attr_reader :nested
- lambda { |target, _, &blk|
- filter.public_send method_to_call, target, &blk
- }
+ def final?
+ !@call_template
end
- end
- def compute_identifier(filter)
- case filter
- when String, ::Proc
- filter.object_id
- else
- filter
+ def expand_call_template(arg, block)
+ @call_template.expand(arg.target, arg.value, block)
end
- end
- def conditions_lambdas
- @if.map { |c| make_lambda c } +
- @unless.map { |c| invert_lambda make_lambda c }
- end
- end
+ def invoke_before(arg)
+ @before.each { |b| b.call(arg) }
+ end
- # 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 = []
+ def invoke_after(arg)
+ @after.each { |a| a.call(arg) }
+ end
end
- def before(&before)
- @before.unshift(before)
- self
- end
+ class CallbackChain #:nodoc:#
+ include Enumerable
- def after(&after)
- @after.push(after)
- self
- end
+ attr_reader :name, :config
- def around(&around)
- CallbackSequence.new do |arg|
- around.call(arg) {
- self.call(arg)
- }
+ def initialize(name, config)
+ @name = name
+ @config = {
+ scope: [:kind],
+ terminator: default_terminator
+ }.merge!(config)
+ @chain = []
+ @callbacks = nil
+ @mutex = Mutex.new
end
- end
- def call(arg)
- @before.each { |b| b.call(arg) }
- value = @call.call(arg)
- @after.each { |a| a.call(arg) }
- value
- end
- end
+ def each(&block); @chain.each(&block); end
+ def index(o); @chain.index(o); end
+ def empty?; @chain.empty?; end
- # An Array with a compile method.
- class CallbackChain #:nodoc:#
- include Enumerable
-
- attr_reader :name, :config
-
- 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
+ @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback|
+ callback.apply callback_sequence
+ end
+ end
+ end
- def initialize_copy(other)
- @callbacks = nil
- @chain = other.chain.dup
- @mutex = Mutex.new
- end
+ def append(*callbacks)
+ callbacks.each { |c| append_one(c) }
+ 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
- end
+ def prepend(*callbacks)
+ callbacks.each { |c| prepend_one(c) }
end
- end
- def append(*callbacks)
- callbacks.each { |c| append_one(c) }
- end
+ protected
+ attr_reader :chain
- def prepend(*callbacks)
- callbacks.each { |c| prepend_one(c) }
- end
+ private
- protected
- def chain; @chain; end
+ def append_one(callback)
+ @callbacks = nil
+ remove_duplicates(callback)
+ @chain.push(callback)
+ end
- private
+ def prepend_one(callback)
+ @callbacks = nil
+ remove_duplicates(callback)
+ @chain.unshift(callback)
+ end
- def append_one(callback)
- @callbacks = nil
- remove_duplicates(callback)
- @chain.push(callback)
- end
+ def remove_duplicates(callback)
+ @callbacks = nil
+ @chain.delete_if { |c| callback.duplicates?(c) }
+ end
- def prepend_one(callback)
- @callbacks = nil
- remove_duplicates(callback)
- @chain.unshift(callback)
+ def default_terminator
+ Proc.new do |target, result_lambda|
+ terminate = true
+ catch(:abort) do
+ result_lambda.call
+ terminate = false
+ end
+ terminate
+ end
+ end
end
- def remove_duplicates(callback)
- @callbacks = nil
- @chain.delete_if { |c| callback.duplicates?(c) }
- 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
- def default_terminator
- Proc.new do |target, result_lambda|
- terminate = true
- catch(:abort) do
- result_lambda.call if result_lambda.is_a?(Proc)
- terminate = false
+ # 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
- terminate
end
- 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
+ # 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
- # 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
+ __update_callbacks(name) do |target, chain|
+ options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
+ target.set_callbacks name, chain
+ end
end
- end
- # 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; as a string to be instance evaluated(deprecated); or as an
- # object that responds to a certain method determined by the <tt>:scope</tt>
- # argument to +define_callbacks+.
- #
- # 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
- 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: -> { 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: -> { 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
+ 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)
+ chain.delete(callback)
end
-
- chain.delete(callback)
+ 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
- end
+ 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
- self.set_callbacks name, callbacks.dup.clear
- end
+ 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 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", instance_writer: false
- set_callbacks name, CallbackChain.new(name, options)
-
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
- def _run_#{name}_callbacks(&block)
- __run_callbacks__(_#{name}_callbacks, &block)
+ # 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
- RUBY
- end
- end
- protected
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def _run_#{name}_callbacks(&block)
+ run_callbacks #{name.inspect}, &block
+ end
- def get_callbacks(name) # :nodoc:
- send "_#{name}_callbacks"
- end
+ def self._#{name}_callbacks
+ get_callbacks(#{name.inspect})
+ end
- def set_callbacks(name, callbacks) # :nodoc:
- send "_#{name}_callbacks=", callbacks
- end
+ def self._#{name}_callbacks=(value)
+ set_callbacks(#{name.inspect}, value)
+ end
- def deprecated_false_terminator # :nodoc:
- Proc.new do |target, result_lambda|
- terminate = true
- catch(:abort) do
- result = result_lambda.call if result_lambda.is_a?(Proc)
- if Callbacks.halt_and_display_warning_on_return_false && result == false
- display_deprecation_warning_for_false_terminator
- else
- terminate = false
- end
+ def _#{name}_callbacks
+ __callbacks[#{name.inspect}]
+ end
+ RUBY
end
- terminate
end
- end
- private
+ protected
+
+ def get_callbacks(name) # :nodoc:
+ __callbacks[name.to_sym]
+ end
- def display_deprecation_warning_for_false_terminator
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in the next release of Rails.
- To explicitly halt the callback chain, please use `throw :abort` instead.
- MSG
+ 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..5d356a0ab6 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) }
@@ -123,9 +125,13 @@ module ActiveSupport
def included(base = nil, &block)
if base.nil?
- raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
-
- @_included_block = block
+ if instance_variable_defined?(:@_included_block)
+ if @_included_block.source_location != block.source_location
+ raise MultipleIncludedBlocks
+ end
+ else
+ @_included_block = block
+ end
else
super
end
diff --git a/activesupport/lib/active_support/concurrency/latch.rb b/activesupport/lib/active_support/concurrency/latch.rb
deleted file mode 100644
index 4abe5ece6f..0000000000
--- a/activesupport/lib/active_support/concurrency/latch.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-require 'concurrent/atomic/count_down_latch'
-
-module ActiveSupport
- module Concurrency
- class Latch < Concurrent::CountDownLatch
-
- def initialize(count = 1)
- ActiveSupport::Deprecation.warn("ActiveSupport::Concurrency::Latch is deprecated. Please use Concurrent::CountDownLatch instead.")
- super(count)
- end
-
- alias_method :release, :count_down
-
- def await
- wait(nil)
- 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 54244317e4..f18ccf1c88 100644
--- a/activesupport/lib/active_support/concurrency/share_lock.rb
+++ b/activesupport/lib/active_support/concurrency/share_lock.rb
@@ -1,5 +1,7 @@
-require 'thread'
-require 'monitor'
+# frozen_string_literal: true
+
+require "thread"
+require "monitor"
module ActiveSupport
module Concurrency
@@ -13,6 +15,37 @@ 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
+
+ # 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()
@@ -21,6 +54,7 @@ module ActiveSupport
@sharing = Hash.new(0)
@waiting = {}
+ @sleeping = {}
@exclusive_thread = nil
@exclusive_depth = 0
end
@@ -46,7 +80,7 @@ module ActiveSupport
return false if no_wait
yield_shares(purpose: purpose, compatible: compatible, block_share: true) do
- @cv.wait_while { busy_for_exclusive?(purpose) }
+ wait_for(:start_exclusive) { busy_for_exclusive?(purpose) }
end
end
@exclusive_thread = Thread.current
@@ -69,7 +103,7 @@ module ActiveSupport
if eligible_waiters?(compatible)
yield_shares(compatible: compatible, block_share: true) do
- @cv.wait_while { @exclusive_thread || eligible_waiters?(compatible) }
+ wait_for(:stop_exclusive) { @exclusive_thread || eligible_waiters?(compatible) }
end
end
@cv.broadcast
@@ -84,11 +118,11 @@ module ActiveSupport
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
- @cv.wait_while { @exclusive_thread }
+ 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
- @cv.wait_while { busy_for_sharing?(false) }
+ wait_for(:start_sharing) { busy_for_sharing?(false) }
end
@sharing[Thread.current] += 1
end
@@ -144,16 +178,16 @@ module ActiveSupport
end
compatible |= [false] unless block_share
@waiting[Thread.current] = [purpose, compatible]
-
- @cv.broadcast
end
+
+ @cv.broadcast
end
begin
yield
ensure
synchronize do
- @cv.wait_while { @exclusive_thread && @exclusive_thread != Thread.current }
+ wait_for(:yield_shares) { @exclusive_thread && @exclusive_thread != Thread.current }
if previous_wait
@waiting[Thread.current] = previous_wait
@@ -167,20 +201,27 @@ module ActiveSupport
private
- # Must be called within synchronize
- def busy_for_exclusive?(purpose)
- busy_for_sharing?(purpose) ||
- @sharing.size > (@sharing[Thread.current] > 0 ? 1 : 0)
- end
+ # 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 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 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..71c23dae9b 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
@@ -66,8 +67,8 @@ module ActiveSupport
# end
# # => NameError: invalid config attribute name
#
- # 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>.
+ # To omit the instance writer method, pass <tt>instance_writer: false</tt>.
+ # To omit the instance reader method, pass <tt>instance_reader: false</tt>.
#
# class User
# include ActiveSupport::Configurable
@@ -80,7 +81,7 @@ module ActiveSupport
# User.new.allowed_access = true # => NoMethodError
# User.new.allowed_access # => NoMethodError
#
- # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods.
+ # Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
#
# class User
# include ActiveSupport::Configurable
@@ -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..88b6567712 100644
--- a/activesupport/lib/active_support/core_ext/array.rb
+++ b/activesupport/lib/active_support/core_ext/array.rb
@@ -1,7 +1,9 @@
-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/inquiry"
diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb
index 37d833887a..ea01e5891c 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+.
#
@@ -27,16 +29,28 @@ class Array
end
end
- # Returns a copy of the Array without the specified elements.
+ # Returns a new array that includes the passed elements.
+ #
+ # [ 1, 2, 3 ].including(4, 5) # => [ 1, 2, 3, 4, 5 ]
+ # [ [ 0, 1 ] ].including([ [ 1, 0 ] ]) # => [ [ 0, 1 ], [ 1, 0 ] ]
+ def including(*elements)
+ self + elements.flatten(1)
+ end
+
+ # Returns a copy of the Array excluding the specified elements.
#
- # people = ["David", "Rafael", "Aaron", "Todd"]
- # people.without "Aaron", "Todd"
- # => ["David", "Rafael"]
+ # ["David", "Rafael", "Aaron", "Todd"].excluding("Aaron", "Todd") # => ["David", "Rafael"]
+ # [ [ 0, 1 ], [ 1, 0 ] ].excluding([ [ 1, 0 ] ]) # => [ [ 0, 1 ] ]
#
- # 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#excluding</tt> that uses <tt>Array#-</tt>
+ # instead of <tt>Array#reject</tt> for performance reasons.
+ def excluding(*elements)
+ self - elements.flatten(1)
+ end
+
+ # Alias for #excluding.
def without(*elements)
- self - elements
+ excluding(*elements)
end
# Equal to <tt>self[1]</tt>.
diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb
index 8718b7e1e5..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
@@ -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
@@ -92,9 +94,9 @@ class Array
case format
when :db
if empty?
- 'null'
+ "null"
else
- collect(&:id).join(',')
+ collect(&:id).join(",")
end
else
to_default_s
@@ -179,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
@@ -187,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]
@@ -197,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..ba3739f640 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,5 @@
-class Array
- # The human way of thinking about adding stuff to the end of a list is with append.
- alias_method :append, :<<
+# frozen_string_literal: true
- # 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
+require "active_support/deprecation"
+
+ActiveSupport::Deprecation.warn "Ruby 2.5+ (required by Rails 6) provides Array#append and Array#prepend natively, so requiring active_support/core_ext/array/prepend_and_append is no longer necessary. Requiring it will raise LoadError in Rails 6.1."
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 22fc7ecf92..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,12 +1,12 @@
-require 'bigdecimal'
-require 'bigdecimal/util'
+# frozen_string_literal: true
+
+require "bigdecimal"
+require "bigdecimal/util"
module ActiveSupport
module BigDecimalWithDefaultFormat #:nodoc:
- DEFAULT_STRING_FORMAT = 'F'
-
- def to_s(format = nil)
- super(format || DEFAULT_STRING_FORMAT)
+ def to_s(format = "F")
+ super(format)
end
end
end
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 802d988af2..255cbee55c 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,32 +80,34 @@ class Class
# object.setting = false # => NoMethodError
#
# To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
- 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_predicate = options.fetch(:instance_predicate, true)
-
+ #
+ # To set a default value for the attribute, pass <tt>default:</tt>, like so:
+ #
+ # class_attribute :settings, default: {}
+ def class_attribute(
+ *attrs,
+ instance_accessor: true,
+ instance_reader: instance_accessor,
+ instance_writer: instance_accessor,
+ instance_predicate: true,
+ default: nil
+ )
attrs.each do |name|
- remove_possible_singleton_method(name)
- define_singleton_method(name) { nil }
+ singleton_class.silence_redefinition_of_method(name)
+ define_singleton_method(name) { default }
- remove_possible_singleton_method("#{name}?")
+ singleton_class.silence_redefinition_of_method("#{name}?")
define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate
- ivar = "@#{name}"
+ ivar = "@#{name}".to_sym
- remove_possible_singleton_method("#{name}=")
+ 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 }
- end
+ redefine_singleton_method(name) { val }
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
@@ -106,8 +120,7 @@ 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
@@ -115,13 +128,13 @@ class Class
end
end
- remove_possible_method "#{name}?"
- define_method("#{name}?") { !!public_send(name) } if instance_predicate
+ redefine_method("#{name}?") { !!public_send(name) } if instance_predicate
end
if instance_writer
- remove_possible_method "#{name}="
- attr_writer name
+ redefine_method("#{name}=") do |val|
+ instance_variable_set ivar, val
+ 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 b0f9a8be34..56fb46a88d 100644
--- a/activesupport/lib/active_support/core_ext/class/subclasses.rb
+++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb
@@ -1,20 +1,33 @@
-require 'active_support/core_ext/module/anonymous'
-require 'active_support/core_ext/module/reachable'
+# frozen_string_literal: true
class Class
begin
# Test if this Ruby supports each_object against singleton_class
- ObjectSpace.each_object(Numeric.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 9.0.4.0 and earlier
- def descendants # :nodoc:
+ def descendants
descendants = []
ObjectSpace.each_object(Class) do |k|
descendants.unshift k if k < self
@@ -26,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 7f0f4639a2..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/blank'
-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
index 71627b6a6f..e6271c79b3 100644
--- a/activesupport/lib/active_support/core_ext/date/blank.rb
+++ b/activesupport/lib/active_support/core_ext/date/blank.rb
@@ -1,4 +1,6 @@
-require 'date'
+# frozen_string_literal: true
+
+require "date"
class Date #:nodoc:
# No Date is blank:
diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb
index d589b67bf7..d03a8d3997 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
@@ -108,12 +110,13 @@ class Date
# Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with
# any of these keys: <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>.
def advance(options)
- options = options.dup
d = self
- d = d >> options.delete(:years) * 12 if options[:years]
- d = d >> options.delete(:months) if options[:months]
- d = d + options.delete(:weeks) * 7 if options[:weeks]
- d = d + options.delete(:days) if options[:days]
+
+ d = d >> options[:years] * 12 if options[:years]
+ d = d >> options[:months] if options[:months]
+ d = d + options[:weeks] * 7 if options[:weeks]
+ d = d + options[:days] if options[:days]
+
d
end
@@ -129,11 +132,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 ed8bca77ac..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,11 +59,13 @@ 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).
#
@@ -79,10 +75,16 @@ class Date
# date.to_time(:local) # => 2007-11-10 00:00:00 0800
#
# date.to_time(:utc) # => 2007-11-10 00:00:00 UTC
+ #
+ # 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 4da7fdd159..e2e11545e2 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,15 +1,17 @@
-require 'active_support/core_ext/object/try'
+# 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 ]
@@ -18,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.
@@ -28,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.
@@ -58,44 +60,54 @@ module DateAndTime
!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.
@@ -108,7 +120,7 @@ module DateAndTime
# 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
@@ -122,8 +134,8 @@ module DateAndTime
# 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)
+ first_quarter_month = month - (2 + month) % 3
+ beginning_of_month.change(month: first_quarter_month)
end
alias :at_beginning_of_quarter :beginning_of_quarter
@@ -137,8 +149,8 @@ module DateAndTime
# 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
+ last_quarter_month = month + (12 - month) % 3
+ beginning_of_month.change(month: last_quarter_month).end_of_month
end
alias :at_end_of_quarter :end_of_quarter
@@ -152,7 +164,7 @@ module DateAndTime
# 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
@@ -186,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)
@@ -196,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.
@@ -221,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
@@ -233,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.
@@ -290,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
@@ -315,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
@@ -325,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 d29a8db5cf..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,3 +1,5 @@
+# frozen_string_literal: true
+
module DateAndTime
module Zones
# Returns the simultaneous time in <tt>Time.zone</tt> if a zone is given or
@@ -5,7 +7,7 @@ module DateAndTime
#
# Time.zone = 'Hawaii' # => 'Hawaii'
# 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
+ # 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,7 +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
- # 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
@@ -22,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 5450533935..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/blank'
-require 'active_support/core_ext/date_time/calculations'
-require 'active_support/core_ext/date_time/conversions'
+# 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
index 56981b75fb..a52c8bc150 100644
--- a/activesupport/lib/active_support/core_ext/date_time/blank.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/blank.rb
@@ -1,4 +1,6 @@
-require 'date'
+# frozen_string_literal: true
+
+require "date"
class DateTime #:nodoc:
# No DateTime is ever blank:
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 ac46f5ffe8..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?
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 f59d05b214..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.
@@ -64,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
@@ -95,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/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 8a74ad4d66..4675c41936 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(:+) || 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+
@@ -55,30 +97,50 @@ module Enumerable
end
end
+ # Returns a new array that includes the passed elements.
+ #
+ # [ 1, 2, 3 ].including(4, 5)
+ # # => [ 1, 2, 3, 4, 5 ]
+ #
+ # ["David", "Rafael"].including %w[ Aaron Todd ]
+ # # => ["David", "Rafael", "Aaron", "Todd"]
+ def including(*elements)
+ to_a.including(*elements)
+ end
+
# The negative of the <tt>Enumerable#include?</tt>. Returns +true+ if the
# collection does not include the object.
def exclude?(object)
!include?(object)
end
- # Returns a copy of the enumerable without the specified elements.
+ # Returns a copy of the enumerable excluding the specified elements.
#
- # ["David", "Rafael", "Aaron", "Todd"].without "Aaron", "Todd"
- # => ["David", "Rafael"]
+ # ["David", "Rafael", "Aaron", "Todd"].excluding "Aaron", "Todd"
+ # # => ["David", "Rafael"]
#
- # {foo: 1, bar: 2, baz: 3}.without :bar
- # => {foo: 1, baz: 3}
- def without(*elements)
+ # ["David", "Rafael", "Aaron", "Todd"].excluding %w[ Aaron Todd ]
+ # # => ["David", "Rafael"]
+ #
+ # {foo: 1, bar: 2, baz: 3}.excluding :bar
+ # # => {foo: 1, baz: 3}
+ def excluding(*elements)
+ elements.flatten!(1)
reject { |element| elements.include?(element) }
end
+ # Alias for #excluding.
+ def without(*elements)
+ excluding(*elements)
+ end
+
# 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 +153,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 463fd78412..9deceb1bb4 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
@@ -17,7 +19,7 @@ class File
# file.write('hello')
# end
def self.atomic_write(file_name, temp_dir = dirname(file_name))
- require 'tempfile' unless defined?(Tempfile)
+ require "tempfile" unless defined?(Tempfile)
Tempfile.open(".#{basename(file_name)}", temp_dir) do |temp_file|
temp_file.binmode
@@ -27,7 +29,7 @@ class File
old_stat = if exist?(file_name)
# Get original file permissions
stat(file_name)
- elsif temp_dir != dirname(file_name)
+ else
# If not possible, probe which are the default permissions in the
# destination directory.
probe_stat_in(dirname(file_name))
@@ -53,11 +55,11 @@ class File
# 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..2f0901d853 100644
--- a/activesupport/lib/active_support/core_ext/hash.rb
+++ b/activesupport/lib/active_support/core_ext/hash.rb
@@ -1,9 +1,10 @@
-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/deep_transform_values"
+require "active_support/core_ext/hash/except"
+require "active_support/core_ext/hash/indifferent_access"
+require "active_support/core_ext/hash/keys"
+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..5cb858af5c 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.5+ (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 6741e732f0..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]
@@ -160,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
@@ -205,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)
@@ -235,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
@@ -258,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/deep_transform_values.rb b/activesupport/lib/active_support/core_ext/hash/deep_transform_values.rb
new file mode 100644
index 0000000000..720a1f67c8
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/hash/deep_transform_values.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+class Hash
+ # Returns a new hash with all keys converted by the block operation.
+ # This includes the keys from the root hash and from all
+ # nested hashes and arrays.
+ #
+ # hash = { person: { name: 'Rob', age: '28' } }
+ #
+ # hash.deep_transform_values{ |value| value.to_s.upcase }
+ # # => {person: {name: "ROB", age: "28"}}
+ def deep_transform_values(&block)
+ _deep_transform_values_in_object(self, &block)
+ end
+
+ # Destructively converts all values by using the block operation.
+ # This includes the values from the root hash and from all
+ # nested hashes and arrays.
+ def deep_transform_values!(&block)
+ _deep_transform_values_in_object!(self, &block)
+ end
+
+ private
+ # support methods for deep transforming nested hashes and arrays
+ def _deep_transform_values_in_object(object, &block)
+ case object
+ when Hash
+ object.transform_values { |value| _deep_transform_values_in_object(value, &block) }
+ when Array
+ object.map { |e| _deep_transform_values_in_object(e, &block) }
+ else
+ yield(object)
+ end
+ end
+
+ def _deep_transform_values_in_object!(object, &block)
+ case object
+ when Hash
+ object.transform_values! { |value| _deep_transform_values_in_object!(value, &block) }
+ when Array
+ object.map! { |e| _deep_transform_values_in_object!(e, &block) }
+ else
+ yield(object)
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb
index 2f6d38c1f6..5013812460 100644
--- a/activesupport/lib/active_support/core_ext/hash/except.rb
+++ b/activesupport/lib/active_support/core_ext/hash/except.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Hash
# Returns a hash that includes everything except given keys.
# hash = { a: true, b: false, c: nil }
@@ -8,7 +10,7 @@ class Hash
# This is useful for limiting a set of parameters to everything but a few known toggles:
# @person.update(params[:person].except(:admin))
def except(*keys)
- dup.except!(*keys)
+ slice(*self.keys - keys)
end
# Removes the given keys from hash and returns it.
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 6df7b4121b..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,7 +1,8 @@
-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
diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb
index 8b2366c4b3..7d3495db2c 100644
--- a/activesupport/lib/active_support/core_ext/hash/keys.rb
+++ b/activesupport/lib/active_support/core_ext/hash/keys.rb
@@ -1,33 +1,6 @@
-class Hash
- # 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"}
- #
- # 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) { size } unless block_given?
- result = self.class.new
- each_key do |key|
- result[yield(key)] = self[key]
- end
- result
- end
-
- # Destructively converts all keys using the +block+ operations.
- # Same as +transform_keys+ but modifies +self+.
- def transform_keys!
- return enum_for(:transform_keys!) { size } unless block_given?
- keys.each do |key|
- self[yield(key)] = delete(key)
- end
- self
- end
+# frozen_string_literal: true
+class Hash
# Returns a new hash with all keys converted to strings.
#
# hash = { name: 'Rob', age: '28' }
@@ -52,14 +25,14 @@ 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!
@@ -128,14 +101,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
@@ -147,7 +120,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
@@ -162,7 +135,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..3d0f8a1e62 100644
--- a/activesupport/lib/active_support/core_ext/hash/slice.rb
+++ b/activesupport/lib/active_support/core_ext/hash/slice.rb
@@ -1,35 +1,13 @@
-class Hash
- # 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
- # passing to a method:
- #
- # def search(criteria = {})
- # criteria.assert_valid_keys(:mass, :velocity, :time)
- # end
- #
- # search(options.slice(:mass, :velocity, :time))
- #
- # If you have an array of keys you want to limit to, you should splat them:
- #
- # 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
+# frozen_string_literal: true
+class Hash
# Replaces the hash with only the given keys.
# Returns a hash containing the removed key/value pairs.
#
- # { a: 1, b: 2, c: 3, d: 4 }.slice!(:a, :b)
- # # => {:c=>3, :d=>4}
+ # hash = { a: 1, b: 2, c: 3, d: 4 }
+ # hash.slice!(:a, :b) # => {:c=>3, :d=>4}
+ # hash # => {:a=>1, :b=>2}
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 7d507ac998..e4aeb0e891 100644
--- a/activesupport/lib/active_support/core_ext/hash/transform_values.rb
+++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
@@ -1,29 +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 }
- #
- # If you do not provide a +block+, it will return an Enumerator
- # for chaining with other methods:
- #
- # { a: 1, b: 2 }.transform_values.with_index { |v, i| [v, i].join.to_i } # => { a: 10, b: 21 }
- def transform_values
- return enum_for(:transform_values) { size } unless block_given?
- return {} if empty?
- result = self.class.new
- each do |key, value|
- result[key] = yield(value)
- end
- result
- end
+# frozen_string_literal: true
- # Destructively converts all values using the +block+ operations.
- # Same as +transform_values+ but modifies +self+.
- def transform_values!
- return enum_for(:transform_values!) { size } unless block_given?
- each do |key, value|
- self[key] = yield(value)
- end
- end
-end
+require "active_support/deprecation"
+
+ActiveSupport::Deprecation.warn "Ruby 2.5+ (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 87185b024f..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.to_i, [[: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..7708069301 100644
--- a/activesupport/lib/active_support/core_ext/kernel.rb
+++ b/activesupport/lib/active_support/core_ext/kernel.rb
@@ -1,4 +1,5 @@
-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/concern"
+require "active_support/core_ext/kernel/reporting"
+require "active_support/core_ext/kernel/singleton_class"
diff --git a/activesupport/lib/active_support/core_ext/kernel/agnostics.rb b/activesupport/lib/active_support/core_ext/kernel/agnostics.rb
deleted file mode 100644
index 64837d87aa..0000000000
--- a/activesupport/lib/active_support/core_ext/kernel/agnostics.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class Object
- # Makes backticks behave (somewhat more) similarly on all platforms.
- # On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the
- # spawned shell prints a message to stderr and sets $?. We emulate
- # Unix on the former but not the latter.
- def `(command) #:nodoc:
- super
- rescue Errno::ENOENT => e
- STDERR.puts "#$0: #{e}"
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/kernel/concern.rb b/activesupport/lib/active_support/core_ext/kernel/concern.rb
index 18bcc01fa4..0b2baed780 100644
--- a/activesupport/lib/active_support/core_ext/kernel/concern.rb
+++ b/activesupport/lib/active_support/core_ext/kernel/concern.rb
@@ -1,4 +1,6 @@
-require 'active_support/core_ext/module/concerning'
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/concerning"
module Kernel
module_function
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 d0197af95f..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 @@
+# frozen_string_literal: true
+
module Kernel
module_function
- # Sets $VERBOSE to nil for the duration of the block and back to its original
+ # 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 60732eb41a..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$/, ''.freeze) == path.sub(/\.rb$/, ''.freeze)
+ 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 ca278cb2fa..0c72cd7b47 100644
--- a/activesupport/lib/active_support/core_ext/marshal.rb
+++ b/activesupport/lib/active_support/core_ext/marshal.rb
@@ -1,9 +1,11 @@
+# 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
loaded = $1.constantize
diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb
index ef038331c2..542af98c04 100644
--- a/activesupport/lib/active_support/core_ext/module.rb
+++ b/activesupport/lib/active_support/core_ext/module.rb
@@ -1,12 +1,13 @@
-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/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/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 510c9a5430..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.
#
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 567ac825e9..cc1926e022 100644
--- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
@@ -1,4 +1,4 @@
-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
@@ -6,7 +6,8 @@ require 'active_support/core_ext/array/extract_options'
class Module
# Defines a class attribute and creates a class and instance reader methods.
# The underlying class variable is set to +nil+, if it is not previously
- # defined.
+ # defined. All class and instance methods created will be public, even if
+ # this method is called with a private or protected access modifier.
#
# module HairColors
# mattr_reader :hair_colors
@@ -23,7 +24,7 @@ class Module
# end
# # => NameError: invalid attribute name: 1_Badname
#
- # If you want to opt out the creation on the instance reader method, pass
+ # To omit the instance reader method, pass
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
#
# module HairColors
@@ -36,13 +37,10 @@ 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
- # mattr_reader :hair_colors do
- # [:brown, :black, :blonde, :red]
- # end
+ # mattr_reader :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
@@ -50,10 +48,9 @@ class Module
# end
#
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
- def mattr_reader(*syms)
- options = syms.extract_options!
+ 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[_A-Za-z]\w*\z/
+ 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
@@ -90,7 +91,7 @@ class Module
# Person.new.hair_colors = [:blonde, :red]
# HairColors.class_variable_get("@@hair_colors") # => [:blonde, :red]
#
- # If you want to opt out the instance writer method, pass
+ # To omit the instance writer method, pass
# <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
#
# module HairColors
@@ -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.
#
# module HairColors
- # mattr_writer :hair_colors do
- # [:brown, :black, :blonde, :red]
- # end
+ # 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[_A-Za-z]\w*\z/
+ 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
@@ -158,14 +160,14 @@ class Module
# parent class. Similarly if parent class changes the value then that would
# change the value of subclasses too.
#
- # class Male < Person
+ # class Citizen < Person
# end
#
- # Male.new.hair_colors << :blue
+ # Citizen.new.hair_colors << :blue
# Person.new.hair_colors # => [:brown, :black, :blonde, :red, :blue]
#
- # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
- # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
+ # To omit the instance writer method, pass <tt>instance_writer: false</tt>.
+ # To omit the instance reader method, pass <tt>instance_reader: false</tt>.
#
# module HairColors
# mattr_accessor :hair_colors, instance_writer: false, instance_reader: false
@@ -178,7 +180,7 @@ class Module
# Person.new.hair_colors = [:brown] # => NoMethodError
# Person.new.hair_colors # => NoMethodError
#
- # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods.
+ # Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
#
# module HairColors
# mattr_accessor :hair_colors, instance_accessor: false
@@ -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)
+ 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
index 0b3d18301e..a6e87aeb68 100644
--- a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb
@@ -1,9 +1,9 @@
-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, 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
@@ -25,7 +25,7 @@ class Module
# end
# # => NameError: invalid attribute name: 1_Badname
#
- # If you want to opt out the creation on the instance reader method, pass
+ # To omit the instance reader method, pass
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
#
# class Current
@@ -33,21 +33,22 @@ class Module
# end
#
# Current.new.user # => NoMethodError
- def thread_mattr_reader(*syms)
- options = syms.extract_options!
-
+ def thread_mattr_reader(*syms, instance_reader: true, instance_accessor: true) # :nodoc:
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-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}"]
+ Thread.current["attr_" + name + "_#{sym}"]
end
EOS
- unless options[:instance_reader] == false || options[:instance_accessor] == false
+ if instance_reader && instance_accessor
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}
- Thread.current[:"attr_#{name}_#{sym}"]
+ self.class.#{sym}
end
EOS
end
@@ -65,7 +66,7 @@ class Module
# Current.user = "DHH"
# Thread.current[:attr_Current_user] # => "DHH"
#
- # If you want to opt out the instance writer method, pass
+ # To omit the instance writer method, pass
# <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
#
# class Current
@@ -73,20 +74,22 @@ class Module
# end
#
# Current.new.user = "DHH" # => NoMethodError
- def thread_mattr_writer(*syms)
- options = syms.extract_options!
+ def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true) # :nodoc:
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-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
+ Thread.current["attr_" + name + "_#{sym}"] = obj
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)
- Thread.current[:"attr_#{name}_#{sym}"] = obj
+ self.class.#{sym} = obj
end
EOS
end
@@ -115,8 +118,8 @@ class Module
# 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>.
+ # To omit the instance writer method, pass <tt>instance_writer: false</tt>.
+ # To omit the instance reader method, pass <tt>instance_reader: false</tt>.
#
# class Current
# thread_mattr_accessor :user, instance_writer: false, instance_reader: false
@@ -125,17 +128,17 @@ class Module
# Current.new.user = "DHH" # => NoMethodError
# Current.new.user # => NoMethodError
#
- # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods.
+ # Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
#
# class Current
- # mattr_accessor :user, instance_accessor: false
+ # thread_mattr_accessor :user, instance_accessor: false
# end
#
# Current.new.user = "DHH" # => NoMethodError
# Current.new.user # => NoMethodError
- def thread_mattr_accessor(*syms, &blk)
- thread_mattr_reader(*syms, &blk)
- thread_mattr_writer(*syms, &blk)
+ 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 0d46248582..5652f2d1cc 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -1,24 +1,29 @@
-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_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
+ else elsif END end ensure false for if in module next nil not or redo rescue retry
+ return self super then true undef unless until when while yield)
+ DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
DELEGATION_RESERVED_METHOD_NAMES = Set.new(
- %w(_ arg args alias and BEGIN begin block break case class def defined? do
- else elsif END end ensure false for if in module next nil not or redo
- rescue retry return self super then true undef unless until when while
- yield)
+ RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
).freeze
# Provides a +delegate+ class method to easily expose contained objects'
# public methods as your own.
#
# ==== Options
- # * <tt>:to</tt> - Specifies the target object
+ # * <tt>:to</tt> - Specifies the target object name as a symbol or string
# * <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
@@ -109,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:
@@ -148,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(':'.freeze, 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 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.
@@ -194,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}")
@@ -209,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
+ # detail.person || 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
+ # detail.person || 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 f3f2e7f5fc..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'
diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb
index fa692e1b0e..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,11 +76,11 @@ class Module
parents
end
- def local_constants #:nodoc:
+ def parents
ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Module#local_constants is deprecated and will be removed in Rails 5.1.
- Use Module#constants(false) instead.
+ `Module#parents` has been renamed to `module_parents`.
+ `parents` is deprecated and will be removed in Rails 6.1.
MSG
- constants(false)
+ 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 3ea39d4267..0000000000
--- a/activesupport/lib/active_support/core_ext/module/qualified_const.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-require 'active_support/core_ext/string/inflections'
-
-#--
-# Allows code reuse in the methods below without polluting Module.
-#++
-
-module ActiveSupport
- 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
-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)
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- Module#qualified_const_defined? is deprecated in favour of the builtin
- Module#const_defined? and will be removed in Rails 5.1.
- MESSAGE
-
- ActiveSupport::QualifiedConstUtils.raise_if_absolute(path)
-
- ActiveSupport::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)
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- Module#qualified_const_get is deprecated in favour of the builtin
- Module#const_get and will be removed in Rails 5.1.
- MESSAGE
-
- ActiveSupport::QualifiedConstUtils.raise_if_absolute(path)
-
- ActiveSupport::QualifiedConstUtils.names(path).inject(self) do |mod, name|
- mod.const_get(name)
- end
- end
-
- def qualified_const_set(path, value)
- ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- Module#qualified_const_set is deprecated in favour of the builtin
- Module#const_set and will be removed in Rails 5.1.
- MESSAGE
-
- ActiveSupport::QualifiedConstUtils.raise_if_absolute(path)
-
- const_name = path.demodulize
- mod_name = path.deconstantize
- mod = mod_name.empty? ? self : 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..2020f5204c 100644
--- a/activesupport/lib/active_support/core_ext/module/reachable.rb
+++ b/activesupport/lib/active_support/core_ext/module/reachable.rb
@@ -1,8 +1,6 @@
-require 'active_support/core_ext/module/anonymous'
-require 'active_support/core_ext/string/inflections'
+# frozen_string_literal: true
-class Module
- def reachable? #:nodoc:
- !anonymous? && name.safe_constantize.equal?(self)
- end
-end
+require "active_support/core_ext/module/anonymous"
+require "active_support/core_ext/string/inflections"
+
+ActiveSupport::Deprecation.warn("reachable is deprecated and will be removed from the framework.")
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 d5ec16d68a..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)
@@ -8,28 +12,6 @@ class Module
# Removes the named singleton method, if it exists.
def remove_possible_singleton_method(method)
- singleton_class.instance_eval do
- remove_possible_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)
- remove_possible_method(method)
- define_method(method, &block)
- send(visibility, method)
- end
-
- def method_visibility(method) # :nodoc:
- case
- when private_method_defined?(method)
- :private
- when protected_method_defined?(method)
- :protected
- else
- :public
- end
+ 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 b25925b9d4..8acad6164a 100644
--- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
@@ -1,139 +1,136 @@
-require 'active_support/core_ext/big_decimal/conversions'
-require 'active_support/number_helper'
-require 'active_support/core_ext/module/deprecation'
+# frozen_string_literal: true
-module ActiveSupport::NumericWithFormat
+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.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(*args)
- format, options = args
- 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
- if is_a?(Float) || format.is_a?(Symbol)
+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
- super
+ super(format)
end
end
end
-
- def to_formatted_s(*args)
- to_s(*args)
- end
- deprecate to_formatted_s: :to_s
end
-[Fixnum, Bignum, Float, BigDecimal].each do |klass|
- klass.prepend(ActiveSupport::NumericWithFormat)
-end
+Integer.prepend ActiveSupport::NumericWithFormat
+Float.prepend ActiveSupport::NumericWithFormat
+BigDecimal.prepend ActiveSupport::NumericWithFormat
diff --git a/activesupport/lib/active_support/core_ext/numeric/inquiry.rb b/activesupport/lib/active_support/core_ext/numeric/inquiry.rb
index 7e7ac1b0b2..6b5240d051 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.5+ (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 039c50a4a2..f36fef6cc9 100644
--- a/activesupport/lib/active_support/core_ext/object/blank.rb
+++ b/activesupport/lib/active_support/core_ext/object/blank.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
+require "concurrent/map"
+
class Object
# An object is blank if it's false, empty, or a whitespace string.
- # For example, +false+, '', ' ', +nil+, [], and {} are all blank.
+ # For example, +nil+, '', ' ', [], {}, and +false+ are all blank.
#
# This simplifies
#
@@ -98,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:
#
@@ -112,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
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 8dfeed0066..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
diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb
index befa5aee21..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:...,'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 d4c17dfb07..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:
@@ -22,6 +24,6 @@ class Object
#
# @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 8c16d95b62..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?
+ 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
@@ -99,7 +109,7 @@ class Object
#
# "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..ea1dc29a76
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/range/compare_range.rb
@@ -0,0 +1,70 @@
+# 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) === (1...6) # => 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.
+ # 1..10 includes 1...11 but it does not include 1...12.
+ operator = exclude_end? && !value.exclude_end? ? :< : :<=
+ value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
+ super(value.first) && value_max.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?(1...6) # => 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.
+ # 1..10 includes 1...11 but it does not include 1...12.
+ operator = exclude_end? && !value.exclude_end? ? :< : :<=
+ value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
+ super(value.first) && value_max.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?(1...6) # => 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.
+ # 1..10 covers 1...11 but it does not cover 1...12.
+ operator = exclude_end? && !value.exclude_end? ? :< : :<=
+ value_max = !exclude_end? && value.exclude_end? ? value.max : value.last
+ super(value.first) && value_max.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 965436c23a..024e32db40 100644
--- a/activesupport/lib/active_support/core_ext/range/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/range/conversions.rb
@@ -1,31 +1,41 @@
-module ActiveSupport::RangeWithFormat
- 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.
- #
- # 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()
+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_formatted_s, :to_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 98cf7430f7..ef812f7e1a 100644
--- a/activesupport/lib/active_support/core_ext/securerandom.rb
+++ b/activesupport/lib/active_support/core_ext/securerandom.rb
@@ -1,18 +1,21 @@
-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"]
+ BASE36_ALPHABET = ("0".."9").to_a + ("a".."z").to_a
+
# SecureRandom.base58 generates a random base58 string.
#
- # The argument _n_ specifies the length, of the random string to be generated.
+ # 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
+ # The result may contain alphanumeric characters except 0, O, I and l.
#
# p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE"
# p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7"
- #
def self.base58(n = 16)
SecureRandom.random_bytes(n).unpack("C*").map do |byte|
idx = byte % 64
@@ -20,4 +23,23 @@ module SecureRandom
BASE58_ALPHABET[idx]
end.join
end
+
+ # SecureRandom.base36 generates a random base36 string in lowercase.
+ #
+ # 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.
+ # This method can be used over +base58+ if a deterministic case key is necessary.
+ #
+ # The result will contain alphanumeric characters in lowercase.
+ #
+ # p SecureRandom.base36 # => "4kugl2pdqmscqtje"
+ # p SecureRandom.base36(24) # => "77tmhrhjfvfdwodq8w7ev2m7"
+ def self.base36(n = 16)
+ SecureRandom.random_bytes(n).unpack("C*").map do |byte|
+ idx = byte % 64
+ idx = SecureRandom.random_number(36) if idx >= 36
+ BASE36_ALPHABET[idx]
+ end.join
+ end
end
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 71612e09fa..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.
@@ -32,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 7277f51076..5eb8d9f99b 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
@@ -153,6 +162,11 @@ class String
# Replaces special characters in a string so that it may be used as part of a 'pretty' URL.
#
+ # If the optional parameter +locale+ is specified,
+ # the word will be parameterized as a word of that language.
+ # By default, this parameter is set to <tt>nil</tt> and it will use
+ # the configured <tt>I18n.locale</tt>.
+ #
# class Person
# def to_param
# "#{id}-#{name.parameterize}"
@@ -164,8 +178,8 @@ class String
#
# <%= link_to(@person.name, person_path) %>
# # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
- #
- # To preserve the case of the characters in a string, use the `preserve_case` argument.
+ #
+ # To preserve the case of the characters in a string, use the +preserve_case+ argument.
#
# class Person
# def to_param
@@ -178,12 +192,8 @@ class String
#
# <%= link_to(@person.name, person_path) %>
# # => <a href="/person/1-Donald-E-Knuth">Donald E. Knuth</a>
- def parameterize(sep = :unused, separator: '-', preserve_case: false)
- unless sep == :unused
- ActiveSupport::Deprecation.warn("Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '#{sep}'` instead.")
- separator = sep
- end
- ActiveSupport::Inflector.parameterize(self, separator: separator, preserve_case: preserve_case)
+ def parameterize(separator: "-", preserve_case: false, locale: nil)
+ ActiveSupport::Inflector.parameterize(self, separator: separator, preserve_case: preserve_case, locale: locale)
end
# Creates the name of a table like Rails does for models to table names. This method
@@ -206,7 +216,7 @@ class String
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.
#
@@ -214,12 +224,17 @@ 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.
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 cc6f2158e7..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,11 +11,14 @@ class String
# encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy
# class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string.
#
- # >> "lj".upcase
- # => "lj"
# >> "lj".mb_chars.upcase.to_s
# => "LJ"
#
+ # NOTE: Ruby 2.4 and later support native Unicode case mappings:
+ #
+ # >> "lj".upcase
+ # => "LJ"
+ #
# == Method chaining
#
# All the methods on the Chars proxy which normally return a string will return a Chars object. This allows
diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb
index 005ad93b08..638152626b 100644
--- a/activesupport/lib/active_support/core_ext/string/output_safety.rb
+++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb
@@ -1,32 +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 = { "&" => "&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.
@@ -134,35 +134,34 @@ 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 lstrip next reverse rstrip slice squeeze strip
+ succ swapcase tr tr_s unicode_normalize upcase
)
+ UNSAFE_STRING_METHODS_WITH_BACKREF = %w(gsub sub)
+
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 html_safe?
+ new_safe_buffer = super
- if new_safe_buffer
- new_safe_buffer.instance_variable_set :@html_safe, true
- end
-
- 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 +170,7 @@ module ActiveSupport #:nodoc:
original_concat(value)
end
- def initialize(str = '')
+ def initialize(str = "")
@html_safe = true
super
end
@@ -190,10 +189,26 @@ 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 []=(*args)
+ if args.count == 3
+ super(args[0], args[1], html_escape_interpolated_argument(args[2]))
+ else
+ super(args[0], html_escape_interpolated_argument(args[1]))
+ end
+ end
+
def +(other)
dup.concat(other)
end
@@ -201,7 +216,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
@@ -240,11 +255,44 @@ module ActiveSupport #:nodoc:
end
end
+ UNSAFE_STRING_METHODS_WITH_BACKREF.each do |unsafe_method|
+ if unsafe_method.respond_to?(unsafe_method)
+ class_eval <<-EOT, __FILE__, __LINE__ + 1
+ def #{unsafe_method}(*args, &block) # def gsub(*args, &block)
+ if block # if block
+ to_str.#{unsafe_method}(*args) { |*params| # to_str.gsub(*args) { |*params|
+ set_block_back_references(block, $~) # set_block_back_references(block, $~)
+ block.call(*params) # block.call(*params)
+ } # }
+ else # else
+ to_str.#{unsafe_method}(*args) # to_str.gsub(*args)
+ end # end
+ end # end
+
+ def #{unsafe_method}!(*args, &block) # def gsub!(*args, &block)
+ @html_safe = false # @html_safe = false
+ if block # if block
+ super(*args) { |*params| # super(*args) { |*params|
+ set_block_back_references(block, $~) # set_block_back_references(block, $~)
+ block.call(*params) # block.call(*params)
+ } # }
+ else # else
+ super # super
+ end # end
+ end # end
+ EOT
+ end
+ end
+
private
- def html_escape_interpolated_argument(arg)
- (!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s)
- end
+ def html_escape_interpolated_argument(arg)
+ (!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s)
+ end
+
+ def set_block_back_references(block, match_data)
+ block.binding.eval("proc { |m| $~ = m }").call(match_data)
+ end
end
end
@@ -252,7 +300,7 @@ class String
# Marks a string as trusted safe. It will be inserted into HTML with no
# additional escaping performed. It is your responsibility to ensure that the
# string contains no malicious content. This method is equivalent to the
- # `raw` helper in views. It is recommended that you use `sanitize` instead of
+ # +raw+ helper in views. It is recommended that you use +sanitize+ instead of
# this method. It should never be called on user input.
def html_safe
ActiveSupport::SafeBuffer.new(self)
diff --git a/activesupport/lib/active_support/core_ext/string/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 55b9b87352..60e9952ee6 100644
--- a/activesupport/lib/active_support/core_ext/string/strip.rb
+++ b/activesupport/lib/active_support/core_ext/string/strip.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class String
# Strips indentation in heredocs.
#
@@ -18,6 +20,8 @@ class String
# Technically, it looks for the least indented non-empty line
# in the whole string, and removes that amount of leading whitespace.
def strip_heredoc
- gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, ''.freeze)
+ gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "").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 768c9a1b2c..f09a6271ad 100644
--- a/activesupport/lib/active_support/core_ext/time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/time/calculations.rb
@@ -1,9 +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'
-require 'active_support/core_ext/date/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
@@ -53,6 +55,29 @@ class Time
end
alias_method :at_without_coercion, :at
alias_method :at, :at_with_coercion
+
+ # Creates a +Time+ instance from an RFC 3339 string.
+ #
+ # Time.rfc3339('1999-12-31T14:00:00-10:00') # => 2000-01-01 00:00:00 -1000
+ #
+ # If the time or offset components are missing then an +ArgumentError+ will be raised.
+ #
+ # Time.rfc3339('1999-12-31') # => ArgumentError: invalid date
+ def rfc3339(str)
+ parts = Date._rfc3339(str)
+
+ raise ArgumentError, "invalid date" if parts.empty?
+
+ Time.new(
+ parts.fetch(:year),
+ parts.fetch(:mon),
+ parts.fetch(:mday),
+ parts.fetch(:hour),
+ parts.fetch(:min),
+ parts.fetch(:sec) + parts.fetch(:sec_fraction, 0),
+ parts.fetch(:offset)
+ )
+ end
end
# Returns the number of seconds since 00:00:00.
@@ -61,7 +86,7 @@ class Time
# 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.
@@ -73,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
@@ -132,9 +170,8 @@ class Time
options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
end
- 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)
+ d = to_date.gregorian.advance(options)
+ 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 +
@@ -162,7 +199,7 @@ class Time
# Returns a new Time 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
@@ -170,7 +207,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
@@ -181,50 +218,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)
@@ -279,5 +311,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 536c4bf525..345cb2832c 100644
--- a/activesupport/lib/active_support/core_ext/time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/time/conversions.rb
@@ -1,24 +1,26 @@
-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',
- :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|
+ 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.
@@ -64,4 +66,7 @@ class Time
def formatted_offset(colon = true, alternate_utc_string = nil)
utc? && alternate_utc_string || ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon)
end
+
+ # Aliased to +xmlschema+ for compatibility with +DateTime+
+ alias_method :rfc3339, :xmlschema
end
diff --git a/activesupport/lib/active_support/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 7a60f94996..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
@@ -53,10 +55,10 @@ class Time
# end
# end
#
- # NOTE: This won't affect any <tt>ActiveSupport::TimeWithZone</tt>
- # objects that have already been created, e.g. any model timestamp
- # attributes that have been read before the block will remain in
- # the application's default timezone.
+ # NOTE: This won't affect any <tt>ActiveSupport::TimeWithZone</tt>
+ # objects that have already been created, e.g. any model timestamp
+ # attributes that have been read before the block will remain in
+ # the application's default timezone.
def use_zone(time_zone)
new_zone = find_zone!(time_zone)
begin
diff --git a/activesupport/lib/active_support/core_ext/uri.rb b/activesupport/lib/active_support/core_ext/uri.rb
index c6c183edd9..cdd81ae562 100644
--- a/activesupport/lib/active_support/core_ext/uri.rb
+++ b/activesupport/lib/active_support/core_ext/uri.rb
@@ -1,16 +1,17 @@
-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
+# frozen_string_literal: true
-unless str == parser.unescape(parser.escape(str))
+require "uri"
+
+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..67ebe102d7
--- /dev/null
+++ b/activesupport/lib/active_support/current_attributes.rb
@@ -0,0 +1,203 @@
+# 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 before #reset is called on the instance. Used for resetting external collaborators that depend on current values.
+ def before_reset(&block)
+ set_callback :reset, :before, &block
+ end
+
+ # Calls this block after #reset is called on the instance. Used for resetting external collaborators, like Time.zone.
+ def resets(&block)
+ set_callback :reset, :after, &block
+ end
+ alias_method :after_reset, :resets
+
+ delegate :set, :reset, to: :instance
+
+ 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 57f6286de3..82f07c085e 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -1,26 +1,26 @@
-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/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:
@@ -47,46 +47,48 @@ module ActiveSupport #:nodoc:
# :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: []
+
+ # This is a private set that collects all eager load paths during bootstrap.
+ # Useful for Zeitwerk integration. Its public interface is the config.* path
+ # accessors of each engine.
+ mattr_accessor :_eager_load_paths, default: Set.new
# 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 used when tracing autoloads.
+ mattr_accessor :logger
+
+ # 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
@@ -94,7 +96,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.
@@ -106,9 +108,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)
@@ -147,7 +151,7 @@ module ActiveSupport #:nodoc:
# Normalize the list of new constants, and add them to the list we will return
new_constants.each do |suffix|
- constants << ([namespace, suffix] - ["Object"]).join("::".freeze)
+ constants << ([namespace, suffix] - ["Object"]).join("::")
end
end
constants
@@ -170,14 +174,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:
@@ -234,6 +237,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>.
@@ -243,7 +248,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}"
@@ -252,9 +257,13 @@ module ActiveSupport #:nodoc:
Dependencies.depend_on(file_name, message)
end
+ # :nodoc:
+
def load_dependency(file)
if Dependencies.load? && Dependencies.constant_watch_stack.watching?
- Dependencies.new_constants_in(Object) { yield }
+ descs = Dependencies.constant_watch_stack.watching.flatten.uniq
+
+ Dependencies.new_constants_in(*descs) { yield }
else
yield
end
@@ -282,17 +291,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.
@@ -351,7 +360,7 @@ module ActiveSupport #:nodoc:
end
def require_or_load(file_name, const_path = nil)
- file_name = $` if file_name =~ /\.rb\z/
+ file_name = file_name.chomp(".rb")
expanded = File.expand_path(file_name)
return if loaded.include?(expanded)
@@ -371,7 +380,7 @@ module ActiveSupport #:nodoc:
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) }
@@ -401,7 +410,7 @@ 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 = []
@@ -410,7 +419,7 @@ module ActiveSupport #:nodoc:
next unless expanded_path.start_with?(expanded_root)
root_size = expanded_root.size
- next if expanded_path[root_size] != ?/.freeze
+ next if expanded_path[root_size] != ?/
nesting = expanded_path[(root_size + 1)..-1]
paths << nesting.camelize unless nesting.blank?
@@ -422,7 +431,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".freeze)
+ path_suffix += ".rb" unless path_suffix.ends_with?(".rb")
autoload_paths.each do |root|
path = File.join(root, path_suffix)
@@ -456,7 +465,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
@@ -496,26 +507,31 @@ module ActiveSupport #:nodoc:
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/, ''.freeze)
+ 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
@@ -547,7 +563,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
@@ -559,6 +575,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!
@@ -591,7 +608,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
@@ -625,7 +642,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?
@@ -675,29 +692,29 @@ module ActiveSupport #:nodoc:
# 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?
@@ -710,7 +727,7 @@ 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
@@ -748,6 +765,10 @@ module ActiveSupport #:nodoc:
# The constant is no longer reachable, just skip it.
end
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 f1865ca2f8..948be75638 100644
--- a/activesupport/lib/active_support/dependencies/interlock.rb
+++ b/activesupport/lib/active_support/dependencies/interlock.rb
@@ -1,4 +1,6 @@
-require 'active_support/concurrency/share_lock'
+# frozen_string_literal: true
+
+require "active_support/concurrency/share_lock"
module ActiveSupport #:nodoc:
module Dependencies #:nodoc:
@@ -46,6 +48,10 @@ module ActiveSupport #:nodoc:
yield
end
end
+
+ def raw_state(&block) # :nodoc:
+ @lock.raw_state(&block)
+ end
end
end
end
diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
new file mode 100644
index 0000000000..a43d03cf09
--- /dev/null
+++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require "set"
+require "active_support/core_ext/string/inflections"
+
+module ActiveSupport
+ module Dependencies
+ module ZeitwerkIntegration # :nodoc: all
+ module Decorations
+ def clear
+ Dependencies.unload_interlock do
+ Rails.autoloaders.main.reload
+ end
+ end
+
+ def constantize(cpath)
+ ActiveSupport::Inflector.constantize(cpath)
+ end
+
+ def safe_constantize(cpath)
+ ActiveSupport::Inflector.safe_constantize(cpath)
+ end
+
+ def autoloaded_constants
+ cpaths = []
+ Rails.autoloaders.each do |autoloader|
+ cpaths.concat(autoloader.loaded_cpaths.to_a)
+ end
+ cpaths
+ end
+
+ def autoloaded?(object)
+ cpath = object.is_a?(Module) ? object.name : object.to_s
+ Rails.autoloaders.any? { |autoloader| autoloader.loaded?(cpath) }
+ end
+
+ def verbose=(verbose)
+ l = verbose ? logger || Rails.logger : nil
+ Rails.autoloaders.each { |autoloader| autoloader.logger = l }
+ end
+
+ def unhook!
+ :no_op
+ end
+ end
+
+ module Inflector
+ def self.camelize(basename, _abspath)
+ basename.camelize
+ end
+ end
+
+ class << self
+ def take_over
+ setup_autoloaders
+ freeze_paths
+ decorate_dependencies
+ end
+
+ private
+
+ def setup_autoloaders
+ Dependencies.autoload_paths.each do |autoload_path|
+ # Zeitwerk only accepts existing directories in `push_dir` to
+ # prevent misconfigurations.
+ next unless File.directory?(autoload_path)
+
+ autoloader = \
+ autoload_once?(autoload_path) ? Rails.autoloaders.once : Rails.autoloaders.main
+
+ autoloader.push_dir(autoload_path)
+ autoloader.do_not_eager_load(autoload_path) unless eager_load?(autoload_path)
+ end
+
+ Rails.autoloaders.each(&:setup)
+ end
+
+ def autoload_once?(autoload_path)
+ Dependencies.autoload_once_paths.include?(autoload_path)
+ end
+
+ def eager_load?(autoload_path)
+ Dependencies._eager_load_paths.member?(autoload_path)
+ end
+
+ def freeze_paths
+ Dependencies.autoload_paths.freeze
+ Dependencies.autoload_once_paths.freeze
+ Dependencies._eager_load_paths.freeze
+ end
+
+ def decorate_dependencies
+ Dependencies.unhook!
+ Dependencies.singleton_class.prepend(Decorations)
+ Object.class_eval { alias_method :require_dependency, :require }
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb
index 24545d766c..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.1', 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 dc24e2d0e1..725667d139 100644
--- a/activesupport/lib/active_support/deprecation/behaviors.rb
+++ b/activesupport/lib/active_support/deprecation/behaviors.rb
@@ -1,43 +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 behaviour to raise errors and proactively report exceptions from deprecations.
+ # 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.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.
@@ -79,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 f5ea6669ce..d99571790f 100644
--- a/activesupport/lib/active_support/deprecation/method_wrappers.rb
+++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb
@@ -1,14 +1,13 @@
-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
- #
+ # class Fred
# def aaa; end
# def bbb; end
# def ccc; end
@@ -18,17 +17,17 @@ module ActiveSupport
#
# Using the default deprecator:
# ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead')
- # # => [:aaa, :bbb, :ccc]
+ # # => Fred
#
- # Fred.aaa
+ # Fred.new.aaa
# # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.1. (called from irb_binding at (irb):10)
# # => nil
#
- # Fred.bbb
+ # Fred.new.bbb
# # DEPRECATION WARNING: bbb is deprecated and will be removed from Rails 5.1 (use zzz instead). (called from irb_binding at (irb):11)
# # => nil
#
- # Fred.ccc
+ # Fred.new.ccc
# # DEPRECATION WARNING: ccc is deprecated and will be removed from Rails 5.1 (use Bar#ccc instead). (called from irb_binding at (irb):12)
# # => nil
#
@@ -37,7 +36,7 @@ module ActiveSupport
# ActiveSupport::Deprecation.deprecate_methods(Fred, ddd: :zzz, deprecator: custom_deprecator)
# # => [:ddd]
#
- # Fred.ddd
+ # Fred.new.ddd
# DEPRECATION WARNING: ddd is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):15)
# # => nil
#
@@ -46,24 +45,44 @@ module ActiveSupport
# custom_deprecator.deprecate_methods(Fred, eee: :zzz)
# # => [:eee]
#
- # Fred.eee
+ # Fred.new.eee
# DEPRECATION WARNING: eee is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):18)
# # => nil
def deprecate_methods(target_module, *method_names)
options = method_names.extract_options!
deprecator = options.delete(:deprecator) || self
method_names += options.keys
+ mod = Module.new
+
+ method_names.each do |method_name|
+ if target_module.method_defined?(method_name) || target_module.private_method_defined?(method_name)
+ 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.define_method(with_method) do |*args, &block|
+ deprecator.deprecation_warning(method_name, options[method_name])
+ send(without_method, *args, &block)
+ end
- mod = Module.new do
- method_names.each do |method_name|
- define_method(method_name) do |*args, &block|
+ target_module.alias_method(without_method, method_name)
+ target_module.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
+ else
+ mod.define_method(method_name) do |*args, &block|
deprecator.deprecation_warning(method_name, options[method_name])
super(*args, &block)
end
end
end
- target_module.prepend(mod)
+ target_module.prepend(mod) unless mod.instance_methods(false).empty?
end
end
end
diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
index 0cb2d4d22e..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.
@@ -111,7 +111,7 @@ module ActiveSupport
#
# PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto)
#
- # (In a later update, the original implementation of `PLANETS` has been removed.)
+ # # (In a later update, the original implementation of `PLANETS` has been removed.)
#
# PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
# PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006')
@@ -121,10 +121,13 @@ module ActiveSupport
# (Backtrace information…)
# ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
class DeprecatedConstantProxy < DeprecationProxy
- def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance)
+ def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance, message: "#{old_const} is deprecated! Use #{new_const} instead.")
+ 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 35f084dd7a..7075b5b869 100644
--- a/activesupport/lib/active_support/deprecation/reporting.rb
+++ b/activesupport/lib/active_support/deprecation/reporting.rb
@@ -1,4 +1,6 @@
-require 'rbconfig'
+# frozen_string_literal: true
+
+require "rbconfig"
module ActiveSupport
class Deprecation
@@ -18,7 +20,7 @@ module ActiveSupport
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
@@ -48,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
@@ -103,10 +104,10 @@ module ActiveSupport
end
end
- RAILS_GEM_ROOT = File.expand_path("../../../../..", __FILE__) + "/"
+ RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/"
def ignored_callstack(path)
- path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG['rubylibdir'])
+ path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG["rubylibdir"])
end
end
end
diff --git a/activesupport/lib/active_support/descendants_tracker.rb b/activesupport/lib/active_support/descendants_tracker.rb
index 27861e01d0..2dca990712 100644
--- a/activesupport/lib/active_support/descendants_tracker.rb
+++ b/activesupport/lib/active_support/descendants_tracker.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require "weakref"
+
module ActiveSupport
# This module provides an internal implementation to track descendants
# which is faster than iterating through ObjectSpace.
@@ -6,7 +10,8 @@ module ActiveSupport
class << self
def direct_descendants(klass)
- @@direct_descendants[klass] || []
+ descendants = @@direct_descendants[klass]
+ descendants ? descendants.to_a : []
end
def descendants(klass)
@@ -32,16 +37,19 @@ module ActiveSupport
# This is the only method that is not thread safe, but is only ever called
# during the eager loading phase.
def store_inherited(klass, descendant)
- (@@direct_descendants[klass] ||= []) << descendant
+ (@@direct_descendants[klass] ||= DescendantsArray.new) << descendant
end
private
- def accumulate_descendants(klass, acc)
- if direct_descendants = @@direct_descendants[klass]
- acc.concat(direct_descendants)
- direct_descendants.each { |direct_descendant| accumulate_descendants(direct_descendant, acc) }
+
+ def accumulate_descendants(klass, acc)
+ if direct_descendants = @@direct_descendants[klass]
+ direct_descendants.each do |direct_descendant|
+ acc << direct_descendant
+ accumulate_descendants(direct_descendant, acc)
+ end
+ end
end
- end
end
def inherited(base)
@@ -56,5 +64,46 @@ module ActiveSupport
def descendants
DescendantsTracker.descendants(self)
end
+
+ # DescendantsArray is an array that contains weak references to classes.
+ class DescendantsArray # :nodoc:
+ include Enumerable
+
+ def initialize
+ @refs = []
+ end
+
+ def initialize_copy(orig)
+ @refs = @refs.dup
+ end
+
+ def <<(klass)
+ cleanup!
+ @refs << WeakRef.new(klass)
+ end
+
+ def each
+ @refs.each do |ref|
+ yield ref.__getobj__
+ rescue WeakRef::RefError
+ end
+ end
+
+ def refs_size
+ @refs.size
+ end
+
+ def cleanup!
+ @refs.delete_if { |ref| !ref.weakref_alive? }
+ end
+
+ def reject!
+ @refs.reject! do |ref|
+ yield ref.__getobj__
+ rescue WeakRef::RefError
+ true
+ end
+ end
+ end
end
end
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..97b4634d7b 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,240 @@ 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:
+ case other
+ when Scalar
+ [other, self]
+ when Duration
+ [Scalar.new(other.value), 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 +255,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 +329,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 +356,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 +370,14 @@ 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}"}.
+ 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 +385,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 +419,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..2b7db568a5
--- /dev/null
+++ b/activesupport/lib/active_support/encrypted_file.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require "pathname"
+require "tmpdir"
+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
index 6a02a838b7..3893b0de0e 100644
--- a/activesupport/lib/active_support/evented_file_update_checker.rb
+++ b/activesupport/lib/active_support/evented_file_update_checker.rb
@@ -1,10 +1,43 @@
-require 'set'
-require 'pathname'
-require 'concurrent/atomic/atomic_boolean'
+# 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
@@ -13,26 +46,49 @@ module ActiveSupport
@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)
+ @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?
+ dtw = directories_to_watch
+ @dtw, @missing = dtw.partition(&:exist?)
+
+ if @dtw.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
+ 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
- Listen.to(*dtw, &method(:changed)).start
end
+ boot!
end
def updated?
+ @boot_mutex.synchronize do
+ if @pid != Process.pid
+ boot!
+ @pid = Process.pid
+ @updated.make_true
+ end
+ end
+
+ if @missing.any?(&:exist?)
+ @boot_mutex.synchronize do
+ appeared, @missing = @missing.partition(&:exist?)
+ shutdown!
+
+ @dtw += appeared
+ boot!
+
+ @updated.make_true
+ end
+ end
+
@updated.true?
end
@@ -50,6 +106,13 @@ module ActiveSupport
end
private
+ def boot!
+ Listen.to(*@dtw, &method(:changed)).start
+ end
+
+ def shutdown!
+ Listen.stop
+ end
def changed(modified, added, removed)
unless updated?
@@ -68,7 +131,9 @@ module ActiveSupport
ext = @ph.normalize_extension(file.extname)
file.dirname.ascend do |dir|
- if @dirs.fetch(dir, []).include?(ext)
+ matching = @dirs[dir]
+
+ if matching && (matching.empty? || matching.include?(ext))
break true
elsif dir == @lcsp || dir.root?
break false
@@ -78,80 +143,83 @@ module ActiveSupport
end
def directories_to_watch
- dtw = (@files + @dirs.keys).map { |f| @ph.existing_parent(f) }
+ dtw = @files.map(&:dirname) + @dirs.keys
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
- using Module.new {
- refine Pathname do
- def ascendant_of?(other)
- self != other && other.ascend do |ascendant|
- break true if self == ascendant
- end
- end
+ class PathHelper
+ def xpath(path)
+ Pathname.new(path).expand_path
end
- }
-
- def xpath(path)
- Pathname.new(path).expand_path
- end
- def normalize_extension(ext)
- ext.to_s.sub(/\A\./, '')
- 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 lcsp.ascendant_of?(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
+ # 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
- end
- lcsp
- 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?
+ # 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
- end
- # Filters out directories which are descendants of others in the collection (stable).
- def filter_out_descendants(dirs)
- return dirs if dirs.length < 2
+ # 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 = []
+ 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
+ until dirs_sorted_by_nparts.empty?
+ dir = dirs_sorted_by_nparts.shift
- dirs_sorted_by_nparts.reject! do |possible_descendant|
- dir.ascendant_of?(possible_descendant) && descendants << possible_descendant
+ dirs_sorted_by_nparts.reject! do |possible_descendant|
+ ascendant_of?(dir, possible_descendant) && descendants << possible_descendant
+ end
end
+
+ # Array#- preserves order.
+ dirs - descendants
end
- # Array#- preserves order.
- dirs - descendants
+ private
+
+ def ascendant_of?(base, other)
+ base != other && other.ascend do |ascendant|
+ break true if base == ascendant
+ end
+ end
end
- end
end
end
diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb
index 2bd1c01d35..ca810db584 100644
--- a/activesupport/lib/active_support/execution_wrapper.rb
+++ b/activesupport/lib/active_support/execution_wrapper.rb
@@ -1,4 +1,7 @@
-require 'active_support/callbacks'
+# frozen_string_literal: true
+
+require "active_support/callbacks"
+require "concurrent/hash"
module ActiveSupport
class ExecutionWrapper
@@ -19,6 +22,41 @@ module ActiveSupport
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
@@ -29,7 +67,15 @@ module ActiveSupport
if active?
Null
else
- new.tap(&:run!)
+ new.tap do |instance|
+ success = nil
+ begin
+ instance.run!
+ success = true
+ ensure
+ instance.complete! unless success
+ end
+ end
end
end
@@ -37,11 +83,11 @@ module ActiveSupport
def self.wrap
return yield if active?
- state = run!
+ instance = run!
begin
yield
ensure
- state.complete!
+ instance.complete!
end
end
@@ -74,5 +120,10 @@ module ActiveSupport
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
index 602fb11a44..ce391b07ec 100644
--- a/activesupport/lib/active_support/executor.rb
+++ b/activesupport/lib/active_support/executor.rb
@@ -1,4 +1,6 @@
-require 'active_support/execution_wrapper'
+# frozen_string_literal: true
+
+require "active_support/execution_wrapper"
module ActiveSupport
class Executor < ExecutionWrapper
diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb
index 8708a502e6..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:
@@ -36,6 +40,10 @@ module ActiveSupport
# changes. The array of files and list of directories cannot be changed
# after FileUpdateChecker has been initialized.
def initialize(files, dirs = {}, &block)
+ unless block
+ raise ArgumentError, "A block is required to initialize a FileUpdateChecker"
+ end
+
@files = files.freeze
@glob = compile_glob(dirs)
@block = block
@@ -91,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 4166ffc2fb..cf17922d7d 100644
--- a/activesupport/lib/active_support/gem_version.rb
+++ b/activesupport/lib/active_support/gem_version.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module ActiveSupport
# Returns the version of the currently loaded Active Support as a <tt>Gem::Version</tt>.
def self.gem_version
@@ -5,7 +7,7 @@ module ActiveSupport
end
module VERSION
- MAJOR = 5
+ MAJOR = 6
MINOR = 0
TINY = 0
PRE = "beta3"
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 03770a197c..42ae7e9b7b 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.
@@ -68,25 +77,6 @@ module ActiveSupport
end
end
- def default(*args)
- arg_key = args.first
-
- if include?(key = convert_key(arg_key))
- self[key]
- else
- super
- end
- end
-
- def self.new_from_hash_copying_default(hash)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- `ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default`
- has been deprecated, and will be removed in Rails 5.1. The behavior of
- this method is now identical to the behavior of `.new`.
- MSG
- new(hash)
- end
-
def self.[](*args)
new.merge!(Hash[*args])
end
@@ -161,7 +151,6 @@ 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:
#
@@ -175,6 +164,19 @@ module ActiveSupport
super(convert_key(key))
end
+ # Same as <tt>Hash#assoc</tt> where the key passed as argument can be
+ # either a string or a symbol:
+ #
+ # counters = ActiveSupport::HashWithIndifferentAccess.new
+ # counters[:foo] = 1
+ #
+ # counters.assoc('foo') # => ["foo", 1]
+ # counters.assoc(:foo) # => ["foo", 1]
+ # counters.assoc(:zoo) # => nil
+ def assoc(key)
+ super(convert_key(key))
+ end
+
# Same as <tt>Hash#fetch</tt> where the key passed as argument can be
# either a string or a symbol:
#
@@ -189,14 +191,55 @@ 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
# hash[:a] = 'x'
# hash[:b] = 'y'
# hash.values_at('a', 'b') # => ["x", "y"]
- def values_at(*indices)
- indices.collect { |key| self[convert_key(key)] }
+ def values_at(*keys)
+ super(*keys.map { |key| 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)
+ super(*indices.map { |key| convert_key(key) }, &block)
end
# Returns a shallow copy of the hash.
@@ -205,7 +248,7 @@ module ActiveSupport
# 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|
@@ -217,7 +260,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
@@ -229,11 +272,13 @@ module ActiveSupport
def reverse_merge(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.
#
@@ -248,6 +293,11 @@ module ActiveSupport
super(convert_key(key))
end
+ def except(*keys)
+ slice(*self.keys - keys.map { |key| convert_key(key) })
+ end
+ alias_method :without, :except
+
def stringify_keys!; self end
def deep_stringify_keys!; self end
def stringify_keys; dup end
@@ -255,6 +305,7 @@ 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
@@ -268,6 +319,38 @@ module ActiveSupport
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
@@ -279,12 +362,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
@@ -301,7 +384,7 @@ module ActiveSupport
end
end
- def set_defaults(target)
+ def set_defaults(target) # :doc:
if default_proc
target.default_proc = default_proc.dup
else
@@ -311,4 +394,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 6cc7c90c12..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.
@@ -42,7 +43,7 @@ module I18n
case setting
when :railties_load_path
reloadable_paths = value
- app.config.i18n.load_path.unshift(*value.map(&:existent).flatten)
+ app.config.i18n.load_path.unshift(*value.flat_map(&:existent))
when :load_path
I18n.load_path += value
else
@@ -58,7 +59,7 @@ module I18n
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.map(&:existent).flatten
+ I18n.load_path |= reloadable_paths.flat_map(&:existent)
I18n.reload!
end
@@ -66,10 +67,6 @@ module I18n
app.reloaders << reloader
app.reloader.to_run do
reloader.execute_if_updated { require_unload_lock! }
- # TODO: remove the following line as soon as the return value of
- # callbacks is ignored, that is, returning `false` does not
- # display a deprecation warning or halts the callback chain.
- true
end
reloader.execute
@@ -83,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)
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 f3e52b48ac..88cdd99dbd 100644
--- a/activesupport/lib/active_support/inflector/inflections.rb
+++ b/activesupport/lib/active_support/inflector/inflections.rb
@@ -1,6 +1,8 @@
-require 'concurrent/map'
-require 'active_support/core_ext/array/prepend_and_append'
-require 'active_support/i18n'
+# frozen_string_literal: true
+
+require "concurrent/map"
+require "active_support/i18n"
+require "active_support/deprecation"
module ActiveSupport
module Inflector
@@ -43,13 +45,14 @@ module ActiveSupport
end
def add(words)
- self.concat(words.flatten.map(&:downcase))
- @regex_array += self.map {|word| to_regex(word) }
+ words = words.flatten.map(&:downcase)
+ concat(words)
+ @regex_array += words.map { |word| to_regex(word) }
self
end
def uncountable?(str)
- @regex_array.any? { |regex| regex === str }
+ @regex_array.any? { |regex| regex.match? str }
end
private
@@ -62,17 +65,21 @@ module ActiveSupport
@__instance__[locale] ||= new
end
- attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex
+ attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms
+
+ attr_reader :acronyms_camelize_regex, :acronyms_underscore_regex # :nodoc:
def initialize
- @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], Uncountables.new, [], {}, /(?=a)b/
+ @plurals, @singulars, @uncountables, @humans, @acronyms = [], [], Uncountables.new, [], {}
+ define_acronym_regex_patterns
end
# Private, for the test suite.
def initialize_dup(orig) # :nodoc:
- %w(plurals singulars uncountables humans acronyms acronym_regex).each do |scope|
+ %w(plurals singulars uncountables humans acronyms).each do |scope|
instance_variable_set("@#{scope}", orig.send(scope).dup)
end
+ define_acronym_regex_patterns
end
# Specifies a new acronym. An acronym must be specified as it will appear
@@ -126,7 +133,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
@@ -215,12 +222,20 @@ module ActiveSupport
# clear :plurals
def clear(scope = :all)
case scope
- when :all
- @plurals, @singulars, @uncountables, @humans = [], [], Uncountables.new, []
- 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 f94e12e14f..ee193add6f 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -1,4 +1,6 @@
-require 'active_support/inflections'
+# frozen_string_literal: true
+
+require "active_support/inflections"
module ActiveSupport
# The Inflector transforms words from singular to plural, class names to table
@@ -27,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
@@ -44,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.
@@ -68,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
@@ -87,12 +89,12 @@ 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 && '_'.freeze }#{$2.downcase}" }
- word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
- word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze)
- word.tr!("-".freeze, "_".freeze)
+ 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
end
@@ -107,33 +109,38 @@ 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_+/, ''.freeze)
- result.sub!(/_id\z/, ''.freeze)
- result.tr!('_'.freeze, ' '.freeze)
+ 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
@@ -146,21 +153,28 @@ module ActiveSupport
# upcase_first('w') # => "W"
# upcase_first('') # => ""
def upcase_first(string)
- string.length > 0 ? string[0].upcase.concat(string[1..-1]) : ''
+ 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.
@@ -185,28 +199,28 @@ module ActiveSupport
# classify('calculus') # => "Calculus"
def classify(table_name)
# strip out any leading schema name
- camelize(singularize(table_name.to_s.sub(/.*\./, ''.freeze)))
+ 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('_'.freeze, '-'.freeze)
+ 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
@@ -222,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.
@@ -238,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
@@ -248,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('::'.freeze)
+ 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?
@@ -273,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
@@ -313,7 +327,9 @@ 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)
+ rescue LoadError => e
+ raise unless /Unable to autoload constant #{const_regexp(camel_cased_word)}/.match?(e.message)
end
# Returns the suffix that should be added to a number to denote the position
@@ -326,18 +342,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
@@ -350,41 +355,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("::".freeze)
+ # 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.uncountable?(result)
- 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 871cfb8a72..ec6e9ccb59 100644
--- a/activesupport/lib/active_support/inflector/transliterate.rb
+++ b/activesupport/lib/active_support/inflector/transliterate.rb
@@ -1,9 +1,10 @@
-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 "?".
#
@@ -50,48 +51,56 @@ module ActiveSupport
#
# Now you can have different transliterations for each locale:
#
- # I18n.locale = :en
- # transliterate('Jürgen')
+ # transliterate('Jürgen', locale: :en)
# # => "Jurgen"
#
- # I18n.locale = :de
- # transliterate('Jürgen')
+ # transliterate('Jürgen', locale: :de)
# # => "Juergen"
- def transliterate(string, replacement = "?".freeze)
- I18n.transliterate(ActiveSupport::Multibyte::Unicode.normalize(
- ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c),
- :replacement => replacement)
+ def transliterate(string, replacement = "?", locale: nil)
+ 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,
+ locale: locale
+ )
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"
+ # parameterize("^très|Jolie-- ") # => "tres-jolie"
#
- # To use a custom separator, override the `separator` argument.
+ # To use a custom separator, override the +separator+ argument.
#
- # parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth"
- # parameterize("^trés|Jolie-- ", separator: '_') # => "tres_jolie"
+ # parameterize("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.
+ # To preserve the case of the characters in a string, use the +preserve_case+ argument.
#
# parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth"
- # parameterize("^trés|Jolie-- ", preserve_case: true) # => "tres-Jolie"
+ # parameterize("^très|Jolie-- ", preserve_case: true) # => "tres-Jolie"
#
- def parameterize(string, sep = :unused, separator: '-', preserve_case: false)
- unless sep == :unused
- ActiveSupport::Deprecation.warn("Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '#{sep}'` instead.")
- separator = sep
- end
+ # 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--"
+ #
+ # If the optional parameter +locale+ is specified,
+ # the word will be parameterized as a word of that language.
+ # By default, this parameter is set to <tt>nil</tt> and it will use
+ # the configured <tt>I18n.locale<tt>.
+ def parameterize(string, separator: "-", preserve_case: false, locale: nil)
# Replace accented chars with their ASCII equivalents.
- parameterized_string = transliterate(string)
+ parameterized_string = transliterate(string, locale: locale)
# Turn unwanted chars into the separator.
parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator)
unless separator.nil? || separator.empty?
- if separator == "-".freeze
+ if separator == "-"
re_duplicate_separator = /-{2,}/
re_leading_trailing_separator = /^-|-$/i
else
@@ -102,9 +111,9 @@ module ActiveSupport
# No more than one of the separator in a row.
parameterized_string.gsub!(re_duplicate_separator, separator)
# Remove leading/trailing separator.
- parameterized_string.gsub!(re_leading_trailing_separator, ''.freeze)
+ parameterized_string.gsub!(re_leading_trailing_separator, "")
end
-
+
parameterized_string.downcase! unless preserve_case
parameterized_string
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..402a3fbe60 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.
@@ -42,26 +45,32 @@ module ActiveSupport
private
- def convert_dates_from(data)
- case data
- when nil
- nil
- when DATE_REGEX
- begin
- DateTime.parse(data)
- rescue ArgumentError
+ def convert_dates_from(data)
+ case data
+ when nil
+ nil
+ when DATE_REGEX
+ begin
+ Date.parse(data)
+ rescue ArgumentError
+ data
+ end
+ when DATETIME_REGEX
+ begin
+ Time.zone.parse(data)
+ rescue ArgumentError
+ data
+ end
+ when Array
+ data.map! { |d| convert_dates_from(d) }
+ when Hash
+ data.each do |key, value|
+ data[key] = convert_dates_from(value)
+ end
+ else
data
end
- when Array
- data.map! { |d| convert_dates_from(d) }
- when Hash
- data.each do |key, value|
- data[key] = convert_dates_from(value)
- end
- else
- data
end
- end
end
end
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 7f73f9ddfc..8b61982883 100644
--- a/activesupport/lib/active_support/key_generator.rb
+++ b/activesupport/lib/active_support/key_generator.rb
@@ -1,5 +1,7 @@
-require 'concurrent/map'
-require 'openssl'
+# frozen_string_literal: true
+
+require "concurrent/map"
+require "openssl"
module ActiveSupport
# KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2.
@@ -17,7 +19,7 @@ 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
@@ -31,43 +33,9 @@ module ActiveSupport
@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)
- end
- end
-
- class LegacyKeyGenerator # :nodoc:
- SECRET_MIN_LENGTH = 30 # Characters
-
- def initialize(secret)
- ensure_secret_secure(secret)
- @secret = secret
- end
-
- def generate_key(salt)
- @secret
- end
-
- 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
-
- 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
+ # Returns a derived key suitable for use.
+ def generate_key(*args)
+ @cache_keys[args.join] ||= @key_generator.generate_key(*args)
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/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb
index e782cd2d4b..938cfdb914 100644
--- a/activesupport/lib/active_support/log_subscriber.rb
+++ b/activesupport/lib/active_support/log_subscriber.rb
@@ -1,10 +1,12 @@
-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
- # ActiveSupport::Notifications with the sole purpose of logging them.
+ # <tt>ActiveSupport::LogSubscriber</tt> is an object set to consume
+ # <tt>ActiveSupport::Notifications</tt> with the sole purpose of logging them.
# The log subscriber dispatches notifications to a registered object based
# on its given namespace.
#
@@ -14,7 +16,7 @@ module ActiveSupport
# module ActiveRecord
# class LogSubscriber < ActiveSupport::LogSubscriber
# def sql(event)
- # "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}"
+ # info "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}"
# end
# end
# end
@@ -27,13 +29,36 @@ module ActiveSupport
# subscriber, the line above should be called after your
# <tt>ActiveRecord::LogSubscriber</tt> definition.
#
- # After configured, whenever a "sql.active_record" notification is published,
- # it will properly dispatch the event (ActiveSupport::Notifications::Event) to
- # the sql method.
+ # After configured, whenever a <tt>"sql.active_record"</tt> notification is published,
+ # it will properly dispatch the event
+ # (<tt>ActiveSupport::Notifications::Event</tt>) to the sql method.
+ #
+ # Being an <tt>ActiveSupport::Notifications</tt> consumer,
+ # <tt>ActiveSupport::LogSubscriber</tt> exposes a simple interface to check if
+ # instrumented code raises an exception. It is common to log a different
+ # message in case of an error, and this can be achieved by extending
+ # the previous example:
+ #
+ # module ActiveRecord
+ # class LogSubscriber < ActiveSupport::LogSubscriber
+ # def sql(event)
+ # exception = event.payload[:exception]
+ #
+ # if exception
+ # exception_object = event.payload[:exception_object]
+ #
+ # error "[ERROR] #{event.payload[:name]}: #{exception.join(', ')} " \
+ # "(#{exception_object.backtrace.first})"
+ # else
+ # # standard logger code
+ # end
+ # end
+ # end
+ # end
#
# Log subscriber also has some helpers to deal with logging and automatically
- # flushes all logs when the request finishes (via action_dispatch.callback
- # notification) in a Rails environment.
+ # flushes all logs when the request finishes
+ # (via <tt>action_dispatch.callback</tt> notification) in a Rails environment.
class LogSubscriber < Subscriber
# Embed in a String to clear all previous ANSI sequences.
CLEAR = "\e[0m"
@@ -49,8 +74,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 +105,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
@@ -99,7 +125,7 @@ module ActiveSupport
# option is set to +true+, it also adds bold to the string. This is based
# on the Highline implementation and will automatically append CLEAR to the
# end of the returned String.
- def color(text, color, bold=false)
+ def color(text, color, bold = false) # :doc:
return text unless colorize_logging
color = self.class.const_get(color.upcase) if color.is_a?(Symbol)
bold = bold ? BOLD : ""
diff --git a/activesupport/lib/active_support/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb
index 588ed67c81..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
@@ -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 de48e717b6..b8555c887b 100644
--- a/activesupport/lib/active_support/logger.rb
+++ b/activesupport/lib/active_support/logger.rb
@@ -1,10 +1,11 @@
-require 'active_support/logger_silence'
-require 'active_support/logger_thread_safe_level'
-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 ActiveSupport::LoggerThreadSafeLevel
include LoggerSilence
# Returns true if the logger destination matches one of the sources
@@ -55,26 +56,30 @@ module ActiveSupport
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
def initialize(*args)
super
@formatter = SimpleFormatter.new
- after_initialize if respond_to? :after_initialize
- end
-
- def add(severity, message = nil, progname = nil, &block)
- return true if @logdev.nil? || (severity || UNKNOWN) < level
- super
- 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
# Simple formatter which only displays the message.
diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb
index 3eb8098c77..b2444c1e34 100644
--- a/activesupport/lib/active_support/logger_silence.rb
+++ b/activesupport/lib/active_support/logger_silence.rb
@@ -1,28 +1,45 @@
-require 'active_support/concern'
-require 'active_support/core_ext/module/attribute_accessors'
-require 'concurrent'
+# 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_local_level = local_level
+ self.local_level = temporary_level
+ yield self
+ ensure
+ self.local_level = old_local_level
+ end
+ else
yield self
- ensure
- self.local_level = old_local_level
end
- else
- yield self
end
end
end
diff --git a/activesupport/lib/active_support/logger_thread_safe_level.rb b/activesupport/lib/active_support/logger_thread_safe_level.rb
index 5fedb5e689..f16c90cfc6 100644
--- a/activesupport/lib/active_support/logger_thread_safe_level.rb
+++ b/activesupport/lib/active_support/logger_thread_safe_level.rb
@@ -1,11 +1,30 @@
-require 'active_support/concern'
+# 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
- @local_levels = Concurrent::Map.new(initial_capacity: 2)
+ 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
@@ -13,19 +32,24 @@ module ActiveSupport
end
def local_level
- @local_levels[local_log_id]
+ self.class.local_levels[local_log_id]
end
def local_level=(level)
if level
- @local_levels[local_log_id] = level
+ self.class.local_levels[local_log_id] = level
else
- @local_levels.delete(local_log_id)
+ 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 2dde01c844..7d6f8937f0 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 up to 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 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("--".freeze).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 4c3deffe6e..c4a4afe95f 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
@@ -27,16 +31,82 @@ module ActiveSupport
#
# +MessageVerifier+ creates HMAC signatures using SHA1 hash algorithm by default.
# If you want to use a different hash algorithm, you can change it by providing
- # `:digest` key as an option while initializing the verifier:
+ # +:digest+ key as an option while initializing the verifier:
#
# @verifier = ActiveSupport::MessageVerifier.new('s3Krit', digest: 'SHA256')
+ #
+ # === 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 up to 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
@@ -52,7 +122,7 @@ module ActiveSupport
def valid_message?(signed_message)
return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank?
- data, digest = signed_message.split("--".freeze)
+ data, digest = signed_message.split("--")
data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
end
@@ -77,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("--".freeze)[0]
- @serializer.load(decode(data))
+ data = signed_message.split("--")[0]
+ 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
@@ -101,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.
@@ -112,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
@@ -127,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 707cf200b5..a1e23aeaca 100644
--- a/activesupport/lib/active_support/multibyte/chars.rb
+++ b/activesupport/lib/active_support/multibyte/chars.rb
@@ -1,7 +1,9 @@
-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:
@@ -15,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
@@ -45,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)
@@ -56,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
@@ -73,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
@@ -86,7 +94,7 @@ module ActiveSupport #:nodoc:
end
# Works like <tt>String#slice!</tt>, but returns an instance of
- # Chars, or nil if the string was not modified. The string will not be
+ # 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'
@@ -105,7 +113,7 @@ module ActiveSupport #:nodoc:
#
# '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
@@ -114,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
@@ -162,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.
@@ -170,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.
@@ -178,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.
@@ -186,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
@@ -202,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 72b20fff06..ce8ecece69 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -1,16 +1,24 @@
+# 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 = '8.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
@@ -20,219 +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*'.freeze }.join('|'.freeze)
- 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]
-
- should_break =
- # GB3. CR X LF
- if previous == database.boundary[:cr] and current == database.boundary[:lf]
- false
- # GB4. (Control|CR|LF) ÷
- elsif previous and in_char_class?(previous, [:control,:cr,:lf])
- true
- # GB5. ÷ (Control|CR|LF)
- elsif in_char_class?(current, [:control,:cr,:lf])
- true
- # GB6. L X (L|V|LV|LVT)
- elsif database.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt])
- false
- # GB7. (LV|V) X (V|T)
- elsif in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t])
- false
- # GB8. (LVT|T) X (T)
- elsif in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current
- false
- # GB8a. Regional_Indicator X Regional_Indicator
- elsif database.boundary[:regional_indicator] === previous and database.boundary[:regional_indicator] === current
- false
- # GB9. X Extend
- elsif database.boundary[:extend] === current
- false
- # GB9a. X SpacingMark
- elsif database.boundary[:spacingmark] === current
- false
- # GB9b. Prepend X
- elsif database.boundary[:prepend] === previous
- false
- # GB10. Any ÷ Any
- else
- true
- end
+ 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
- if should_break
- unpacked << codepoints[marker..pos-1]
- marker = pos
- end
- end
- unpacked
+ 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.
@@ -259,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)
@@ -282,132 +115,43 @@ module ActiveSupport
# * <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::Unicode.default_normalization_form.
- def normalize(string, form=nil)
+ 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*'.freeze)
- 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_key do |k|
- @boundary[k].instance_eval do
- def ===(other)
- detect { |i| i === other } ? true : false
- end
- end if @boundary[k].kind_of?(Array)
- end
-
- # define attr_reader methods for the instance variables
- class << self
- attr_reader(*ATTRIBUTES)
- end
- end
-
- # Returns the directory in which the data files are stored.
- def self.dirname
- File.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 823d68e507..d9e93b530c 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,15 +59,20 @@ 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:
+ #
+ # event.payload[:exception] # => ["ArgumentError", "Invalid value"]
+ # event.payload[:exception_object] # => #<ArgumentError: Invalid value>
#
- # As the previous example depicts, the class <tt>ActiveSupport::Notifications::Event</tt>
+ # As the earlier 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.
#
@@ -146,6 +153,15 @@ module ActiveSupport
#
# ActiveSupport::Notifications.unsubscribe("render")
#
+ # Subscribers using a regexp or other pattern-matching object will remain subscribed
+ # to all events that match their original pattern, unless those events match a string
+ # passed to `unsubscribe`:
+ #
+ # subscriber = ActiveSupport::Notifications.subscribe(/render/) { }
+ # ActiveSupport::Notifications.unsubscribe('render_template.action_view')
+ # subscriber.matches?('render_template.action_view') # => false
+ # subscriber.matches?('render_partial.action_view') # => true
+ #
# == Default Queue
#
# Notifications ships with a queue implementation that consumes and publishes events
@@ -167,6 +183,31 @@ 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|
+ # @event = ActiveSupport::Notifications::Event.new(*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
+ #
+ # If the block passed to the method only takes one parameter,
+ # it will yield an event object to the block:
+ #
+ # ActiveSupport::Notifications.subscribe(/render/) do |event|
+ # @event = event
+ # 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 c53f9c1039..c506b35b1e 100644
--- a/activesupport/lib/active_support/notifications/fanout.rb
+++ b/activesupport/lib/active_support/notifications/fanout.rb
@@ -1,5 +1,8 @@
-require 'mutex_m'
-require 'concurrent/map'
+# frozen_string_literal: true
+
+require "mutex_m"
+require "concurrent/map"
+require "set"
module ActiveSupport
module Notifications
@@ -11,16 +14,22 @@ module ActiveSupport
include Mutex_m
def initialize
- @subscribers = []
+ @string_subscribers = Hash.new { |h, k| h[k] = [] }
+ @other_subscribers = []
@listeners_for = Concurrent::Map.new
super
end
- def subscribe(pattern = nil, block = Proc.new)
- subscriber = Subscribers.new pattern, block
+ def subscribe(pattern = nil, callable = nil, &block)
+ subscriber = Subscribers.new(pattern, callable || block)
synchronize do
- @subscribers << subscriber
- @listeners_for.clear
+ if String === pattern
+ @string_subscribers[pattern] << subscriber
+ @listeners_for.delete(pattern)
+ else
+ @other_subscribers << subscriber
+ @listeners_for.clear
+ end
end
subscriber
end
@@ -29,12 +38,19 @@ module ActiveSupport
synchronize do
case subscriber_or_name
when String
- @subscribers.reject! { |s| s.matches?(subscriber_or_name) }
+ @string_subscribers[subscriber_or_name].clear
+ @listeners_for.delete(subscriber_or_name)
+ @other_subscribers.each { |sub| sub.unsubscribe!(subscriber_or_name) }
else
- @subscribers.delete(subscriber_or_name)
+ pattern = subscriber_or_name.try(:pattern)
+ if String === pattern
+ @string_subscribers[pattern].delete(subscriber_or_name)
+ @listeners_for.delete(pattern)
+ else
+ @other_subscribers.delete(subscriber_or_name)
+ @listeners_for.clear
+ end
end
-
- @listeners_for.clear
end
end
@@ -54,7 +70,8 @@ module ActiveSupport
# this is correctly done double-checked locking (Concurrent::Map's lookups have volatile semantics)
@listeners_for[name] || synchronize do
# use synchronisation when accessing @subscribers
- @listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) }
+ @listeners_for[name] ||=
+ @string_subscribers[name] + @other_subscribers.select { |s| s.subscribed_to?(name) }
end
end
@@ -68,12 +85,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
@@ -81,9 +115,33 @@ module ActiveSupport
end
end
+ class Matcher #:nodoc:
+ attr_reader :pattern, :exclusions
+
+ def self.wrap(pattern)
+ return pattern if String === pattern
+ new(pattern)
+ end
+
+ def initialize(pattern)
+ @pattern = pattern
+ @exclusions = Set.new
+ end
+
+ def unsubscribe!(name)
+ exclusions << -name if pattern === name
+ end
+
+ def ===(name)
+ pattern === name && !exclusions.include?(name)
+ end
+ end
+
class Evented #:nodoc:
+ attr_reader :pattern
+
def initialize(pattern, delegate)
- @pattern = pattern
+ @pattern = Matcher.wrap(pattern)
@delegate = delegate
@can_publish = delegate.respond_to?(:publish)
end
@@ -103,11 +161,15 @@ module ActiveSupport
end
def subscribed_to?(name)
- @pattern === name
+ pattern === name
end
def matches?(name)
- @pattern && @pattern === name
+ pattern && pattern === name
+ end
+
+ def unsubscribe!(name)
+ pattern.unsubscribe!(name)
end
end
@@ -128,6 +190,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
@@ -149,6 +232,10 @@ module ActiveSupport
true
end
+ def unsubscribe!(*)
+ false
+ end
+
alias :matches? :===
end
end
diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb
index 91f94cb2d7..a03e7e483e 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
@@ -11,14 +13,15 @@ module ActiveSupport
@notifier = notifier
end
- # 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={})
+ # Given a block, instrument it by measuring the time taken to execute
+ # and publish it. Without a block, simply send a message via the
+ # notifier. Notice that events get sent even if an error occurs in the
+ # passed-in block.
+ def instrument(name, payload = {})
# some of the listeners might have state
listeners_state = start name, payload
begin
- yield payload
+ yield payload if block_given?
rescue Exception => e
payload[:exception] = [e.class.name, e.message]
payload[:exception_object] = e
@@ -44,14 +47,19 @@ module ActiveSupport
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
+ attr_reader :name, :time, :end, :transaction_id, :payload, :children
+
+ def self.clock_gettime_supported? # :nodoc:
+ defined?(Process::CLOCK_PROCESS_CPUTIME_ID) &&
+ !Gem.win_platform?
+ end
+ private_class_method :clock_gettime_supported?
def initialize(name, start, ending, transaction_id, payload)
@name = name
@@ -61,6 +69,47 @@ 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
+
+ def end=(ending)
+ ActiveSupport::Deprecation.deprecation_warning(:end=, :finish!)
+ @end = ending
+ 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
@@ -86,6 +135,31 @@ module ActiveSupport
def parent_of?(event)
@children.include? event
end
+
+ private
+ def now
+ Concurrent.monotonic_time
+ 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 55628f0313..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,6 +32,8 @@ 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"
@@ -40,6 +47,11 @@ module ActiveSupport
#
# 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 = {})
NumberToPhoneConverter.convert(number, options)
end
@@ -71,10 +83,13 @@ 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
#
@@ -90,6 +105,8 @@ module ActiveSupport
# # => "&pound;1234567890,50"
# number_to_currency(1234567890.50, unit: '&pound;', separator: ',', delimiter: '', format: '%n %u')
# # => "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
@@ -102,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+).
@@ -176,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+).
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 57f40f33bf..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,4 +1,6 @@
-require 'active_support/core_ext/numeric/inquiry'
+# frozen_string_literal: true
+
+require "active_support/number_helper/number_converter"
module ActiveSupport
module NumberHelper
@@ -15,13 +17,13 @@ module ActiveSupport
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 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 45ae8f1a93..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,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/number_helper/number_converter"
+
module ActiveSupport
module NumberHelper
class NumberToDelimitedConverter < NumberConverter #:nodoc:
@@ -12,7 +16,7 @@ module ActiveSupport
private
def parts
- left, right = number.to_s.split('.')
+ left, right = number.to_s.split(".")
left.gsub!(delimiter_pattern) do |digit_to_delimit|
"#{digit_to_delimit}#{options[:delimiter]}"
end
@@ -22,7 +26,6 @@ module ActiveSupport
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 7a1f8171c0..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,29 +23,26 @@ module ActiveSupport
units = opts[:units]
exponent = calculate_exponent(units)
- @number = number / (10 ** exponent)
+ @number = number / (10**exponent)
- until (rounded_number = NumberToRoundedConverter.convert(number, options)) != NumberToRoundedConverter.convert(1000, options)
- @number = number / 1000.0
- exponent += 3
- end
+ rounded_number = NumberToRoundedConverter.convert(number, options)
unit = determine_unit(units, exponent)
- format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, unit).strip
+ 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
@@ -56,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 a83b368b7f..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,3 +1,7 @@
+# frozen_string_literal: true
+
+require "active_support/number_helper/number_converter"
+
module ActiveSupport
module NumberHelper
class NumberToHumanSizeConverter < NumberConverter #:nodoc:
@@ -7,10 +11,6 @@ module ActiveSupport
self.validate_float = true
def convert
- if opts.key?(:prefix)
- ActiveSupport::Deprecation.warn('The :prefix option of `number_to_human_size` is deprecated and will be removed in Rails 5.1 with no replacement.')
- end
-
@number = Float(number)
# for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
@@ -21,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
@@ -54,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 b1658f0f27..5758513021 100644
--- a/activesupport/lib/active_support/ordered_hash.rb
+++ b/activesupport/lib/active_support/ordered_hash.rb
@@ -1,7 +1,9 @@
-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
@@ -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 53a55bd986..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: key not found: :dog
+ # 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 88e2b12cc7..eb92fb4371 100644
--- a/activesupport/lib/active_support/per_thread_registry.rb
+++ b/activesupport/lib/active_support/per_thread_registry.rb
@@ -1,7 +1,9 @@
-require 'active_support/core_ext/module/delegation'
+# 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 and friends.
+ # 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.
@@ -38,15 +40,15 @@ 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.
#
# By letting #delegate handle it, we avoid an enclosure that'll capture args.
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 845788b669..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 time:zones:all" for a time zone names list.'
- 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
index 5d1f0e1e66..2f81cd4f80 100644
--- a/activesupport/lib/active_support/reloader.rb
+++ b/activesupport/lib/active_support/reloader.rb
@@ -1,4 +1,7 @@
-require 'active_support/execution_wrapper'
+# frozen_string_literal: true
+
+require "active_support/execution_wrapper"
+require "active_support/executor"
module ActiveSupport
#--
@@ -26,14 +29,17 @@ module ActiveSupport
define_callbacks :class_unload
+ # Registers a callback that will run once at application startup and every time the code is reloaded.
def self.to_prepare(*args, &block)
set_callback(:prepare, *args, &block)
end
+ # Registers a callback that will run immediately before the classes are unloaded.
def self.before_class_unload(*args, &block)
set_callback(:class_unload, *args, &block)
end
+ # Registers a callback that will run immediately after the classes are unloaded.
def self.after_class_unload(*args, &block)
set_callback(:class_unload, :after, *args, &block)
end
@@ -43,7 +49,11 @@ module ActiveSupport
# Initiate a manual reload
def self.reload!
executor.wrap do
- new.tap(&:run!).complete!
+ new.tap do |instance|
+ instance.run!
+ ensure
+ instance.complete!
+ end
end
prepare!
end
@@ -63,11 +73,8 @@ module ActiveSupport
end
end
- class_attribute :executor
- class_attribute :check
-
- self.executor = Executor
- self.check = lambda { false }
+ class_attribute :executor, default: Executor
+ class_attribute :check, default: lambda { false }
def self.check! # :nodoc:
@should_reload ||= check.call
diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb
index 73bc52b56f..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,65 +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
- def index_of_handler_for_rescue(exception)
- handlers = self.class.rescue_handlers.reverse_each.with_index
- _, index = handlers.detect do |(klass_name, _), _|
- klass = self.class.const_get(klass_name) rescue nil
- klass ||= (klass_name.constantize rescue nil)
- klass === exception if klass
- end
- index
+ # 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 9be8613ada..5e455fca57 100644
--- a/activesupport/lib/active_support/security_utils.rb
+++ b/activesupport/lib/active_support/security_utils.rb
@@ -1,15 +1,15 @@
-require 'digest'
+# 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}"
@@ -17,11 +17,15 @@ module ActiveSupport
b.each_byte { |byte| res |= byte ^ l.shift }
res == 0
end
- module_function :secure_compare
+ module_function :fixed_length_secure_compare
- def variable_size_secure_compare(a, b) # :nodoc:
- secure_compare(::Digest::SHA256.hexdigest(a), ::Digest::SHA256.hexdigest(b))
+ # Constant time string comparison, for variable length strings.
+ #
+ # The values are first processed by SHA256, so that we don't leak length info
+ # via timing attacks.
+ def secure_compare(a, b)
+ fixed_length_secure_compare(::Digest::SHA256.digest(a), ::Digest::SHA256.digest(b)) && a == b
end
- module_function :variable_size_secure_compare
+ 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 1fc12d0bc1..7be4108ed7 100644
--- a/activesupport/lib/active_support/test_case.rb
+++ b/activesupport/lib/active_support/test_case.rb
@@ -1,15 +1,18 @@
-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/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"
+require "concurrent/utility/processor_counter"
module ActiveSupport
class TestCase < ::Minitest::Test
@@ -38,12 +41,101 @@ module ActiveSupport
def test_order
ActiveSupport.test_order ||= :random
end
+
+ # Parallelizes the test suite.
+ #
+ # Takes a +workers+ argument that controls how many times the process
+ # is forked. For each process a new database will be created suffixed
+ # with the worker number.
+ #
+ # test-database-0
+ # test-database-1
+ #
+ # If <tt>ENV["PARALLEL_WORKERS"]</tt> is set the workers argument will be ignored
+ # and the environment variable will be used instead. This is useful for CI
+ # environments, or other environments where you may need more workers than
+ # you do for local testing.
+ #
+ # If the number of workers is set to +1+ or fewer, the tests will not be
+ # parallelized.
+ #
+ # If +workers+ is set to +:number_of_processors+, the number of workers will be
+ # set to the actual core count on the machine you are on.
+ #
+ # 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: :number_of_processors, with: :threads)
+ #
+ # The threaded parallelization uses minitest's parallel executor directly.
+ # The processes parallelization uses a Ruby DRb server.
+ def parallelize(workers: :number_of_processors, with: :processes)
+ workers = Concurrent.physical_processor_count if workers == :number_of_processors
+ workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
+
+ return if workers <= 1
+
+ executor = case with
+ when :processes
+ Testing::Parallelization.new(workers)
+ when :threads
+ Minitest::Parallel::Executor.new(workers)
+ else
+ raise ArgumentError, "#{with} is not a supported parallelization executor."
+ end
+
+ self.lock_threads = false if defined?(self.lock_threads) && with == :threads
+
+ Minitest.parallel_executor = executor
+
+ parallelize_me!
+ end
+
+ # Set up hook for parallel testing. This can be used if you have multiple
+ # databases or any behavior that needs to be run after the process is forked
+ # but before the tests run.
+ #
+ # Note: this feature is not available with the threaded parallelization.
+ #
+ # In your +test_helper.rb+ add the following:
+ #
+ # class ActiveSupport::TestCase
+ # parallelize_setup do
+ # # create databases
+ # end
+ # end
+ def parallelize_setup(&block)
+ ActiveSupport::Testing::Parallelization.after_fork_hook do |worker|
+ yield worker
+ end
+ end
+
+ # Clean up hook for parallel testing. This can be used to drop databases
+ # if your app uses multiple write/read databases or other clean up before
+ # the tests finish. This runs before the forked process is closed.
+ #
+ # Note: this feature is not available with the threaded parallelization.
+ #
+ # In your +test_helper.rb+ add the following:
+ #
+ # class ActiveSupport::TestCase
+ # parallelize_teardown do
+ # # drop databases
+ # end
+ # end
+ def parallelize_teardown(&block)
+ ActiveSupport::Testing::Parallelization.run_cleanup_hook do |worker|
+ yield worker
+ end
+ end
end
alias_method :method_name, :name
include ActiveSupport::Testing::TaggedLogging
- include ActiveSupport::Testing::SetupAndTeardown
+ prepend ActiveSupport::Testing::SetupAndTeardown
include ActiveSupport::Testing::Assertions
include ActiveSupport::Testing::Deprecation
include ActiveSupport::Testing::TimeHelpers
@@ -66,21 +158,6 @@ module ActiveSupport
alias :assert_not_respond_to :refute_respond_to
alias :assert_not_same :refute_same
-
- # Assertion that the block should not raise an exception.
- #
- # Passes if evaluated code in the yielded block raises no exception.
- #
- # assert_nothing_raised do
- # perform_service(param: 'no_exception')
- # end
- def assert_nothing_raised(*args)
- if args.present?
- ActiveSupport::Deprecation.warn(
- "Passing arguments to assert_nothing_raised " \
- "is deprecated and will be removed in Rails 5.1.")
- end
- yield
- end
+ 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 29305e0082..b27ac7ce99 100644
--- a/activesupport/lib/active_support/testing/assertions.rb
+++ b/activesupport/lib/active_support/testing/assertions.rb
@@ -1,8 +1,10 @@
-require 'active_support/core_ext/object/blank'
+# frozen_string_literal: true
module ActiveSupport
module Testing
module Assertions
+ 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,20 +79,28 @@ module ActiveSupport
# assert_difference 'Article.count', -1, 'An Article should be destroyed' do
# post :delete, params: { id: ... }
# end
- def assert_difference(expression, difference = 1, message = nil, &block)
- expressions = Array(expression)
+ 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)
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
@@ -86,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/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 5dfa14eeba..18d63d2780 100644
--- a/activesupport/lib/active_support/testing/deprecation.rb
+++ b/activesupport/lib/active_support/testing/deprecation.rb
@@ -1,4 +1,6 @@
-require 'active_support/deprecation'
+# frozen_string_literal: true
+
+require "active_support/deprecation"
module ActiveSupport
module Testing
@@ -8,7 +10,7 @@ module ActiveSupport
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
diff --git a/activesupport/lib/active_support/testing/file_fixtures.rb b/activesupport/lib/active_support/testing/file_fixtures.rb
index affb84cda5..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.
diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb
index edf8b30a0a..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
@@ -54,7 +45,8 @@ module ActiveSupport
end
}
end
- result = Marshal.dump(self.dup)
+ test_result = defined?(Minitest::Result) ? Minitest::Result.from(self) : dup
+ result = Marshal.dump(test_result)
end
write.puts [result].pack("m")
@@ -64,7 +56,7 @@ module ActiveSupport
write.close
result = read.read
Process.wait2(pid)
- return result.unpack("m")[0]
+ result.unpack1("m")
end
end
@@ -78,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)
@@ -103,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 fccaa54f40..03c38be481 100644
--- a/activesupport/lib/active_support/testing/method_call_assertions.rb
+++ b/activesupport/lib/active_support/testing/method_call_assertions.rb
@@ -1,4 +1,6 @@
-require 'minitest/mock'
+# frozen_string_literal: true
+
+require "minitest/mock"
module ActiveSupport
module Testing
@@ -15,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) }
@@ -33,6 +35,33 @@ module ActiveSupport
assert_called(object, method_name, message, times: 0, &block)
end
+ def assert_called_on_instance_of(klass, method_name, message = nil, times: 1, returns: nil)
+ times_called = 0
+ klass.define_method("stubbed_#{method_name}") do |*|
+ times_called += 1
+
+ returns
+ end
+
+ klass.alias_method "original_#{method_name}", method_name
+ klass.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.alias_method method_name, "original_#{method_name}"
+ klass.undef_method "original_#{method_name}"
+ klass.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
diff --git a/activesupport/lib/active_support/testing/parallelization.rb b/activesupport/lib/active_support/testing/parallelization.rb
new file mode 100644
index 0000000000..63440069b1
--- /dev/null
+++ b/activesupport/lib/active_support/testing/parallelization.rb
@@ -0,0 +1,109 @@
+# 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)
+ o[2] = DRbObject.new(o[2]) if 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
+ 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
+
+ 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..5a3fa9346c 100644
--- a/activesupport/lib/active_support/testing/time_helpers.rb
+++ b/activesupport/lib/active_support/testing/time_helpers.rb
@@ -1,48 +1,64 @@
+# 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.singleton_class.alias_method new_name, method_name
+ 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 :alias_method, stub.method_name, stub.original_method
- singleton_class.send :undef_method, stub.original_method
+ singleton_class.silence_redefinition_of_method stub.method_name
+ singleton_class.alias_method stub.method_name, stub.original_method
+ singleton_class.undef_method stub.original_method
end
end
# 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 79cc748cf5..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
@@ -35,29 +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
- PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N".freeze }
- PRECISIONS[0] = '%FT%T'.freeze
+ PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N" }
+ PRECISIONS[0] = "%FT%T"
- include Comparable
+ 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
@@ -77,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
@@ -133,7 +135,7 @@ 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.inspect # => "Thu, 04 Dec 2014 11:00:25 EST -05:00"
def inspect
@@ -145,9 +147,10 @@ module ActiveSupport
#
# Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00"
def xmlschema(fraction_digits = 0)
- "#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z'.freeze)}"
+ "#{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
@@ -170,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
@@ -222,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.
@@ -279,9 +284,12 @@ 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 # => Mon, 03 Nov 2014 00:26:28 EST -05:00
@@ -296,6 +304,12 @@ module ActiveSupport
#
# 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
@@ -328,6 +342,42 @@ module ActiveSupport
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.
@@ -401,17 +451,23 @@ module ActiveSupport
utc.to_r
end
- # Returns an instance of Time in the system timezone.
- def to_time
- utc.to_time
- end
-
# 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
- utc.to_datetime.new_offset(Rational(utc_offset, 86_400))
+ @to_datetime ||= utc.to_datetime.new_offset(Rational(utc_offset, 86_400))
+ end
+
+ # Returns an instance of +Time+, either with the same UTC offset
+ # as +self+ or in the local system timezone depending on the setting
+ # of +ActiveSupport.to_time_preserves_timezone+.
+ def to_time
+ if preserve_timezone
+ @to_time_with_instance_offset ||= getlocal(utc_offset)
+ else
+ @to_time_with_system_offset ||= getlocal
+ end
end
# So that +self+ <tt>acts_like?(:time)</tt>.
@@ -431,7 +487,8 @@ module ActiveSupport
end
def freeze
- period; utc; time # preload instance variables before freezing
+ # preload instance variables before freezing
+ period; utc; time; to_datetime; to_time
super
end
@@ -454,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
@@ -464,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
@@ -482,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 118bf8eab0..d9e033e23b 100644
--- a/activesupport/lib/active_support/values/time_zone.rb
+++ b/activesupport/lib/active_support/values/time_zone.rb
@@ -1,12 +1,13 @@
-require 'tzinfo'
-require 'concurrent/map'
-require 'active_support/core_ext/object/blank'
+# 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").
@@ -28,7 +29,7 @@ module ActiveSupport
class TimeZone
# Keys are Rails TimeZone names, values are TZInfo identifiers.
MAPPING = {
- "International Date Line West" => "Pacific/Midway",
+ "International Date Line West" => "Etc/GMT+12",
"Midway Island" => "Pacific/Midway",
"American Samoa" => "Pacific/Pago_Pago",
"Hawaii" => "Pacific/Honolulu",
@@ -59,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",
@@ -180,10 +182,12 @@ 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 = Concurrent::Map.new
+ @country_zones = Concurrent::Map.new
class << self
# Assumes self represents an offset from UTC in seconds (as returned from
@@ -192,7 +196,7 @@ module ActiveSupport
# 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]
@@ -225,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
@@ -266,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.
@@ -274,8 +305,7 @@ 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
@@ -285,7 +315,7 @@ module ActiveSupport
# 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)
+ 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
@@ -325,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
@@ -345,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+,
@@ -369,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
@@ -406,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
@@ -418,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:
@@ -427,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 dd2c178fb6..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..075cd4ed8b 100644
--- a/activesupport/lib/active_support/xml_mini.rb
+++ b/activesupport/lib/active_support/xml_mini.rb
@@ -1,9 +1,12 @@
-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 "bigdecimal/util"
+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 +23,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 +35,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 +67,16 @@ 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
+ number.to_d
+ 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 +91,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 +113,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 +131,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 +154,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 +198,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 7f0fcd6996..62356e4d46 100644
--- a/activesupport/test/abstract_unit.rb
+++ b/activesupport/test/abstract_unit.rb
@@ -1,36 +1,42 @@
+# frozen_string_literal: true
+
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
-
-# 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
+
+ # 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/activesupport/test/array_inquirer_test.rb b/activesupport/test/array_inquirer_test.rb
index 263ab3802b..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, 'api'])
+ @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
@@ -19,7 +21,7 @@ class ArrayInquirerTest < ActiveSupport::TestCase
end
def test_any_string_symbol_mismatch
- assert @array_inquirer.any?('mobile')
+ assert @array_inquirer.any?("mobile")
assert @array_inquirer.any?(:api)
end
@@ -33,9 +35,29 @@ class ArrayInquirerTest < ActiveSupport::TestCase
end
def test_inquiry
- result = [:mobile, :tablet, 'api'].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
index 3ca4213c71..118ee6bdd1 100644
--- a/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb
+++ b/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
RaisesArbitraryException = 1
_ = A::B # Autoloading recursion, also expected to be watched and discarded.
-raise Exception, 'arbitray exception message'
+raise Exception, "arbitrary exception message"
diff --git a/activesupport/test/autoloading_fixtures/raises_load_error.rb b/activesupport/test/autoloading_fixtures/raises_load_error.rb
new file mode 100644
index 0000000000..f97be29b71
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/raises_load_error.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+# raises a load error typical of the dynamic code that manually raises load errors
+raise LoadError, "required gem not present kind of error"
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
index e1d96cc512..b6fb391032 100644
--- a/activesupport/test/autoloading_fixtures/throws.rb
+++ b/activesupport/test/autoloading_fixtures/throws.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
Throws = 1
_ = A::B # Autoloading recursion, expected to be discarded.
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 5af041f458..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,20 +38,20 @@ 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
+ 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
+ assert_difference "buffer.count", +1 do
+ benchmark("test_run", silence: true) do
logger.info "NOTHING"
end
end
@@ -57,20 +59,20 @@ class BenchmarkableTest < ActiveSupport::TestCase
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..a696760bb2
--- /dev/null
+++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb
@@ -0,0 +1,545 @@
+# 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_fetch_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_returns_ordered_names
+ @cache.write("bam", "BAM")
+
+ values = @cache.fetch_multi("foo", "bar", "bam") { |key| key.upcase }
+
+ assert_equal(%w(foo bar bam), values.keys)
+ 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..aed04d07d4
--- /dev/null
+++ b/activesupport/test/cache/behaviors/connection_pool_behavior.rb
@@ -0,0 +1,56 @@
+# 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
+ 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
+ ensure
+ Thread.report_on_exception = original_report_on_exception
+ end
+
+ def test_no_connection_pool
+ threads = []
+
+ emulating_latency do
+ 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
+
+ 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..0364d9ab64
--- /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.children(sub_cache_dir)
+ 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..0e472f5a1a
--- /dev/null
+++ b/activesupport/test/cache/stores/mem_cache_store_test.rb
@@ -0,0 +1,140 @@
+# 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
+ servers = ENV["MEMCACHE_SERVERS"] || "localhost:11211"
+ ss = Dalli::Client.new(servers).stats
+ raise Dalli::DalliError unless ss[servers]
+
+ 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(*store, expires_in: 60)
+ @peek = ActiveSupport::Cache.lookup_store(*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(*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(*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(*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(*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(*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(*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, ENV["MEMCACHE_SERVERS"] || "localhost:11211"]
+ 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..1d87f74347
--- /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: [ENV["REDIS_URL"] || "redis://localhost:6379/0"] * 2 }
+ 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 9e744afb2b..0000000000
--- a/activesupport/test/caching_test.rb
+++ /dev/null
@@ -1,1180 +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, @cache.send(:normalize_key, '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
-
- 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
-
- def test_can_call_deprecated_namesaced_key
- assert_deprecated "`namespaced_key` is deprecated" do
- @cache.send(:namespaced_key, 111, {})
- end
- 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_read_nil
- @cache.with_local_cache do
- assert_equal nil, @cache.read('foo')
- @cache.send(:bypass_local_cache) { @cache.write 'foo', 'bar' }
- assert_equal 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_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
-
- def test_can_call_deprecated_set_cache_value
- @cache.with_local_cache do
- assert_deprecated "`set_cache_value` is deprecated" do
- @cache.send(:set_cache_value, 1, 'foo', :ignored, {})
- end
- assert_equal 1, @cache.read('foo')
- end
- 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
- 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_keys
- @cache.write("a"*10000, 1)
- assert_equal 1, @cache.read("a"*10000)
- 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, {})
- 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(: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 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
-
- def test_can_call_deprecated_key_file_path
- assert_deprecated "`key_file_path` is deprecated" do
- assert_equal 111, @cache.send(:key_file_path, 111)
- end
- 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
-
- def test_can_call_deprecated_escape_key
- assert_deprecated "`escape_key` is deprecated" do
- assert_equal 111, @cache.send(:escape_key, 111)
- end
- 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_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 @buffer.string.blank?
- 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 a624473f46..79098b2a7d 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,17 +25,13 @@ 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
def callback_object(callback_method)
klass = Class.new
- klass.send(:define_method, callback_method) do |model|
+ klass.define_method(callback_method) do |model|
model.history << [:"#{callback_method}_save", :object]
end
klass.new
@@ -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))
- ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method_sym)) }
send(callback_method, callback_proc(callback_method_sym))
- send(callback_method, callback_object(callback_method_sym.to_s.gsub(/_save/, '')))
+ send(callback_method, 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
- ActiveSupport::Deprecation.silence { set_callback :save, :before, "tweedle_dee" }
- set_callback :save, :before, proc {|m| m.history << "yup" }
- set_callback :save, :before, :nope, :if => proc { false }
- set_callback :save, :before, :nope, :unless => proc { true }
- 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,41 +857,15 @@ module CallbacksTest
def test_block_never_called_if_abort_is_thrown
obj = CallbackDefaultTerminator.new
obj.save
- assert !obj.saved
+ assert_not obj.saved
end
end
- class CallbackFalseTerminatorWithoutConfigTest < ActiveSupport::TestCase
- def test_returning_false_does_not_halt_callback_if_config_variable_is_not_set
+ class CallbackFalseTerminatorTest < ActiveSupport::TestCase
+ def test_returning_false_does_not_halt_callback
obj = CallbackFalseTerminator.new
obj.save
- assert_equal nil, obj.halted
- assert obj.saved
- end
- end
-
- class CallbackFalseTerminatorWithConfigTrueTest < ActiveSupport::TestCase
- def setup
- ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = true
- end
-
- def test_returning_false_does_not_halt_callback_if_config_variable_is_true
- obj = CallbackFalseTerminator.new
- obj.save
- assert_equal nil, obj.halted
- assert obj.saved
- end
- end
-
- class CallbackFalseTerminatorWithConfigFalseTest < ActiveSupport::TestCase
- def setup
- ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = false
- end
-
- def test_returning_false_does_not_halt_callback_if_config_variable_is_false
- obj = CallbackFalseTerminator.new
- obj.save
- assert_equal nil, obj.halted
+ assert_nil obj.halted
assert obj.saved
end
end
@@ -810,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
@@ -821,7 +891,6 @@ module CallbacksTest
writer.save
assert_equal [
[:before_save, :symbol],
- [:before_save, :string],
[:before_save, :proc],
[:before_save, :object],
[:before_save, :class],
@@ -830,7 +899,6 @@ module CallbacksTest
[:after_save, :class],
[:after_save, :object],
[:after_save, :proc],
- [:after_save, :string],
[:after_save, :symbol]
], writer.history
end
@@ -885,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
@@ -903,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
}
@@ -918,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
@@ -964,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
@@ -1044,14 +1112,6 @@ module CallbacksTest
assert_equal 1, calls.length
end
- def test_add_eval
- calls = []
- klass = ActiveSupport::Deprecation.silence { build_class("bar") }
- klass.class_eval { define_method(:bar) { calls << klass } }
- klass.new.run
- assert_equal 1, calls.length
- end
-
def test_skip_class # removes one at a time
calls = []
callback = Class.new {
@@ -1086,7 +1146,7 @@ module CallbacksTest
def test_skip_string # raises error
calls = []
- klass = ActiveSupport::Deprecation.silence { build_class("bar") }
+ klass = build_class(:bar)
klass.class_eval { define_method(:bar) { calls << klass } }
assert_raises(ArgumentError) { klass.skip "bar" }
klass.new.run
@@ -1112,12 +1172,24 @@ module CallbacksTest
end
end
- class DeprecatedWarningTest < ActiveSupport::TestCase
- def test_deprecate_string_callback
+ class NotSupportedStringConditionalTest < ActiveSupport::TestCase
+ def test_string_conditional_options
klass = Class.new(Record)
- assert_deprecated do
- klass.send :before_save, "tweedle_dee"
+ 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
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..4b3cfcd1d2 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
@@ -126,4 +128,12 @@ class ConcernTest < ActiveSupport::TestCase
end
end
end
+
+ def test_no_raise_on_same_included_call
+ assert_nothing_raised do
+ 2.times do
+ load File.expand_path("../fixtures/concern/some_concern.rb", __FILE__)
+ end
+ end
+ end
end
diff --git a/activesupport/test/concurrency/load_interlock_aware_monitor_test.rb b/activesupport/test/concurrency/load_interlock_aware_monitor_test.rb
new file mode 100644
index 0000000000..2d0f45ec5f
--- /dev/null
+++ b/activesupport/test/concurrency/load_interlock_aware_monitor_test.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "concurrent/atomic/count_down_latch"
+require "active_support/concurrency/load_interlock_aware_monitor"
+
+module ActiveSupport
+ module Concurrency
+ class LoadInterlockAwareMonitorTest < ActiveSupport::TestCase
+ def setup
+ @monitor = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
+ end
+
+ def test_entering_with_no_blocking
+ assert @monitor.mon_enter
+ end
+
+ def test_entering_with_blocking
+ load_interlock_latch = Concurrent::CountDownLatch.new
+ monitor_latch = Concurrent::CountDownLatch.new
+
+ able_to_use_monitor = false
+ able_to_load = false
+
+ thread_with_load_interlock = Thread.new do
+ ActiveSupport::Dependencies.interlock.running do
+ load_interlock_latch.count_down
+ monitor_latch.wait
+
+ @monitor.synchronize do
+ able_to_use_monitor = true
+ end
+ end
+ end
+
+ thread_with_monitor_lock = Thread.new do
+ @monitor.synchronize do
+ monitor_latch.count_down
+ load_interlock_latch.wait
+
+ ActiveSupport::Dependencies.interlock.loading do
+ able_to_load = true
+ end
+ end
+ end
+
+ thread_with_load_interlock.join
+ thread_with_monitor_lock.join
+
+ assert able_to_use_monitor
+ assert able_to_load
+ end
+ end
+ end
+end
diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb
index 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 1115bc0fd8..cdb8441b81 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,10 +107,20 @@ 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_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")
+
+ with_autoloading_fixtures do
+ assert_nil yield("Em")
+ end
+
+ assert_raises(LoadError) do
+ with_autoloading_fixtures do
+ yield("RaisesLoadError")
+ end
+ end
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 1d834667f0..427b058925 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
@@ -30,6 +32,18 @@ class AccessTest < ActiveSupport::TestCase
assert_equal array[-2], array.second_to_last
end
+ def test_including
+ assert_equal [1, 2, 3, 4, 5], [1, 2, 4].including(3, 5).sort
+ assert_equal [1, 2, 3, 4, 5], [1, 2, 4].including([3, 5]).sort
+ assert_equal [[0, 1], [1, 0]], [[0, 1]].including([[1, 0]])
+ end
+
+ def test_excluding
+ assert_equal [1, 2, 4], [1, 2, 3, 4, 5].excluding(3, 5)
+ assert_equal [1, 2, 4], [1, 2, 3, 4, 5].excluding([3, 5])
+ assert_equal [[0, 1]], [[0, 1], [1, 0]].excluding([[1, 0]])
+ end
+
def test_without
assert_equal [1, 2, 4], [1, 2, 3, 4, 5].without(3, 5)
end
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..8573dbd5a6 100644
--- a/activesupport/test/core_ext/array/prepend_append_test.rb
+++ b/activesupport/test/core_ext/array/prepend_append_test.rb
@@ -1,12 +1,11 @@
-require 'abstract_unit'
-require 'active_support/core_ext/array'
+# frozen_string_literal: true
-class PrependAppendTest < ActiveSupport::TestCase
- def test_append
- assert_equal [1, 2], [1].append(2)
- end
+require "abstract_unit"
- def test_prepend
- assert_equal [2, 1], [1].prepend(2)
+class PrependAppendTest < ActiveSupport::TestCase
+ def test_requiring_prepend_and_append_is_deprecated
+ assert_deprecated do
+ require "active_support/core_ext/array/prepend_and_append"
+ end
end
end
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 54df87def8..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,138 +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 date_time_init(2015,1,4,0,0,0).on_weekday?
- assert_not date_time_init(2015,1,4,15,15,10).on_weekday?
+ assert_not_predicate date_time_init(2015, 1, 4, 0, 0, 0), :on_weekday?
+ assert_not_predicate date_time_init(2015, 1, 4, 15, 15, 10), :on_weekday?
end
def test_on_weekday_on_monday
- assert date_time_init(2015,1,5,0,0,0).on_weekday?
- assert date_time_init(2015,1,5,15,15,10).on_weekday?
+ assert_predicate date_time_init(2015, 1, 5, 0, 0, 0), :on_weekday?
+ assert_predicate date_time_init(2015, 1, 5, 15, 15, 10), :on_weekday?
+ end
+
+ def test_before
+ assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 5, 12, 0, 0))
+ assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 6, 12, 0, 0))
+ assert_equal true, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 7, 12, 0, 0))
+ end
+
+ def test_after
+ assert_equal true, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 5, 12, 0, 0))
+ assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 6, 12, 0, 0))
+ assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 7, 12, 0, 0))
end
def with_bw_default(bw = :monday)
diff --git a/activesupport/test/core_ext/date_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 16efeeadd5..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,134 +92,136 @@ 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_date_time_should_have_correct_last_week_for_leap_year
assert_equal DateTime.civil(2016, 2, 29), DateTime.civil(2016, 3, 7).last_week
end
- def test_last_month_on_31st
- assert_equal DateTime.civil(2004, 2, 29), DateTime.civil(2004, 3, 31).last_month
- end
-
def test_last_quarter_on_31st
assert_equal DateTime.civil(2004, 2, 29), DateTime.civil(2004, 5, 31).last_quarter
end
@@ -209,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
@@ -266,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
@@ -281,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?
@@ -305,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
@@ -318,78 +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) <=> 2440587
+ assert_equal 0, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440588
assert_equal(-1, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440589)
end
def test_compare_with_float
- assert_equal 1, DateTime.civil(1970) <=> 2440586.5
- assert_equal 0, DateTime.civil(1970) <=> 2440587.5
+ assert_equal 1, DateTime.civil(1970) <=> 2440586.5
+ assert_equal 0, DateTime.civil(1970) <=> 2440587.5
assert_equal(-1, DateTime.civil(1970) <=> 2440588.5)
end
def test_compare_with_rational
- assert_equal 1, DateTime.civil(1970) <=> Rational(4881173, 2)
- assert_equal 0, DateTime.civil(1970) <=> Rational(4881175, 2)
+ assert_equal 1, DateTime.civil(1970) <=> Rational(4881173, 2)
+ assert_equal 0, DateTime.civil(1970) <=> Rational(4881175, 2)
assert_equal(-1, DateTime.civil(1970) <=> Rational(4881177, 2))
end
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..381b5a1f32 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?
@@ -106,11 +217,18 @@ class EnumerableTests < ActiveSupport::TestCase
assert_equal false, GenericEnumerable.new([ 1 ]).exclude?(1)
end
+ def test_excluding
+ assert_equal [1, 2, 4], GenericEnumerable.new((1..5).to_a).excluding(3, 5)
+ assert_equal [3, 4, 5], GenericEnumerable.new((1..5).to_a).excluding([1, 2])
+ assert_equal [[0, 1]], GenericEnumerable.new([[0, 1], [1, 0]]).excluding([[1, 0]])
+ assert_equal [1, 2, 4], (1..5).to_a.excluding(3, 5)
+ assert_equal [1, 2, 4], (1..5).to_set.excluding(3, 5)
+ assert_equal({ foo: 1, baz: 3 }, { foo: 1, bar: 2, baz: 3 }.excluding(:bar))
+ end
+
def test_without
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 [3, 4, 5], GenericEnumerable.new((1..5).to_a).without([1, 2])
end
def test_pluck
diff --git a/activesupport/test/core_ext/file_test.rb b/activesupport/test/core_ext/file_test.rb
index cde0132b97..186c863f91 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
@@ -57,6 +59,20 @@ class AtomicWriteTest < ActiveSupport::TestCase
File.unlink(file_name) rescue nil
end
+ def test_atomic_write_preserves_file_permissions_same_directory
+ Dir.mktmpdir do |temp_dir|
+ File.chmod 0700, temp_dir
+
+ probed_permissions = File.probe_stat_in(temp_dir).mode.to_s(8)
+
+ File.atomic_write(File.join(temp_dir, file_name), &:close)
+
+ actual_permissions = File.stat(File.join(temp_dir, file_name)).mode.to_s(8)
+
+ assert_equal actual_permissions, probed_permissions
+ end
+ end
+
def test_atomic_write_returns_result_from_yielded_block
block_return_value = File.atomic_write(file_name, Dir.pwd) do |file|
"Hello world!"
diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb
deleted file mode 100644
index 99af274614..0000000000
--- a/activesupport/test/core_ext/hash/transform_keys_test.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-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' }
- mapped = original.transform_keys { |k| "#{k}!".to_sym }
-
- 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' }
- mapped = original.transform_keys! { |k| "#{k}!".to_sym }
-
- assert_equal({ a!: 'a', b!: 'b' }, original)
- assert_same original, mapped
- 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! 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' }
- mapped = original.transform_keys.with_index { |k, i| [k, i].join.to_sym }
- 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
-end
diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb
index 114022fbaf..e481b5e4a9 100644
--- a/activesupport/test/core_ext/hash/transform_values_test.rb
+++ b/activesupport/test/core_ext/hash/transform_values_test.rb
@@ -1,75 +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 a sized Enumerator if no block is given" do
- original = { a: 'a', b: 'b' }
- enumerator = original.transform_values
- assert_equal original.size, enumerator.size
- assert_equal Enumerator, enumerator.class
- end
-
- test "transform_values! returns a sized Enumerator if no block is given" do
- original = { a: 'a', b: 'b' }
- enumerator = original.transform_values!
- assert_equal original.size, enumerator.size
- assert_equal Enumerator, enumerator.class
- end
-
- test "transform_values is chainable with Enumerable methods" do
- original = { a: 'a', b: 'b' }
- mapped = original.transform_values.with_index { |v, i| [v, i].join }
- assert_equal({ a: 'a0', b: 'b1' }, mapped)
- end
+ 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' }
- original.transform_values!.with_index { |v, i| [v, i].join }
- assert_equal({ a: 'a0', b: 'b1' }, original)
+ 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 be8583e704..8572d56722 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -1,63 +1,40 @@
-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
h = {}
- assert_respond_to h, :transform_keys
- assert_respond_to h, :transform_keys!
assert_respond_to h, :deep_transform_keys
assert_respond_to h, :deep_transform_keys!
+ assert_respond_to h, :deep_transform_values
+ assert_respond_to h, :deep_transform_values!
assert_respond_to h, :symbolize_keys
assert_respond_to h, :symbolize_keys!
assert_respond_to h, :deep_symbolize_keys
@@ -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 }
+ 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 }
end
- def test_transform_keys_not_mutates
- transformed_hash = @mixed.dup
- transformed_hash.transform_keys{ |key| key.to_s.upcase }
- assert_equal @mixed, transformed_hash
+ def test_deep_transform_keys_not_mutates
+ transformed_hash = @nested_mixed.deep_dup
+ transformed_hash.deep_transform_keys { |key| key.to_s.upcase }
+ assert_equal @nested_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 }
+ 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 }
end
- def test_deep_transform_keys_not_mutates
+ def test_deep_transform_keys_with_bang_mutates
transformed_hash = @nested_mixed.deep_dup
- transformed_hash.deep_transform_keys{ |key| key.to_s.upcase }
- assert_equal @nested_mixed, transformed_hash
+ transformed_hash.deep_transform_keys! { |key| key.to_s.upcase }
+ assert_equal @nested_upcase_strings, transformed_hash
+ assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed)
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 }
+ def test_deep_transform_values
+ assert_equal({ "a" => "1", "b" => "2" }, @strings.deep_transform_values { |value| value.to_s })
+ assert_equal({ "a" => { "b" => { "c" => "3" } } }, @nested_strings.deep_transform_values { |value| value.to_s })
+ assert_equal({ "a" => [ { "b" => "2" }, { "c" => "3" }, "4" ] }, @string_array_of_hashes.deep_transform_values { |value| value.to_s })
end
- def test_transform_keys_with_bang_mutates
- transformed_hash = @mixed.dup
- transformed_hash.transform_keys!{ |key| key.to_s.upcase }
- assert_equal @upcase_strings, transformed_hash
- assert_equal @mixed, { :a => 1, "b" => 2 }
+ def test_deep_transform_values_not_mutates
+ transformed_hash = @nested_mixed.deep_dup
+ transformed_hash.deep_transform_values { |value| value.to_s }
+ assert_equal @nested_mixed, transformed_hash
end
- def test_deep_transform_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 }
+ def test_deep_transform_values!
+ assert_equal({ "a" => "1", "b" => "2" }, @strings.deep_transform_values! { |value| value.to_s })
+ assert_equal({ "a" => { "b" => { "c" => "3" } } }, @nested_strings.deep_transform_values! { |value| value.to_s })
+ assert_equal({ "a" => [ { "b" => "2" }, { "c" => "3" }, "4" ] }, @string_array_of_hashes.deep_transform_values! { |value| value.to_s })
end
- def test_deep_transform_keys_with_bang_mutates
+ def test_deep_transform_values_with_bang_mutates
transformed_hash = @nested_mixed.deep_dup
- transformed_hash.deep_transform_keys!{ |key| key.to_s.upcase }
- assert_equal @nested_upcase_strings, transformed_hash
- assert_equal @nested_mixed, { 'a' => { :b => { 'c' => 3 } } }
+ transformed_hash.deep_transform_values! { |value| value.to_s }
+ assert_equal({ "a" => { b: { "c" => "3" } } }, transformed_hash)
+ assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed)
end
def test_symbolize_keys
@@ -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,497 +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_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_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
- skip if RUBY_VERSION < "2.3.0"
- data = {"this" => {"views" => 1234}}.with_indifferent_access
- assert_equal 1234, data.dig(:this, :views)
+ 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)
@@ -757,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)
@@ -821,86 +313,50 @@ 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_slice
- original = { :a => 'x', :b => 'y', :c => 10 }
- expected = { :a => 'x', :b => 'y' }
+ 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 return a new hash with only the given keys.
- assert_equal expected, original.slice(:a, :b)
- assert_not_equal expected, original
+ # 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_inplace
- 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
+ original = { a: "x", b: "y", c: 10 }
+ expected_return = { c: 10 }
+ expected_original = { a: "x", b: "y" }
- def test_slice_with_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 hash containing the removed key/value pairs.
+ assert_equal expected_return, original.slice!(:a, :b)
- # Should return a new hash with only the given keys when given an array key.
- assert_equal expected, original.slice([:a, :b], :c)
- assert_not_equal expected, original
+ # Should replace the hash with only the given keys.
+ assert_equal expected_original, original
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" }
-
- # 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)
@@ -920,39 +376,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)
@@ -964,8 +408,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)
@@ -974,109 +418,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(FrozenError) { 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_deprecated { HashWithIndifferentAccess.new_from_hash_copying_default(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]
+ 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
@@ -1089,139 +458,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
@@ -1260,17 +629,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
@@ -1295,18 +664,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"]
@@ -1325,12 +694,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"]
@@ -1369,17 +738,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
@@ -1395,14 +764,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"]
@@ -1430,7 +799,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
@@ -1441,7 +810,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
@@ -1453,7 +822,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
@@ -1466,7 +835,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
@@ -1478,12 +847,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
@@ -1493,9 +862,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
@@ -1504,7 +873,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
@@ -1527,13 +896,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"]
@@ -1549,8 +918,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"]
@@ -1573,81 +942,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_return_nil_if_no_key_is_supplied
- hash_wia = HashWithIndifferentAccess.new { 1 + 2 }
- assert_equal 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
-
# 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)
@@ -1662,30 +976,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
@@ -1694,8 +1008,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
@@ -1705,8 +1019,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
@@ -1716,8 +1030,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
@@ -1727,20 +1041,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..6d3726e407 100644
--- a/activesupport/test/core_ext/load_error_test.rb
+++ b/activesupport/test/core_ext/load_error_test.rb
@@ -1,26 +1,25 @@
-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'
- rescue LoadError => e
- assert_equal 'nor/this/one.rb', e.path
- end
+ load "nor/this/one.rb"
+ rescue LoadError => e
+ assert_equal "nor/this/one.rb", e.path
+ 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 07c0c0d8cb..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
@@ -128,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
index a9fd878b80..e0e331fc91 100644
--- a/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb
+++ b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'active_support/core_ext/module/attribute_accessors_per_thread'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/core_ext/module/attribute_accessors_per_thread"
class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase
def setup
@@ -8,6 +10,12 @@ class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase
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
@@ -35,45 +43,45 @@ class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase
assert_respond_to @class, :foo
assert_respond_to @class, :foo=
assert_respond_to @object, :bar
- assert !@object.respond_to?(:bar=)
+ assert_not_respond_to @object, :bar=
end.join
end
def test_should_not_create_instance_reader
Thread.new do
assert_respond_to @class, :shaq
- assert !@object.respond_to?(:shaq)
+ assert_not_respond_to @object, :shaq
end.join
end
def test_should_not_create_instance_accessors
Thread.new do
assert_respond_to @class, :camp
- assert !@object.respond_to?(:camp)
- assert !@object.respond_to?(:camp=)
+ assert_not_respond_to @object, :camp
+ assert_not_respond_to @object, :camp=
end.join
end
def test_values_should_not_bleed_between_threads
threads = []
threads << Thread.new do
- @class.foo = 'things'
+ @class.foo = "things"
sleep 1
- assert_equal 'things', @class.foo
+ assert_equal "things", @class.foo
end
threads << Thread.new do
- @class.foo = 'other things'
+ @class.foo = "other things"
sleep 1
- assert_equal 'other things', @class.foo
+ assert_equal "other things", @class.foo
end
-
+
threads << Thread.new do
- @class.foo = 'really other things'
+ @class.foo = "really other things"
sleep 1
- assert_equal 'really other things', @class.foo
+ assert_equal "really other things", @class.foo
end
-
+
threads.each { |t| t.join }
end
@@ -108,8 +116,18 @@ class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase
end
def test_should_return_same_value_by_class_or_instance_accessor
- @class.foo = 'fries'
+ @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 0b0f3a2808..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
@@ -86,14 +110,28 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
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_should_not_invoke_default_value_block_multiple_times
+ def test_method_invocation_should_not_invoke_the_default_block
count = 0
- @module.cattr_accessor(:defcount){ count += 1 }
+
+ @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 a3146cabe1..0000000000
--- a/activesupport/test/core_ext/module/qualified_const_test.rb
+++ /dev/null
@@ -1,118 +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_deprecated 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
- end
-
- test "mod.qualified_const_defined?" do
- assert_deprecated 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
- end
-
- test "qualified_const_get" do
- assert_deprecated 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
- end
-
- test "qualified_const_set" do
- assert_deprecated 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
- end
-
- test "reject absolute paths" do
- assert_deprecated 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
- 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
deleted file mode 100644
index 80eb31a5c4..0000000000
--- a/activesupport/test/core_ext/module/reachable_test.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-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?
- end
-
- test "ordinary named classes or modules are reachable" do
- assert Kernel.reachable?
- assert Object.reachable?
- end
-
- test "a named class or module whose constant has gone is not reachable" do
- c = eval "class C; end; C"
- m = eval "module M; end; M"
-
- self.class.send(:remove_const, :C)
- self.class.send(:remove_const, :M)
-
- assert !c.reachable?
- assert !m.reachable?
- end
-
- test "a named class or module whose constants store different objects are not reachable" do
- c = eval "class C; end; C"
- m = eval "module M; end; M"
-
- self.class.send(:remove_const, :C)
- self.class.send(:remove_const, :M)
-
- eval "class C; end"
- eval "module M; end"
-
- assert C.reachable?
- assert M.reachable?
- assert !c.reachable?
- assert !m.reachable?
- end
-end \ No newline at end of file
diff --git a/activesupport/test/core_ext/module/remove_method_test.rb b/activesupport/test/core_ext/module/remove_method_test.rb
index 0d684dc70e..a18fc0a5e4 100644
--- a/activesupport/test/core_ext/module/remove_method_test.rb
+++ b/activesupport/test/core_ext/module/remove_method_test.rb
@@ -1,51 +1,52 @@
-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
- return 1
+ 1
end
protected :do_something_protected
def do_something_private
- return 1
+ 1
end
private :do_something_private
class << self
def do_something_else
- return 2
+ 2
end
end
end
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{
- self.remove_possible_singleton_method(:do_something_else)
+ RemoveMethodTests::A.class_eval {
+ remove_possible_singleton_method(:do_something_else)
}
- assert !RemoveMethodTests::A.respond_to?(:do_something_else)
+ assert_not_respond_to RemoveMethodTests::A, :do_something_else
end
def test_redefine_method_in_an_object
- RemoveMethodTests::A.class_eval{
- self.redefine_method(:do_something) { return 100 }
- self.redefine_method(:do_something_protected) { return 100 }
- self.redefine_method(:do_something_private) { 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
assert_equal 100, RemoveMethodTests::A.new.send(:do_something_protected)
@@ -55,5 +56,4 @@ class RemoveMethodTest < ActiveSupport::TestCase
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 ae4aed0554..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,6 +72,36 @@ 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
@@ -94,15 +113,15 @@ HasBlock = Struct.new(:block) do
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}"
@@ -112,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]
@@ -160,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
@@ -169,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
@@ -198,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
@@ -226,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
@@ -255,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
@@ -277,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
@@ -287,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
@@ -297,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 }
@@ -313,248 +347,164 @@ class ModuleTest < ActiveSupport::TestCase
def test_delegation_with_method_arguments
has_block = HasBlock.new(Block.new)
- assert has_block.hello?
+ assert_predicate has_block, :hello?
end
- def test_parent
- assert_equal Yz::Zy, Yz::Zy::Cd.parent
- assert_equal Yz, Yz::Zy.parent
- assert_equal Object, Yz.parent
+ def test_delegate_missing_to_with_method
+ assert_equal "David", DecoratedTester.new(@david).name
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_reserved_methods
+ assert_equal "David", DecoratedReserved.new(@david).name
end
- def test_local_constants
- ActiveSupport::Deprecation.silence do
- assert_equal %w(Constant1 Constant3), Ab.local_constants.sort.map(&:to_s)
+ def test_delegate_missing_to_does_not_delegate_to_private_methods
+ e = assert_raises(NoMethodError) do
+ DecoratedReserved.new(@david).private_name
end
- end
- def test_local_constants_is_deprecated
- assert_deprecated { Ab.local_constants.sort.map(&:to_s) }
+ assert_match(/undefined method `private_name' for/, e.message)
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_fake_methods
+ e = assert_raises(NoMethodError) do
+ DecoratedReserved.new(@david).my_fake_method
end
- end
-end
-module BarMethods
- def bar_with_baz
- bar_without_baz << '_with_baz'
+ assert_match(/undefined method `my_fake_method' for/, e.message)
end
- def quux_with_baz!
- quux_without_baz! << '_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 quux_with_baz?
- false
+ assert_equal "name delegated to client, but client is nil", e.message
end
- def quux_with_baz=(v)
- send(:quux_without_baz=, v) << '_with_baz'
- 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 duck_with_orange
- duck_without_orange << '_with_orange'
+ 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
-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_respects_superclass_missing
+ assert_equal 42, DecoratedTester.new(@david).extra_missing
- def teardown
- Object.instance_eval { remove_const :FooClassWithBarMethod }
+ assert_respond_to DecoratedTester.new(@david), :extra_missing
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_with_case
+ event = Event.new(Tester.new)
+ assert_equal 1, event.foo
+ end
- alias_method_chain :base, :deprecated
+ def test_private_delegate
+ location = Class.new do
+ def initialize(place)
+ @place = place
end
+
+ private(*delegate(:street, :city, to: :@place))
end
- 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]
+ place = location.new(Somewhere.new("Such street", "Sad city"))
- feature_aliases.each do |method|
- assert !@instance.respond_to?(method)
- end
+ assert_not_respond_to place, :street
+ assert_not_respond_to place, :city
- assert_equal 'bar', @instance.bar
-
- FooClassWithBarMethod.class_eval { include BarMethodAliaser }
+ assert place.respond_to?(:street, true) # Asking for private method
+ assert place.respond_to?(:city, true)
+ end
- feature_aliases.each do |method|
- assert_respond_to @instance, method
+ def test_private_delegate_prefixed
+ 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, prefix: :the))
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_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_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_with_private_option
+ 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=
-
+ delegate(:street, :city, to: :@place, private: true)
+ 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 place.respond_to?(:street, true) # Asking for private method
+ assert place.respond_to?(: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!
+ def test_some_public_some_private_delegate_with_private_option
+ location = Class.new do
+ def initialize(place)
+ @place = place
end
- assert_nothing_raised do
- assert_equal 'quux_with_baz', @instance.quux_with_baz!
- end
-
- assert_raise(NameError) do
- FooClassWithBarMethod.alias_method_chain :quux?, :baz!
- end
+ delegate(:street, to: :@place)
+ delegate(: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_respond_to place, :street
+ assert_not_respond_to place, :city
- FooClassWithBarMethod.alias_method_chain :quux?, :baz do |target, punctuation|
- args = [target, punctuation]
- end
- end
-
- assert_not_nil args
- assert_equal 'quux', args[0]
- assert_equal '?', args[1]
- end
+ assert place.respond_to?(:city, true) # Asking for private method
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
+ def test_private_delegate_prefixed_with_private_option
+ location = Class.new do
+ def initialize(place)
+ @place = place
end
- assert_raise NoMethodError do
- @instance.duck
- end
-
- assert_equal 'duck_with_orange', @instance.instance_eval { duck }
- assert FooClassWithBarMethod.private_method_defined?(:duck)
+ delegate(:street, :city, to: :@place, prefix: :the, private: true)
end
- 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
- end
+ place = location.new(Somewhere.new("Such street", "Sad city"))
- assert_equal 'duck_with_orange', @instance.instance_eval { duck }
- assert FooClassWithBarMethod.protected_method_defined?(:duck)
- 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_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 5654aeb4f8..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
@@ -154,53 +156,52 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
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
@@ -216,301 +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 '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_si_prefix
- assert_deprecated do
- 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 '1.23 PB', 1234567890123456.to_s(:human_size, :prefix => :si)
- assert_equal '1.23 EB', 1234567890123456789.to_s(:human_size, :prefix => :si)
- end
+ 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 Float, Float(1230).class
- assert_equal '1.23 Thousand', Float(1230).to_s(:human)
-
- assert_equal Bignum, (100**10).class
- assert_equal '100000 Quadrillion', (100**10).to_s(:human)
-
- assert_equal BigDecimal, BigDecimal("1000010").class
- assert_equal '1 Million', BigDecimal("1000010").to_s(:human)
- end
-
- def test_to_formatted_s_is_deprecated
- assert_deprecated do
- 5551234.to_formatted_s(:phone)
- end
+ 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
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)
+ 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 "123", 123.to_s
+ assert_equal "1111011", 123.to_s(2)
- assert_equal '2.5', 2.5.to_s
+ 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 "100000000000000000000", (100**10).to_s
+ assert_equal "1010110101111000111010111100010110101100011000100000000000000000000", (100**10).to_s(2)
- assert_equal '1000010.0', BigDecimal("1000010").to_s
- assert_equal '10000 10.0', BigDecimal("1000010").to_s('5F')
+ assert_equal "1000010.0", BigDecimal("1000010").to_s
+ assert_equal "10000 10.0", BigDecimal("1000010").to_s("5F")
+
+ assert_raises TypeError do
+ 1.to_s({})
+ end
end
def test_in_milliseconds
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 a142096993..954f415383 100644
--- a/activesupport/test/core_ext/object/blank_test.rb
+++ b/activesupport/test/core_ext/object/blank_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 BlankTest < ActiveSupport::TestCase
class EmptyTrue
@@ -14,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" }
@@ -28,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 25bf0207b8..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_overridden_method_on_delegator
- assert_equal 'overridden reverse', Decorator.new(@string).reverse
+ assert_equal "overridden reverse", Decorator.new(@string).try(:reverse)
end
def test_try_with_private_method_on_delegator
@@ -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 f28cebda3d..16d6a4c2f2 100644
--- a/activesupport/test/core_ext/range_ext_test.rb
+++ b/activesupport/test/core_ext/range_ext_test.rb
@@ -1,7 +1,9 @@
-require 'abstract_unit'
-require 'active_support/time'
-require 'active_support/core_ext/numeric'
-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
@@ -14,6 +16,11 @@ class RangeTest < ActiveSupport::TestCase
assert_equal "BETWEEN '2005-12-10 15:30:00' AND '2005-12-10 17:30:00'", date_range.to_s(:db)
end
+ def test_to_s_with_alphabets
+ alphabet_range = ("a".."z")
+ assert_equal "BETWEEN 'a' AND 'z'", alphabet_range.to_s(:db)
+ end
+
def test_to_s_with_numeric
number_range = (1..100)
assert_equal "BETWEEN '1' AND '100'", number_range.to_s(:db)
@@ -30,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
@@ -38,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
@@ -50,7 +57,7 @@ class RangeTest < ActiveSupport::TestCase
end
def test_should_include_other_with_exclusive_end
- assert((1..10).include?(1...10))
+ assert((1..10).include?(1...11))
end
def test_should_compare_identical_inclusive
@@ -62,19 +69,19 @@ class RangeTest < ActiveSupport::TestCase
end
def test_should_compare_other_with_exclusive_end
- assert((1..10) === (1...10))
+ assert((1..10) === (1...11))
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
@@ -86,6 +93,10 @@ class RangeTest < ActiveSupport::TestCase
assert range.method(:include?) != range.method(:cover?)
end
+ def test_should_cover_other_with_exclusive_end
+ assert((1..10).cover?(1...11))
+ end
+
def test_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, 00)..Time.utc(2005, 12, 10, 18, 00)
@@ -95,37 +106,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..4b73233971 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
@@ -17,4 +19,24 @@ class SecureRandomTest < ActiveSupport::TestCase
assert_not_equal s1, s2
assert_equal 24, s1.length
end
+
+ def test_base36
+ s1 = SecureRandom.base36
+ s2 = SecureRandom.base36
+
+ assert_not_equal s1, s2
+ assert_equal 16, s1.length
+ assert_match(/^[a-z0-9]+$/, s1)
+ assert_match(/^[a-z0-9]+$/, s2)
+ end
+
+ def test_base36_with_length
+ s1 = SecureRandom.base36(24)
+ s2 = SecureRandom.base36(24)
+
+ assert_not_equal s1, s2
+ assert_equal 24, s1.length
+ assert_match(/^[a-z0-9]+$/, s1)
+ assert_match(/^[a-z0-9]+$/, s2)
+ end
end
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index f38b225b38..4ffa33aa61 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,12 @@ 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
@@ -95,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
@@ -151,54 +172,44 @@ class StringInflectionsTest < ActiveSupport::TestCase
def test_string_parameterized_normal
StringToParameterized.each do |normal, slugged|
- assert_equal(normal.parameterize, slugged)
+ assert_equal(slugged, normal.parameterize)
end
end
def test_string_parameterized_normal_preserve_case
StringToParameterizedPreserveCase.each do |normal, slugged|
- assert_equal(normal.parameterize(preserve_case: true), slugged)
+ assert_equal(slugged, normal.parameterize(preserve_case: true))
end
end
def test_string_parameterized_no_separator
StringToParameterizeWithNoSeparator.each do |normal, slugged|
- assert_equal(normal.parameterize(separator: ''), slugged)
- end
- end
-
- def test_string_parameterized_no_separator_deprecated
- StringToParameterizeWithNoSeparator.each do |normal, slugged|
- assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: ''` instead./i) do
- assert_equal(normal.parameterize(''), slugged)
- end
+ assert_equal(slugged, normal.parameterize(separator: ""))
end
end
def test_string_parameterized_no_separator_preserve_case
StringToParameterizePreserveCaseWithNoSeparator.each do |normal, slugged|
- assert_equal(normal.parameterize(separator: '', preserve_case: true), slugged)
+ assert_equal(slugged, normal.parameterize(separator: "", preserve_case: true))
end
end
def test_string_parameterized_underscore
StringToParameterizeWithUnderscore.each do |normal, slugged|
- assert_equal(normal.parameterize(separator: '_'), slugged)
+ assert_equal(slugged, normal.parameterize(separator: "_"))
end
end
- def test_string_parameterized_underscore_deprecated
- StringToParameterizeWithUnderscore.each do |normal, slugged|
- assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '_'` instead./i) do
- assert_equal(normal.parameterize('_'), slugged)
- end
+ def test_string_parameterized_underscore_preserve_case
+ StringToParameterizePreserveCaseWithUnderscore.each do |normal, slugged|
+ assert_equal(slugged, normal.parameterize(separator: "_", preserve_case: true))
end
end
- def test_string_parameterized_underscore_preserve_case
- StringToParameterizePreserceCaseWithUnderscore.each do |normal, slugged|
- assert_equal(normal.parameterize(separator: '_', preserve_case: true), slugged)
- end
+ def test_parameterize_with_locale
+ word = "Fünf autos"
+ I18n.backend.store_translations(:de, i18n: { transliterate: { rule: { "ü" => "ue" } } })
+ assert_equal("fuenf-autos", word.parameterize(locale: :de))
end
def test_humanize
@@ -213,31 +224,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:
@@ -252,8 +269,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
@@ -262,16 +279,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
@@ -280,37 +359,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
@@ -327,7 +406,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/)
@@ -344,7 +423,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
@@ -354,22 +433,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
@@ -380,40 +459,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
@@ -463,10 +560,17 @@ class StringConversionsTest < ActiveSupport::TestCase
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
@@ -625,14 +729,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
@@ -640,10 +744,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
@@ -653,7 +757,7 @@ end
class OutputSafetyTest < ActiveSupport::TestCase
def setup
- @string = "hello"
+ @string = +"hello"
@object = Class.new(Object) do
def to_s
"other"
@@ -662,28 +766,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
@@ -691,7 +795,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
@@ -700,7 +804,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
@@ -711,36 +815,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
@@ -748,22 +852,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
@@ -771,7 +875,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
@@ -779,7 +883,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
@@ -787,7 +891,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
@@ -795,7 +899,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
@@ -805,26 +909,74 @@ 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 "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)
+ 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
@@ -840,7 +992,7 @@ 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)
@@ -855,15 +1007,16 @@ class OutputSafetyTest < ActiveSupport::TestCase
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")
@@ -885,8 +1038,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
@@ -896,7 +1049,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
@@ -911,7 +1064,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 d8bb38621b..590b81b770 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -1,527 +1,548 @@
-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 { 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)
+ assert_equal Time.local(999, 10, 4, 15, 15, 10), Time.local(1000, 10, 4, 15, 15, 10).advance(years: -1)
+ assert_equal Time.local(1000, 10, 4, 15, 15, 10), Time.local(999, 10, 4, 15, 15, 10).advance(years: 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
@@ -550,28 +571,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
@@ -636,128 +662,124 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
end
- def test_last_month_on_31st
- assert_equal Time.local(2004, 2, 29), Time.local(2004, 3, 31).last_month
- end
-
def test_xmlschema_is_available
assert_nothing_raised { Time.now.xmlschema }
end
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
@@ -772,42 +794,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
@@ -817,121 +839,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 7acada011d..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,52 +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['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 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?
+ 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?
@@ -80,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
@@ -93,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
@@ -139,8 +151,18 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal "1999-12-31T19:00:00-05:00", @twz.xmlschema(nil)
end
+ def test_iso8601_with_fractional_seconds
+ @twz += Rational(1, 8)
+ assert_equal "1999-12-31T19:00:00.125-05:00", @twz.iso8601(3)
+ end
+
+ def test_rfc3339_with_fractional_seconds
+ @twz += Rational(1, 8)
+ assert_equal "1999-12-31T19:00:00.125-05:00", @twz.rfc3339(3)
+ end
+
def test_to_yaml
- yaml = <<-EOF.strip_heredoc
+ yaml = <<~EOF
--- !ruby/object:ActiveSupport::TimeWithZone
utc: 2000-01-01 00:00:00.000000000 Z
zone: !ruby/object:ActiveSupport::TimeZone
@@ -152,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
@@ -161,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
@@ -177,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
@@ -186,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
@@ -198,81 +220,95 @@ 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)
@@ -284,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
@@ -406,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
@@ -440,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
@@ -450,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
@@ -457,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
@@ -498,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
@@ -528,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
@@ -549,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
@@ -570,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
@@ -670,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
@@ -679,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
@@ -904,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
@@ -920,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
@@ -947,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
@@ -1068,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
@@ -1096,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
@@ -1109,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
@@ -1139,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
@@ -1158,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
@@ -1192,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..adbdc646bc
--- /dev/null
+++ b/activesupport/test/current_attributes_test.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class CurrentAttributesTest < ActiveSupport::TestCase
+ Person = Struct.new(:id, :name, :time_zone)
+
+ class Current < ActiveSupport::CurrentAttributes
+ attribute :world, :account, :person, :request
+ delegate :time_zone, to: :person
+
+ before_reset { Session.previous = person.try(:id) }
+
+ resets do
+ Time.zone = "UTC"
+ Session.current = nil
+ end
+
+ def account=(account)
+ super
+ self.person = "#{account}'s person"
+ end
+
+ def person=(person)
+ super
+ Time.zone = person.try(:time_zone)
+ Session.current = person.try(:id)
+ end
+
+ def request
+ "#{super} something"
+ end
+
+ def intro
+ "#{person.name}, in #{time_zone}"
+ end
+ end
+
+ class Session < ActiveSupport::CurrentAttributes
+ attribute :current, :previous
+ end
+
+ setup do
+ @original_time_zone = Time.zone
+ Current.reset
+ Session.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(42, "David", "Central Time (US & Canada)")
+ assert_equal "Central Time (US & Canada)", Time.zone.name
+ assert_equal 42, Session.current
+ end
+
+ test "resets auxiliary classes via callback" do
+ Current.person = Person.new(42, "David", "Central Time (US & Canada)")
+ assert_equal "Central Time (US & Canada)", Time.zone.name
+
+ Current.reset
+ assert_equal "UTC", Time.zone.name
+ assert_nil Session.current
+ end
+
+ test "set auxiliary class based on current attributes via before callback" do
+ Current.person = Person.new(42, "David", "Central Time (US & Canada)")
+ assert_nil Session.previous
+ assert_equal 42, Session.current
+
+ Current.reset
+ assert_equal 42, Session.previous
+ assert_nil Session.current
+ end
+
+ test "set attribute only via scope" do
+ 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(42, "David", "Central Time (US & Canada)")
+ assert_equal "Central Time (US & Canada)", Current.time_zone
+ assert_equal "Central Time (US & Canada)", Current.instance.time_zone
+ end
+
+ test "all methods forward to the instance" do
+ Current.person = Person.new(42, "David", "Central Time (US & Canada)")
+ assert_equal "David, in Central Time (US & Canada)", Current.intro
+ assert_equal "David, in Central Time (US & Canada)", Current.instance.intro
+ end
+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 04e7b24d30..d4e709137e 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,16 +77,16 @@ 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?(expanded)
@@ -95,16 +97,16 @@ class DependenciesTest < ActiveSupport::TestCase
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)
@@ -112,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
@@ -167,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
@@ -175,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 }
@@ -183,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
@@ -225,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 # assignment to silence parse-time warning "possibly useless use of :: in void context"
+
+ 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 # assignment to silence parse-time warning "possibly useless use of a constant in void context"
+
+ 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
@@ -253,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)
@@ -271,7 +354,8 @@ class DependenciesTest < ActiveSupport::TestCase
def test_raising_discards_autoloaded_constants
with_autoloading_fixtures do
- assert_raises(Exception, 'arbitray exception message') { RaisesArbitraryException }
+ e = assert_raises(Exception) { RaisesArbitraryException }
+ assert_equal("arbitrary exception message", e.message)
assert_not defined?(A)
assert_not defined?(RaisesArbitraryException)
end
@@ -292,7 +376,7 @@ class DependenciesTest < ActiveSupport::TestCase
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
@@ -311,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)
@@ -331,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)
@@ -344,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)
@@ -358,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)
@@ -373,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)
@@ -393,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
@@ -429,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
@@ -474,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
@@ -486,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)
@@ -503,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)
@@ -524,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
@@ -584,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
@@ -596,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)
@@ -610,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")
@@ -625,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
@@ -633,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
@@ -659,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
@@ -677,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
@@ -687,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
@@ -718,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
@@ -747,24 +833,24 @@ 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)
end
- def test_new_contants_in_without_constants
+ def test_new_constants_in_without_constants
assert_equal [], (ActiveSupport::Dependencies.new_constants_in(Object) { })
- assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? }
+ 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
@@ -781,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
@@ -800,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
@@ -818,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
@@ -832,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
@@ -841,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)
@@ -881,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)
@@ -920,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
@@ -932,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
@@ -959,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
@@ -974,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)
@@ -986,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"
@@ -1001,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
@@ -1036,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? }
@@ -1060,14 +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
+
+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.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
index 9a4ca2b217..18729941bc 100644
--- a/activesupport/test/deprecation/method_wrappers_test.rb
+++ b/activesupport/test/deprecation/method_wrappers_test.rb
@@ -1,17 +1,36 @@
-require 'abstract_unit'
-require 'active_support/deprecation'
+# 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_without_alternate_method
+ warning = /old_method is deprecated and will be removed from Rails \d.\d./
+ ActiveSupport::Deprecation.deprecate_methods(@klass, :old_method)
+
+ assert_deprecated(warning) { assert_equal "abc", @klass.new.old_method }
+ 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)
+ ActiveSupport::Deprecation.deprecate_methods(@klass, old_method: :new_method)
assert_deprecated(warning) { assert_equal "abc", @klass.new.old_method }
end
@@ -19,7 +38,7 @@ class MethodWrappersTest < ActiveSupport::TestCase
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)
+ ActiveSupport::Deprecation.deprecate_methods(@klass, old_method: :new_method, deprecator: deprecator)
assert_deprecated(warning, deprecator) { assert_equal "abc", @klass.new.old_method }
end
@@ -27,8 +46,55 @@ class MethodWrappersTest < ActiveSupport::TestCase
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)
+ 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 45c88b79cb..f25c704586 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,17 +26,31 @@ 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=
+ deprecate :g
+ def g; end
+
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 +90,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,26 +105,28 @@ 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!'
+ 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.map(&:to_s), e.backtrace.map(&:to_s)
@@ -119,7 +137,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 +147,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 +159,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 +206,21 @@ 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
@@ -178,12 +236,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
@@ -191,17 +249,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
@@ -280,12 +338,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
@@ -341,7 +409,7 @@ class DeprecationTest < ActiveSupport::TestCase
end
def test_default_deprecation_horizon_should_always_bigger_than_current_rails_version
- assert ActiveSupport::Deprecation.new.deprecation_horizon > ActiveSupport::VERSION::STRING
+ assert_operator ActiveSupport::Deprecation.new.deprecation_horizon, :>, ActiveSupport::VERSION::STRING
end
def test_default_gem_name
@@ -353,22 +421,25 @@ 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)
end
end
+ def test_deprecate_work_before_define_method
+ assert_deprecated { @dtc.g }
+ end
+
private
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..f8752688d2 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
@@ -25,6 +27,15 @@ module DescendantsTrackerTestCases
assert_equal_sets [], Child2.descendants
end
+ def test_descendants_with_garbage_collected_classes
+ 1.times do
+ child_klass = Class.new(Parent)
+ assert_equal_sets [Child1, Grandchild1, Grandchild2, Child2, child_klass], Parent.descendants
+ end
+ GC.start
+ assert_equal_sets [Child1, Grandchild1, Grandchild2, Child2], Parent.descendants
+ end
+
def test_direct_descendants
assert_equal_sets [Child1, Child2], Parent.direct_descendants
assert_equal_sets [Grandchild1, Grandchild2], Child1.direct_descendants
@@ -35,31 +46,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
+ private
- def assert_equal_sets(expected, actual)
- assert_equal Set.new(expected), Set.new(actual)
- end
-
- 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
index bc3f77bd54..b2d5eb94c2 100644
--- a/activesupport/test/evented_file_update_checker_test.rb
+++ b/activesupport/test/evented_file_update_checker_test.rb
@@ -1,17 +1,20 @@
-require 'abstract_unit'
-require 'pathname'
-require 'file_update_checker_shared_tests'
+# 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'
+ skip if ENV["LISTEN"] == "0"
+ require "listen"
super
end
def new_checker(files = [], dirs = {}, &block)
- ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do
+ ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do |c|
wait
end
end
@@ -30,9 +33,76 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase
wait # wait for the events to fire
end
- def rm_f(files)
- super
- wait
+ 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
+
+ test "updated should become true when nonexistent directory is added later" do
+ Dir.mktmpdir do |dir|
+ watched_dir = File.join(dir, "app")
+ unwatched_dir = File.join(dir, "node_modules")
+ not_exist_watched_dir = File.join(dir, "test")
+
+ Dir.mkdir(watched_dir)
+ Dir.mkdir(unwatched_dir)
+
+ checker = new_checker([], watched_dir => ".rb", not_exist_watched_dir => ".rb") { }
+
+ FileUtils.touch(File.join(watched_dir, "a.rb"))
+ wait
+ assert_predicate checker, :updated?
+ assert checker.execute_if_updated
+
+ Dir.mkdir(not_exist_watched_dir)
+ wait
+ assert_predicate checker, :updated?
+ assert checker.execute_if_updated
+
+ FileUtils.touch(File.join(unwatched_dir, "a.rb"))
+ wait
+ assert_not_predicate checker, :updated?
+ assert_not checker.execute_if_updated
+ end
end
end
@@ -45,33 +115,33 @@ class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase
@ph = ActiveSupport::EventedFileUpdateChecker::PathHelper.new
end
- test '#xpath returns the expanded path as a Pathname object' do
+ 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')
+ 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')
+ 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)
+ 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
+ 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)
+ assert_equal pn("/foo"), @ph.longest_common_subpath(paths)
end
- test '#longest_common_subpath returns the root directory as an edge case' do
+ test "#longest_common_subpath returns the root directory as an edge case" do
paths = %w(
/foo/bar
/foo/baz
@@ -79,30 +149,30 @@ class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase
/wadus
).map { |path| pn(path) }
- assert_equal pn('/'), @ph.longest_common_subpath(paths)
+ assert_equal pn("/"), @ph.longest_common_subpath(paths)
end
- test '#longest_common_subpath returns nil for an empty collection' do
+ 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
+ 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'))
+ 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
+ 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'])
+ 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
+ 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
@@ -112,7 +182,7 @@ class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase
assert_equal paths, @ph.filter_out_descendants(paths)
end
- test '#filter_out_descendants filters out descendants preserving order' do
+ test "#filter_out_descendants filters out descendants preserving order" do
paths = %w(
/Rails.root/app/controllers
/Rails.root/app/controllers/concerns
@@ -124,7 +194,7 @@ class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase
assert_equal paths.values_at(0, 2, 4), @ph.filter_out_descendants(paths)
end
- test '#filter_out_descendants works on path units' do
+ test "#filter_out_descendants works on path units" do
paths = %w(
/foo/bar
/foo/barrrr
@@ -133,7 +203,7 @@ class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase
assert_equal paths, @ph.filter_out_descendants(paths)
end
- test '#filter_out_descendants deals correctly with the root directory' do
+ test "#filter_out_descendants deals correctly with the root directory" do
paths = %w(
/
/foo
@@ -143,7 +213,7 @@ class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase
assert_equal paths.values_at(0), @ph.filter_out_descendants(paths)
end
- test '#filter_out_descendants preserves duplicates' do
+ test "#filter_out_descendants preserves duplicates" do
paths = %w(
/foo
/foo/bar
diff --git a/activesupport/test/executor_test.rb b/activesupport/test/executor_test.rb
index 6db6db4fa8..3026f002c3 100644
--- a/activesupport/test/executor_test.rb
+++ b/activesupport/test/executor_test.rb
@@ -1,6 +1,11 @@
-require 'abstract_unit'
+# 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 }
@@ -35,6 +40,20 @@ class ExecutorTest < ActiveSupport::TestCase
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 }
@@ -51,6 +70,153 @@ class ExecutorTest < ActiveSupport::TestCase
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)
diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb
index 5207860a0e..84d89fa0a7 100644
--- a/activesupport/test/file_update_checker_shared_tests.rb
+++ b/activesupport/test/file_update_checker_shared_tests.rb
@@ -1,4 +1,6 @@
-require 'fileutils'
+# frozen_string_literal: true
+
+require "fileutils"
module FileUpdateCheckerSharedTests
extend ActiveSupport::Testing::Declarative
@@ -9,7 +11,7 @@ module FileUpdateCheckerSharedTests
end
def tmpfile(name)
- "#{tmpdir}/#{name}"
+ File.join(tmpdir, name)
end
def tmpfiles
@@ -17,42 +19,45 @@ module FileUpdateCheckerSharedTests
end
def run(*args)
- Dir.mktmpdir(nil, __dir__) { |dir| @tmpdir = dir; super }
+ 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' }
+ test "should not execute the block if no paths are given" do
+ silence_warnings { require "listen" }
i = 0
checker = new_checker { i += 1 }
- assert !checker.execute_if_updated
+ assert_not checker.execute_if_updated
assert_equal 0, i
end
- test 'should not execute the block if no files change' do
+ test "should not execute the block if no files change" do
i = 0
FileUtils.touch(tmpfiles)
checker = new_checker(tmpfiles) { i += 1 }
- assert !checker.execute_if_updated
+ assert_not checker.execute_if_updated
assert_equal 0, i
end
- test 'should execute the block once when files are created' do
+ 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
+ test "should execute the block once when files are modified" do
i = 0
FileUtils.touch(tmpfiles)
@@ -60,12 +65,13 @@ module FileUpdateCheckerSharedTests
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
+ test "should execute the block once when files are deleted" do
i = 0
FileUtils.touch(tmpfiles)
@@ -73,51 +79,53 @@ module FileUpdateCheckerSharedTests
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
+ test "updated should become true when watched files are created" do
i = 0
checker = new_checker(tmpfiles) { i += 1 }
- assert !checker.updated?
+ assert_not_predicate checker, :updated?
touch(tmpfiles)
+ wait
- assert checker.updated?
+ assert_predicate checker, :updated?
end
-
- test 'updated should become true when watched files are modified' do
+ test "updated should become true when watched files are modified" do
i = 0
FileUtils.touch(tmpfiles)
checker = new_checker(tmpfiles) { i += 1 }
- assert !checker.updated?
+ assert_not_predicate checker, :updated?
touch(tmpfiles)
+ wait
- assert checker.updated?
+ assert_predicate checker, :updated?
end
- test 'updated should become true when watched files are deleted' do
+ test "updated should become true when watched files are deleted" do
i = 0
FileUtils.touch(tmpfiles)
checker = new_checker(tmpfiles) { i += 1 }
- assert !checker.updated?
+ assert_not_predicate checker, :updated?
rm_f(tmpfiles)
+ wait
- assert checker.updated?
+ assert_predicate checker, :updated?
end
- test 'should be robust to handle files with wrong modified time' do
+ test "should be robust to handle files with wrong modified time" do
i = 0
FileUtils.touch(tmpfiles)
@@ -129,114 +137,160 @@ module FileUpdateCheckerSharedTests
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
+ test "should cache updated result until execute" do
i = 0
checker = new_checker(tmpfiles) { i += 1 }
- assert !checker.updated?
+ assert_not_predicate checker, :updated?
touch(tmpfiles)
+ wait
- assert checker.updated?
+ assert_predicate checker, :updated?
checker.execute
- assert !checker.updated?
+ assert_not_predicate checker, :updated?
end
- test 'should execute the block if files change in a watched directory one extension' do
+ 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'))
+ 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
+ test "should execute the block if files change in a watched directory any extensions" do
+ i = 0
+
+ checker = new_checker([], tmpdir => []) { 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'))
+ touch(tmpfile("foo.rb"))
+ wait
assert checker.execute_if_updated
assert_equal 1, i
- touch(tmpfile('foo.txt'))
+ 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
+ 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'))
+ touch(tmpfile("foo.rb"))
+ wait
- assert !checker.execute_if_updated
+ assert_not checker.execute_if_updated
assert_equal 0, i
end
- test 'does not assume files exist on instantiation' do
+ test "does not assume files exist on instantiation" do
i = 0
- non_existing = tmpfile('non_existing.rb')
+ 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
+ test "detects files in new subdirectories" do
i = 0
checker = new_checker([], tmpdir => :rb) { i += 1 }
- subdir = tmpfile('subdir')
+ subdir = tmpfile("subdir")
mkdir(subdir)
wait
- assert !checker.execute_if_updated
+ assert_not checker.execute_if_updated
assert_equal 0, i
- touch("#{subdir}/nested.rb")
+ 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
+ test "looked up extensions are inherited in subdirectories not listening to them" do
i = 0
- subdir = tmpfile('subdir')
+ subdir = tmpfile("subdir")
mkdir(subdir)
checker = new_checker([], tmpdir => :rb, subdir => :txt) { i += 1 }
- touch(tmpfile('new.txt'))
+ touch(tmpfile("new.txt"))
+ wait
- assert !checker.execute_if_updated
+ assert_not checker.execute_if_updated
assert_equal 0, i
# subdir does not look for Ruby files, but its parent tmpdir does.
- touch("#{subdir}/nested.rb")
+ touch(File.join(subdir, "nested.rb"))
+ wait
assert checker.execute_if_updated
assert_equal 1, i
- touch("#{subdir}/nested.txt")
+ 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 752f7836cd..ec1df0df67 100644
--- a/activesupport/test/file_update_checker_test.rb
+++ b/activesupport/test/file_update_checker_test.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'file_update_checker_shared_tests'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "file_update_checker_shared_tests"
class FileUpdateCheckerTest < ActiveSupport::TestCase
include FileUpdateCheckerSharedTests
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/fixtures/concern/some_concern.rb b/activesupport/test/fixtures/concern/some_concern.rb
new file mode 100644
index 0000000000..87f660a81e
--- /dev/null
+++ b/activesupport/test/fixtures/concern/some_concern.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require "active_support/concern"
+
+module SomeConcern
+ extend ActiveSupport::Concern
+
+ included do
+ # shouldn't raise when module is loaded more than once
+ end
+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..1d6a07ec5f
--- /dev/null
+++ b/activesupport/test/hash_with_indifferent_access_test.rb
@@ -0,0 +1,839 @@
+# 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_assoc
+ indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
+ key, value = indifferent_strings.assoc(:a)
+
+ assert_equal("a", key)
+ assert_equal(1, value)
+ end
+
+ def test_indifferent_compact
+ hash_contain_nil_value = @strings.merge("z" => nil)
+ hash = ActiveSupport::HashWithIndifferentAccess.new(hash_contain_nil_value)
+ 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 HashWithIndifferentAccess
+ new_to_hash = @nested_mixed.with_indifferent_access.to_hash
+ assert_not new_to_hash.instance_of?(HashWithIndifferentAccess)
+ assert_not new_to_hash["a"].instance_of?(HashWithIndifferentAccess)
+ 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 06cd41c86d..ddf765a220 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
@@ -246,56 +280,34 @@ class InflectorTest < ActiveSupport::TestCase
end
end
-# FIXME: get following tests to pass on jruby, currently skipped
-#
-# Currently this fails because ActiveSupport::Multibyte::Unicode#tidy_bytes
-# required a specific Encoding::Converter(UTF-8 to UTF8-MAC) which unavailable on JRuby
-# causing our tests to error out.
-# related bug http://jira.codehaus.org/browse/JRUBY-7194
def test_parameterize
- jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterized.each do |some_string, parameterized_string|
assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string))
end
end
def test_parameterize_and_normalize
- jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterizedAndNormalized.each do |some_string, parameterized_string|
assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string))
end
end
def test_parameterize_with_custom_separator
- jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterizeWithUnderscore.each do |some_string, parameterized_string|
- assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, separator: '_'))
- end
- end
-
- def test_parameterize_with_custom_separator_deprecated
- jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
- StringToParameterizeWithUnderscore.each do |some_string, parameterized_string|
- assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '_'` instead./i) do
- assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, '_'))
- end
+ assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, separator: "_"))
end
end
def test_parameterize_with_multi_character_separator
- jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterized.each do |some_string, parameterized_string|
- assert_equal(parameterized_string.gsub('-', '__sep__'), ActiveSupport::Inflector.parameterize(some_string, separator: '__sep__'))
+ assert_equal(parameterized_string.gsub("-", "__sep__"), ActiveSupport::Inflector.parameterize(some_string, separator: "__sep__"))
end
end
- def test_parameterize_with_multi_character_separator_deprecated
- jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
- StringToParameterized.each do |some_string, parameterized_string|
- assert_deprecated(/Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '__sep__'` instead./i) do
- assert_equal(parameterized_string.gsub('-', '__sep__'), ActiveSupport::Inflector.parameterize(some_string, '__sep__'))
- end
- end
+ def test_parameterize_with_locale
+ word = "Fünf autos"
+ I18n.backend.store_translations(:de, i18n: { transliterate: { rule: { "ü" => "ue" } } })
+ assert_equal("fuenf-autos", ActiveSupport::Inflector.parameterize(word, locale: :de))
end
def test_classify
@@ -307,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
@@ -327,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')
@@ -344,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)
@@ -403,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
@@ -435,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
@@ -452,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
@@ -526,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 14fe97a986..689370cccf 100644
--- a/activesupport/test/inflector_test_cases.rb
+++ b/activesupport/test/inflector_test_cases.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module InflectorTestCases
SingularToPlural = {
"search" => "searches",
@@ -128,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 = {
@@ -176,12 +178,12 @@ module InflectorTestCases
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",
+ "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"
}
@@ -199,11 +201,11 @@ module InflectorTestCases
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",
+ "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"
}
@@ -219,15 +221,15 @@ module InflectorTestCases
"Test with malformed utf8 \251" => "test_with_malformed_utf8"
}
- StringToParameterizePreserceCaseWithUnderscore = {
- "Donald E. Knuth" => "Donald_E_Knuth",
+ 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",
+ "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"
}
@@ -237,15 +239,24 @@ module InflectorTestCases
"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 = {
@@ -254,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 = {
@@ -344,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..7589ffd0ea 100644
--- a/activesupport/test/json/encoding_test.rb
+++ b/activesupport/test/json/encoding_test.rb
@@ -1,33 +1,38 @@
-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|
define_method("test_#{class_tests[0..-6].underscore}") do
- 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/
- JSONTest::EncodingTestCases.const_get(class_tests).each do |pair|
- assert_equal pair.last, sorted_json(ActiveSupport::JSON.encode(pair.first))
- end
- ensure
- ActiveSupport.escape_html_entities_in_json = false
- ActiveSupport.use_standard_json_time_format = prev
+ prev = ActiveSupport.use_standard_json_time_format
+
+ 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
+ ensure
+ ActiveSupport.escape_html_entities_in_json = false
+ ActiveSupport.use_standard_json_time_format = prev
end
end
@@ -41,12 +46,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 +62,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 +88,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 +122,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 +132,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 +207,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 +232,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 +265,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 +276,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 +286,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 +299,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 +308,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 +325,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 +350,7 @@ class TestJSONEncoding < ActiveSupport::TestCase
super
end
- def as_json(options={})
+ def as_json(options = {})
@as_json_called = true
super
end
@@ -376,7 +390,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 +398,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 +407,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 +431,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 5a91420f1e..160e1156b6 100644
--- a/activesupport/test/logger_test.rb
+++ b/activesupport/test/logger_test.rb
@@ -1,9 +1,12 @@
-require 'abstract_unit'
-require 'multibyte_test_helpers'
-require 'stringio'
-require 'fileutils'
-require 'tempfile'
-require 'concurrent/atomics'
+# 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
@@ -26,18 +29,18 @@ class LoggerTest < ActiveSupport::TestCase
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
@@ -47,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
@@ -67,7 +70,7 @@ 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
@@ -78,32 +81,32 @@ class LoggerTest < ActiveSupport::TestCase
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
@@ -115,7 +118,7 @@ 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
@@ -125,10 +128,10 @@ class LoggerTest < ActiveSupport::TestCase
@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
@@ -138,27 +141,28 @@ class LoggerTest < ActiveSupport::TestCase
end
assert_not @output.string.include?("NOT THERE")
- assert @output.string.include?("THIS IS HERE")
+ assert_includes @output.string, "THIS IS HERE"
end
def test_logger_silencing_works_for_broadcast
another_output = StringIO.new
- another_logger = Logger.new(another_output)
+ another_logger = ActiveSupport::Logger.new(another_output)
- @logger.extend Logger.broadcast(another_logger)
+ @logger.extend ActiveSupport::Logger.broadcast(another_logger)
@logger.debug "CORRECT DEBUG"
- @logger.silence do
+ @logger.silence do |logger|
+ assert_kind_of ActiveSupport::Logger, logger
@logger.debug "FAILURE"
@logger.error "CORRECT ERROR"
end
- assert @output.string.include?("CORRECT DEBUG")
- assert @output.string.include?("CORRECT ERROR")
+ assert_includes @output.string, "CORRECT DEBUG"
+ assert_includes @output.string, "CORRECT ERROR"
assert_not @output.string.include?("FAILURE")
- assert another_output.string.include?("CORRECT DEBUG")
- assert another_output.string.include?("CORRECT ERROR")
+ assert_includes another_output.string, "CORRECT DEBUG"
+ assert_includes another_output.string, "CORRECT ERROR"
assert_not another_output.string.include?("FAILURE")
end
@@ -166,21 +170,22 @@ class LoggerTest < ActiveSupport::TestCase
another_output = StringIO.new
another_logger = ::Logger.new(another_output)
- @logger.extend Logger.broadcast(another_logger)
+ @logger.extend ActiveSupport::Logger.broadcast(another_logger)
@logger.debug "CORRECT DEBUG"
- @logger.silence do
+ @logger.silence do |logger|
+ assert_kind_of ActiveSupport::Logger, logger
@logger.debug "FAILURE"
@logger.error "CORRECT ERROR"
end
- assert @output.string.include?("CORRECT DEBUG")
- assert @output.string.include?("CORRECT ERROR")
+ assert_includes @output.string, "CORRECT DEBUG"
+ assert_includes @output.string, "CORRECT ERROR"
assert_not @output.string.include?("FAILURE")
- assert another_output.string.include?("CORRECT DEBUG")
- assert another_output.string.include?("CORRECT ERROR")
- assert another_output.string.include?("FAILURE")
+ 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
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 c1e0b19248..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,68 +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_equal nil, @chars.mb_chars.slice!(9..10)
+ assert_nil @chars.mb_chars.slice!(9..10)
end
def test_slice_bang_removes_the_slice_from_the_receiver
- 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 = 'úüù'
+ string = +"úüù"
chars = string.mb_chars
assert_nil chars.slice!(4, 5)
- assert_equal 'úüù', chars
- assert_equal 'úüù', string
+ assert_equal "úüù", chars
+ assert_equal "úüù", string
end
def test_slice_should_throw_exceptions_on_invalid_arguments
@@ -442,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
@@ -495,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
@@ -527,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
@@ -537,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
@@ -565,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
@@ -575,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
@@ -599,19 +617,21 @@ 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
@@ -664,7 +684,6 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
end
def test_tidy_bytes_should_tidy_bytes
-
single_byte_cases = {
"\x21" => "!", # Valid ASCII byte, low
"\x41" => "A", # Valid ASCII byte, mid
@@ -699,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
@@ -718,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, :spacingmark => 0x0903, :r => 0x1F1E6, :control => 0x0001
- }
- 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 5df8f32e46..b19689723f 100644
--- a/activesupport/test/multibyte_conformance_test.rb
+++ b/activesupport/test/multibyte_conformance_test.rb
@@ -1,35 +1,12 @@
-require 'abstract_unit'
-require 'multibyte_test_helpers'
+# frozen_string_literal: true
-require 'fileutils'
-require 'open-uri'
-require 'tmpdir'
+require "abstract_unit"
+require "multibyte_test_helpers"
class MultibyteConformanceTest < ActiveSupport::TestCase
- 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
-
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
@@ -41,80 +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
+ 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}"
+ # 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)
- File.open(File.join(CACHE_DIR, UNIDATA_FILE), 'r') do | f |
+ 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)
@@ -123,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
index 229f24990e..97963279af 100644
--- a/activesupport/test/multibyte_grapheme_break_conformance_test.rb
+++ b/activesupport/test/multibyte_grapheme_break_conformance_test.rb
@@ -1,65 +1,49 @@
-# encoding: utf-8
+# frozen_string_literal: true
-require 'abstract_unit'
-
-require 'fileutils'
-require 'open-uri'
-require 'tmpdir'
+require "abstract_unit"
+require "multibyte_test_helpers"
class MultibyteGraphemeBreakConformanceTest < ActiveSupport::TestCase
- class Downloader
- def self.download(from, to)
- unless File.exist?(to)
- $stderr.puts "Downloading #{from} to #{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
- end
- end
+ include MultibyteTestHelpers
- TEST_DATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd/auxiliary"
- TEST_DATA_FILE = '/GraphemeBreakTest.txt'
- CACHE_DIR = File.join(Dir.tmpdir, 'cache')
+ UNIDATA_FILE = "/auxiliary/GraphemeBreakTest.txt"
+ RUN_P = begin
+ Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE)
+ rescue
+ end
def setup
- FileUtils.mkdir_p(CACHE_DIR)
- Downloader.download(TEST_DATA_URL + TEST_DATA_FILE, CACHE_DIR + TEST_DATA_FILE)
+ skip "Unable to download test data" unless RUN_P
end
def test_breaks
- 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
+ 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
- protected
+ private
def each_line_of_break_tests(&block)
lines = 0
max_test_lines = 0 # Don't limit below 21, because that's the header of the testfile
- File.open(File.join(CACHE_DIR, TEST_DATA_FILE), 'r') do | f |
- until f.eof? || (max_test_lines > 21 and lines > max_test_lines)
+ 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 =~ /^\#/)
+ 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 = 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 = 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)}
+ codepoints.map { |codepoint| codepoint.to_i(16) }
end
# The tests contain a solitary U+D800 <Non Private Use High
diff --git a/activesupport/test/multibyte_normalization_conformance_test.rb b/activesupport/test/multibyte_normalization_conformance_test.rb
index 8bc91ef708..82edf69294 100644
--- a/activesupport/test/multibyte_normalization_conformance_test.rb
+++ b/activesupport/test/multibyte_normalization_conformance_test.rb
@@ -1,121 +1,108 @@
-# encoding: utf-8
+# frozen_string_literal: true
-require 'abstract_unit'
-require 'multibyte_test_helpers'
-
-require 'fileutils'
-require 'open-uri'
-require 'tmpdir'
+require "abstract_unit"
+require "multibyte_test_helpers"
class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase
- class Downloader
- def self.download(from, to)
- unless File.exist?(to)
- $stderr.puts "Downloading #{from} to #{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
- end
- end
-
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')
+ UNIDATA_FILE = "/NormalizationTest.txt"
+ RUN_P = begin
+ Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE)
+ rescue
+ end
def setup
- FileUtils.mkdir_p(CACHE_DIR)
- Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE)
@proxy = ActiveSupport::Multibyte::Chars
+ skip "Unable to download test data" unless RUN_P
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)
+ 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 =~ /^\#/)
+ next if line.empty? || line.start_with?("#")
cols, comment = line.split("#")
- cols = cols.split(";").map{|e| e.strip}.reject{|e| e.empty? }
+ 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.map! { |c| c.split.map { |codepoint| codepoint.to_i(16) }.pack("U*") }
cols << comment
yield(*cols)
@@ -124,6 +111,6 @@ class MultibyteNormalizationConformanceTest < 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_proxy_test.rb b/activesupport/test/multibyte_proxy_test.rb
index 360cf57302..ecedab2569 100644
--- a/activesupport/test/multibyte_proxy_test.rb
+++ b/activesupport/test/multibyte_proxy_test.rb
@@ -1,4 +1,6 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class MultibyteProxyText < ActiveSupport::TestCase
class AsciiOnlyEncoder
@@ -6,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 58cf5488cd..7565655f25 100644
--- a/activesupport/test/multibyte_test_helpers.rb
+++ b/activesupport/test/multibyte_test_helpers.rb
@@ -1,17 +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..ab2a9b8659 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,64 @@ 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, {}
+
+ assert_equal [
+ [:start, "hi", 1, {}],
+ [:finish, "hi", 1, {}]
+ ], listener.events
+ end
- notifier.start 'hi', 1, {}
- notifier.finish 'hi', 1, {}
+ def test_listen_to_regexp
+ notifier = Fanout.new
+ listener = Listener.new
+ notifier.subscribe(/[a-z]*.world/, listener)
+ notifier.start("hi.world", 1, {})
+ notifier.finish("hi.world", 2, {})
+ notifier.start("hello.world", 1, {})
+ notifier.finish("hello.world", 2, {})
+
+ assert_equal [
+ [:start, "hi.world", 1, {}],
+ [:finish, "hi.world", 2, {}],
+ [:start, "hello.world", 1, {}],
+ [:finish, "hello.world", 2, {}]
+ ], listener.events
+ end
+
+ def test_listen_to_regexp_with_exclusions
+ notifier = Fanout.new
+ listener = Listener.new
+ notifier.subscribe(/[a-z]*.world/, listener)
+ notifier.unsubscribe("hi.world")
+ notifier.start("hi.world", 1, {})
+ notifier.finish("hi.world", 2, {})
+ notifier.start("hello.world", 1, {})
+ notifier.finish("hello.world", 2, {})
assert_equal [
- [:start, 'hi', 1, {}],
- [:finish, 'hi', 1, {}]
+ [:start, "hello.world", 1, {}],
+ [:finish, "hello.world", 2, {}]
], listener.events
end
end
diff --git a/activesupport/test/notifications/instrumenter_test.rb b/activesupport/test/notifications/instrumenter_test.rb
index f46e96f636..9729ad5c89 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,25 @@ 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_instrument_works_without_a_block
+ instrumenter.instrument("no.block", payload)
+ assert_equal 1, notifier.finishes.size
+ assert_equal "no.block", notifier.finishes.first.first
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 1cb17e6197..bb20d26a25 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 SubscribeEventObjectsTest < 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)
@@ -43,16 +81,16 @@ module Notifications
assert_equal expected, events
end
- def test_subsribing_to_instrumentation_while_inside_it
+ def test_subscribing_to_instrumentation_while_inside_it
# the repro requires that there are no evented subscribers for the "foo" event,
# so we have to duplicate some of the setup code
old_notifier = ActiveSupport::Notifications.notifier
ActiveSupport::Notifications.notifier = ActiveSupport::Notifications::Fanout.new
- ActiveSupport::Notifications.subscribe('foo', TestSubscriber.new)
+ ActiveSupport::Notifications.subscribe("foo", TestSubscriber.new)
- ActiveSupport::Notifications.instrument('foo') do
- ActiveSupport::Notifications.subscribe('foo') {}
+ ActiveSupport::Notifications.instrument("foo") do
+ ActiveSupport::Notifications.subscribe("foo") { }
end
ensure
ActiveSupport::Notifications.notifier = old_notifier
@@ -90,6 +128,25 @@ module Notifications
assert_equal [["named.subscription", :foo], ["named.subscription", :foo]], @events
end
+ def test_unsubscribing_by_name_leaves_regexp_matched_subscriptions
+ @matched_events = []
+ @notifier.subscribe(/subscription/) { |*args| @matched_events << event(*args) }
+ @notifier.publish("named.subscription", :before)
+ @notifier.wait
+ [@events, @named_events, @matched_events].each do |collector|
+ assert_includes(collector, ["named.subscription", :before])
+ end
+ @notifier.unsubscribe("named.subscription")
+ @notifier.publish("named.subscription", :after)
+ @notifier.publish("other.subscription", :after)
+ @notifier.wait
+ assert_includes(@events, ["named.subscription", :after])
+ assert_includes(@events, ["other.subscription", :after])
+ assert_includes(@matched_events, ["other.subscription", :after])
+ assert_not_includes(@matched_events, ["named.subscription", :after])
+ assert_not_includes(@named_events, ["named.subscription", :after])
+ end
+
private
def event(*args)
args
@@ -141,26 +198,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
@@ -188,7 +245,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 }
@@ -198,7 +255,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
@@ -206,24 +263,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
@@ -231,15 +288,15 @@ module Notifications
end
assert_equal 1, @events.size
- assert_equal Hash[:payload => "notifications",
- :exception => ["RuntimeError", "FAIL"], :exception_object => e], @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
@@ -248,14 +305,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
@@ -268,12 +325,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 6696111476..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
@@ -46,17 +47,19 @@ module ActiveSupport
[@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
@@ -65,28 +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("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))
@@ -120,9 +123,9 @@ module ActiveSupport
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
@@ -130,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
@@ -208,156 +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 '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_si_prefix
- assert_deprecated do
- [@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 '1.23 PB', number_helper.number_to_human_size(1234567890123456, :prefix => :si)
- assert_equal '1.23 EB', number_helper.number_to_human_size(1234567890123456789, :prefix => :si)
- end
+ 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 '1 Million', number_helper.number_to_human(999999)
- assert_equal '1 Billion', number_helper.number_to_human(999999999)
+ 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
@@ -375,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"))
@@ -410,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
index 958cb49993..1b7cc253d9 100644
--- a/activesupport/test/reloader_test.rb
+++ b/activesupport/test/reloader_test.rb
@@ -1,20 +1,34 @@
-require 'abstract_unit'
+# frozen_string_literal: true
+
+require "abstract_unit"
class ReloaderTest < ActiveSupport::TestCase
def test_prepare_callback
- prepared = false
+ prepared = completed = false
reloader.to_prepare { prepared = true }
+ reloader.to_complete { completed = true }
- assert !prepared
+ assert_not prepared
+ assert_not completed
reloader.prepare!
assert prepared
+ assert_not completed
prepared = false
reloader.wrap do
assert prepared
prepared = false
end
- assert !prepared
+ 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
@@ -28,7 +42,7 @@ class ReloaderTest < ActiveSupport::TestCase
invoked = false
r.to_run { invoked = true }
r.wrap { }
- assert !invoked
+ assert_not invoked
end
def test_full_reload_sequence
diff --git a/activesupport/test/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..b1a1c2d390 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,101 @@ 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?
+ {
+ 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 #{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 "can assign value into zero-index" do
+ buffer = ActiveSupport::SafeBuffer.new("012345")
+
+ buffer[0] = "<"
+
+ assert_equal "&lt;12345", buffer
+ end
+
+ test "can assign value into non zero-index" do
+ buffer = ActiveSupport::SafeBuffer.new("012345")
+
+ buffer[2] = "<"
+
+ assert_equal "01&lt;345", buffer
end
- test "Should not return safe buffer from gsub!" do
- @buffer.gsub!('', 'asdf')
- assert_equal 'asdf', @buffer
- assert !@buffer.html_safe?
+ test "can assign value into slice" do
+ buffer = ActiveSupport::SafeBuffer.new("012345")
+
+ buffer[0, 3] = "<"
+
+ assert_equal "&lt;345", buffer
+ end
+
+ test "can assign value into offset slice" do
+ buffer = ActiveSupport::SafeBuffer.new("012345")
+
+ buffer[1, 3] = "<"
+
+ assert_equal "0&lt;45", buffer
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 +180,90 @@ 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?
+ test "clone_empty keeps the original dirtiness" do
+ assert_predicate @buffer.clone_empty, :html_safe?
+ assert_not_predicate @buffer.gsub!("", "").clone_empty, :html_safe?
end
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
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 (hash argument)" do
+ x = "foo %{x} bar".html_safe % { x: "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 escape unsafe interpolated args" do
+ x = "foo %{x} bar".html_safe % { x: "<br/>" }
+ assert_equal "foo &lt;br/&gt; 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 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 not escape safe interpolated args' do
- x = 'foo %{x} bar'.html_safe % { x: '<br/>'.html_safe }
- assert_equal 'foo <br/> bar', x
+ 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 interpolate to a safe string' do
- x = 'foo %{x} bar'.html_safe % { x: 'qux' }
- assert x.html_safe?, 'should be safe'
+ test "Should not affect frozen objects when accessing characters" do
+ x = "Hello".html_safe
+ assert_nil x[/a/, 1]
end
- test 'Should not affect frozen objects when accessing characters' do
- x = 'Hello'.html_safe
- assert_equal x[/a/, 1], nil
+ test "Should set back references" do
+ a = "foo123".html_safe
+ a2 = a.sub(/([a-z]+)([0-9]+)/) { $2 + $1 }
+ assert_equal "123foo", a2
+ assert_not_predicate a2, :html_safe?
+ a.sub!(/([a-z]+)([0-9]+)/) { $2 + $1 }
+ assert_equal "123foo", a
+ assert_not_predicate a, :html_safe?
+
+ b = "foo123 bar456".html_safe
+ b2 = b.gsub(/([a-z]+)([0-9]+)/) { $2 + $1 }
+ assert_equal "123foo 456bar", b2
+ assert_not_predicate b2, :html_safe?
+ b.gsub!(/([a-z]+)([0-9]+)/) { $2 + $1 }
+ assert_equal "123foo 456bar", b
+ assert_not_predicate b, :html_safe?
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
index acefa185a8..a40c813fe3 100644
--- a/activesupport/test/share_lock_test.rb
+++ b/activesupport/test/share_lock_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'concurrent/atomic/count_down_latch'
-require 'active_support/concurrency/share_lock'
+# 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
@@ -9,38 +11,38 @@ class ShareLockTest < ActiveSupport::TestCase
def test_reentrancy
thread = Thread.new do
- @lock.sharing { @lock.sharing {} }
- @lock.exclusive { @lock.exclusive {} }
+ @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 {} })
+ 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 {} }
+ 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 {} }
+ 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
+ def test_multiple_exclusives_are_able_to_progress
with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
exclusive_threads = (1..2).map do
Thread.new do
- @lock.exclusive {}
+ @lock.exclusive { }
end
end
@@ -51,7 +53,7 @@ class ShareLockTest < ActiveSupport::TestCase
def test_sharing_is_upgradeable_to_exclusive
upgrading_thread = Thread.new do
@lock.sharing do
- @lock.exclusive {}
+ @lock.exclusive { }
end
end
assert_threads_not_stuck upgrading_thread
@@ -64,7 +66,7 @@ class ShareLockTest < ActiveSupport::TestCase
upgrading_thread = Thread.new do
@lock.sharing do
in_sharing.count_down
- @lock.exclusive {}
+ @lock.exclusive { }
end
end
@@ -79,7 +81,7 @@ class ShareLockTest < ActiveSupport::TestCase
exclusive_threads = (1..2).map do
Thread.new do
@lock.send(use_upgrading ? :sharing : :tap) do
- @lock.exclusive(purpose: :load, compatible: [:load, :unload]) {}
+ @lock.exclusive(purpose: :load, compatible: [:load, :unload]) { }
end
end
end
@@ -93,7 +95,7 @@ class ShareLockTest < ActiveSupport::TestCase
with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
thread = Thread.new do
@lock.sharing do
- @lock.exclusive {}
+ @lock.exclusive { }
end
end
@@ -103,7 +105,7 @@ class ShareLockTest < ActiveSupport::TestCase
sharing_thread_release_latch.count_down
thread = Thread.new do
- @lock.exclusive {}
+ @lock.exclusive { }
end
assert_threads_not_stuck thread
@@ -113,68 +115,66 @@ class ShareLockTest < ActiveSupport::TestCase
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
+ 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
- ]
-
- 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 # wait for threads to get into their respective `exclusive {}` blocks
- assert_threads_stuck conflicting_exclusive_threads
+ # 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
- sharing_thread_release_latch.count_down
+ assert_threads_stuck conflicting_exclusive_threads
- assert_threads_not_stuck compatible_thread # compatible thread is now able to squeak through
+ sharing_thread_release_latch.count_down
- 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
+ assert_threads_not_stuck compatible_thread # compatible thread is now able to squeak through
- # 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.
+ 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
- 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
+ # 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
@@ -229,7 +229,7 @@ class ShareLockTest < ActiveSupport::TestCase
assert_threads_stuck waiting_exclusive
late_share_attempt = Thread.new do
- @lock.sharing {}
+ @lock.sharing { }
end
assert_threads_stuck late_share_attempt
@@ -250,14 +250,14 @@ class ShareLockTest < ActiveSupport::TestCase
@lock.sharing do
ready.wait
attempt_reentrancy.wait
- @lock.sharing {}
+ @lock.sharing { }
end
end
exclusive = Thread.new do
@lock.sharing do
ready.wait
- @lock.exclusive {}
+ @lock.exclusive { }
end
end
@@ -278,7 +278,7 @@ class ShareLockTest < ActiveSupport::TestCase
Thread.new do
@lock.sharing do
ready.wait
- @lock.exclusive(purpose: :x, compatible: [:x], after_compatible: [:x]) {}
+ @lock.exclusive(purpose: :x, compatible: [:x], after_compatible: [:x]) { }
done.wait
end
end
@@ -295,7 +295,7 @@ class ShareLockTest < ActiveSupport::TestCase
Thread.new do
@lock.sharing do
ready.wait
- @lock.exclusive(purpose: :x) {}
+ @lock.exclusive(purpose: :x) { }
done.wait
end
end,
@@ -321,7 +321,7 @@ class ShareLockTest < ActiveSupport::TestCase
Thread.new do
@lock.sharing do
ready.wait
- @lock.exclusive(purpose: :x) {}
+ @lock.exclusive(purpose: :x) { }
done.wait
end
end,
@@ -350,7 +350,7 @@ class ShareLockTest < ActiveSupport::TestCase
Thread.new do
@lock.sharing do
ready.wait
- @lock.exclusive(purpose: :x) {}
+ @lock.exclusive(purpose: :x) { }
done.wait
end
end,
@@ -384,7 +384,7 @@ class ShareLockTest < ActiveSupport::TestCase
incompatible_thread = Thread.new do
@lock.sharing do
ready.wait
- @lock.exclusive(purpose: :x) {}
+ @lock.exclusive(purpose: :x) { }
end
end
@@ -416,7 +416,7 @@ class ShareLockTest < ActiveSupport::TestCase
incompatible_thread = Thread.new do
ready.wait
- @lock.exclusive(purpose: :z) {}
+ @lock.exclusive(purpose: :z) { }
end
recursive_yield_shares_thread = Thread.new do
@@ -425,7 +425,7 @@ class ShareLockTest < ActiveSupport::TestCase
@lock.yield_shares(compatible: [:y]) do
do_nesting.wait
@lock.sharing do
- @lock.yield_shares(compatible: [:x, :y]) {}
+ @lock.yield_shares(compatible: [:x, :y]) { }
end
after_nesting.wait
end
@@ -437,12 +437,12 @@ class ShareLockTest < ActiveSupport::TestCase
assert_threads_stuck incompatible_thread
compatible_thread = Thread.new do
- @lock.exclusive(purpose: :y) {}
+ @lock.exclusive(purpose: :y) { }
end
assert_threads_not_stuck compatible_thread
post_nesting_incompatible_thread = Thread.new do
- @lock.exclusive(purpose: :x) {}
+ @lock.exclusive(purpose: :x) { }
end
assert_threads_stuck post_nesting_incompatible_thread
@@ -489,90 +489,90 @@ class ShareLockTest < ActiveSupport::TestCase
private
- module CustomAssertions
- SUFFICIENT_TIMEOUT = 0.2
+ module CustomAssertions
+ SUFFICIENT_TIMEOUT = 0.2
- private
+ 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_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_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) })
+ def assert_threads_not_stuck(threads)
+ assert(Array(threads).all? { |t| t.join(SUFFICIENT_TIMEOUT) })
+ end
end
- end
- class CustomAssertionsTest < ActiveSupport::TestCase
- include CustomAssertions
+ class CustomAssertionsTest < ActiveSupport::TestCase
+ include CustomAssertions
- def setup
- @latch = Concurrent::CountDownLatch.new
- @thread = Thread.new { @latch.wait }
- end
+ def setup
+ @latch = Concurrent::CountDownLatch.new
+ @thread = Thread.new { @latch.wait }
+ end
- def teardown
- @latch.count_down
- @thread.join
- 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_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
+ def test_detects_stuck_thread
+ assert_raises(Minitest::Assertion) do
+ assert_threads_not_stuck @thread
+ end
end
- end
- def test_detects_free_thread
- @latch.count_down
- assert_raises(Minitest::Assertion) do
- assert_threads_stuck @thread
+ def test_detects_free_thread
+ @latch.count_down
+ assert_raises(Minitest::Assertion) do
+ assert_threads_stuck @thread
+ end
end
- end
- def test_detects_already_released
- @latch.count_down
- assert_raises(Minitest::Assertion) do
- assert_threads_stuck_but_releasable_by_latch @thread, @latch
+ def test_detects_already_released
+ @latch.count_down
+ assert_raises(Minitest::Assertion) do
+ assert_threads_stuck_but_releasable_by_latch @thread, @latch
+ end
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
+ 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
- end
- include CustomAssertions
+ include CustomAssertions
- def with_thread_waiting_in_lock_section(lock_section)
- in_section = Concurrent::CountDownLatch.new
- section_release = Concurrent::CountDownLatch.new
+ 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
+ stuck_thread = Thread.new do
+ @lock.send(lock_section) do
+ in_section.count_down
+ section_release.wait
+ end
end
- end
- in_section.wait
+ in_section.wait
- yield section_release
- ensure
- section_release.count_down
- stuck_thread.join # clean up
- end
+ 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 917fa46c96..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.info "Dull story"
@logger.tagged("OMG") { @logger.info "Cool story" }
end.join
@logger.info "Funky time"
end
- assert_equal "[OMG] Cool story\n[BCX] Funky time\n", @output.string
+ assert_equal "Dull story\n[OMG] Cool story\n[BCX] Funky time\n", @output.string
end
test "keeps each tag in their own instance" do
- @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
+ other_logger.tagged("BCX") do
@logger.info "Cool story"
- @other_logger.info "Funky time"
+ other_logger.info "Funky time"
end
end
assert_equal "[OMG] Cool story\n", @output.string
- assert_equal "[BCX] Funky time\n", @other_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"
+ other_logger.info "Funky time"
+ end
+ end
+ assert_equal "[OMG] Cool story\n[BCX] Funky time\n", @output.string
end
test "cleans up the taggings on flush" do
diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb
index 18228a2ac5..56cd2665e0 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,21 +45,37 @@ 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
+ incremented = assert_difference "@object.num", +1 do
@object.increment
end
@@ -65,40 +83,41 @@ class AssertDifferenceTest < ActiveSupport::TestCase
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 # 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
@@ -106,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.
@@ -133,7 +314,7 @@ class SetupAndTeardownTest < ActiveSupport::TestCase
def teardown
end
- protected
+ private
def reset_callback_record
@called_back = []
@@ -158,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
@@ -170,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 3e5ba7c079..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
@@ -37,6 +36,8 @@ class MethodCallAssertionsTest < ActiveSupport::TestCase
assert_called(@object, :increment, returns: 10) do
assert_equal 10, @object.increment
end
+
+ assert_equal 1, @object.increment
end
def test_assert_called_failure
@@ -51,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
@@ -59,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
@@ -79,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
@@ -108,6 +164,30 @@ 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
diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb
index 59c3e52c2f..a1f84bf69e 100644
--- a/activesupport/test/time_travel_test.rb
+++ b/activesupport/test/time_travel_test.rb
@@ -1,11 +1,16 @@
-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"
+require "time_zone_test_helpers"
class TimeTravelTest < ActiveSupport::TestCase
- teardown do
- travel_back
- end
+ include TimeZoneTestHelpers
+
+ class TimeSubclass < ::Time; end
+ class DateSubclass < ::Date; end
+ class DateTimeSubclass < ::DateTime; end
def test_time_helper_travel
Time.stub(:now, Time.now) do
@@ -15,6 +20,8 @@ class TimeTravelTest < ActiveSupport::TestCase
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
@@ -42,6 +49,8 @@ class TimeTravelTest < ActiveSupport::TestCase
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
@@ -61,6 +70,20 @@ class TimeTravelTest < ActiveSupport::TestCase
end
end
+ def test_time_helper_travel_to_with_time_zone
+ with_env_tz "US/Eastern" do
+ with_tz_default ActiveSupport::TimeZone["UTC"] do
+ Time.stub(:now, Time.now) do
+ expected_time = 5.minutes.ago
+
+ travel_to 5.minutes.ago do
+ assert_equal expected_time.to_s(:db), Time.zone.now.to_s(:db)
+ end
+ end
+ end
+ end
+ end
+
def test_time_helper_travel_back
Time.stub(:now, Time.now) do
expected_time = Time.new(2004, 11, 24, 01, 04, 44)
@@ -74,10 +97,58 @@ class TimeTravelTest < ActiveSupport::TestCase
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
+
+ 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
+ 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
- def test_travel_to_will_reset_the_usec_to_avoid_mysql_rouding
+ def test_travel_to_will_reset_the_usec_to_avoid_mysql_rounding
Time.stub(:now, Time.now) do
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do
assert_equal 50, Time.now.sec
@@ -87,4 +158,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..f948c363df 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_ambiguous_time
+ zone = ActiveSupport::TimeZone["Moscow"]
+ period = zone.period_for_local(Time.utc(2015, 1, 1))
+ assert_equal period, zone.period_for_local(Time.utc(2014, 10, 26, 1, 0, 0))
+ 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..9e29a93ea0 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,14 +23,38 @@ 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
I18n.locale = default_locale
end
+ def test_transliterate_respects_the_locale_argument
+ char = [117, 776].pack("U*") # "ü" as ASCII "u" plus COMBINING DIAERESIS
+ I18n.backend.store_translations(:de, i18n: { transliterate: { rule: { "ü" => "ue" } } })
+ assert_equal "ue", ActiveSupport::Inflector.transliterate(char, locale: :de)
+ end
+
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..b7013c258a
--- /dev/null
+++ b/ci/qunit-selenium-runner.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "qunit/selenium/test_runner"
+
+if ARGV[1]
+ driver = ::Selenium::WebDriver.for(:remote, url: ARGV[1], desired_capabilities: :chrome)
+else
+ require "webdrivers"
+
+ driver_options = Selenium::WebDriver::Chrome::Options.new
+ driver_options.add_argument("--headless")
+ driver_options.add_argument("--disable-gpu")
+ driver_options.add_argument("--no-sandbox")
+
+ driver = ::Selenium::WebDriver.for(:chrome, options: driver_options)
+end
+
+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 063c6acb07..038db476e9 100755
--- a/ci/travis.rb
+++ b/ci/travis.rb
@@ -1,32 +1,25 @@
#!/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',
- 'ac' => 'actioncable',
- 'guides' => 'guides'
- }
-
attr_reader :component, :options
def initialize(component, options = {})
@@ -36,8 +29,10 @@ class Build
def run!(options = {})
self.options.update(options)
+
Dir.chdir(dir) do
announce(heading)
+
if guides?
run_bug_report_templates
else
@@ -55,41 +50,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?
tasks = ["#{adapter}:#{'isolated_' if isolated?}test"]
case adapter
- when 'mysql2'
- tasks.unshift 'db:mysql:rebuild'
- when 'postgresql'
- tasks.unshift 'db:postgresql:rebuild'
+ 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'
+ 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?
@@ -97,16 +96,16 @@ class Build
end
def integration?
- component.split(':').last == 'integration'
+ component.split(":").last == "integration"
end
def gem
- MAP[component.split(':').first]
+ component.split(":").first
end
alias :dir :gem
def adapter
- component.split(':').last
+ component.split(":").last
end
def rake(*tasks)
@@ -120,40 +119,45 @@ class Build
def env
if activesupport? && !isolated?
- # There is a known issue with the listen tests that casuses files to be
- # incorrectly GC'ed even when they are still in-use. The current is to
- # only run them in isolation to avoid randomly failing our test suite.
- { 'LISTEN' => '0' }
+ # 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"] == "activejob: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 == 'ac' && 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.6" && isolated
+ next if gem == "railties" && isolated
+ next if gem == "actioncable" && isolated
+ next if gem == "actioncable:integration" && isolated
+ next if gem == "activejob:integration" && isolated
+ next if gem == "guides" && isolated
+ next if gem == "actionview:ujs" && isolated
+ next if gem == "activestorage" && isolated
+ next if gem == "actionmailbox" && isolated
+ next if gem == "actiontext" && isolated
+
+ build = Build.new(gem, isolated: isolated)
results[build.key] = build.run!
-
end
end
diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md
index d35d0f1976..beace1ed6f 100644
--- a/guides/CHANGELOG.md
+++ b/guides/CHANGELOG.md
@@ -1,33 +1,30 @@
-## Rails 5.0.0.beta3 (February 24, 2016) ##
+## Rails 6.0.0.beta3 (March 11, 2019) ##
* No changes.
-## Rails 5.0.0.beta2 (February 01, 2016) ##
+## Rails 6.0.0.beta2 (February 25, 2019) ##
* No changes.
-## Rails 5.0.0.beta1 (December 18, 2015) ##
+## Rails 6.0.0.beta1 (January 18, 2019) ##
-* Add code of conduct to contributing guide
+* Add "Action Text Overview" Guide.
- *Jon Moss*
+ *DHH*
-* New section in Configuring: Configuring Active Job
+* Add "Action Mailbox Basics" Guide.
- *Eliot Sykes*
+ *DHH*
-* New section in Active Record Association Basics: Single Table Inheritance
+* New section _Troubleshooting_ in the _Autoloading and Reloading Constants_ guide.
- *Andrey Nering*
+ *Xavier Noria*
-* New section in Active Record Querying: Understanding The Method Chaining
+* Rails 6 requires Ruby 2.5.0 or newer.
- *Andrey Nering*
+ *Jeremy Daer*, *Kasper Timm Hansen*
-* 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 00577377d7..b7425f6de4 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
+ 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,24 @@ 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 :test do
+ templates = Dir.glob("bug_report_templates/*.rb")
+ counter = templates.count do |file|
+ puts "--- Running #{file}"
+ Bundler.clean_system(Gem.ruby, "-w", file) ||
+ puts("+++ 💥 FAILED (exit #{$?.exitstatus})")
end
+ puts "+++ #{counter} / #{templates.size} templates executed successfully"
+ exit 1 if counter < templates.size
end
-task :default => 'guides:help'
+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 077d237e4e..0000000000
--- a/guides/assets/images/belongs_to.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/credits_pic_blank.gif b/guides/assets/images/credits_pic_blank.gif
deleted file mode 100644
index a6b335d0c9..0000000000
--- a/guides/assets/images/credits_pic_blank.gif
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/fxn.png b/guides/assets/images/fxn.png
deleted file mode 100644
index 733d380cba..0000000000
--- a/guides/assets/images/fxn.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/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 baccb11322..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..830b19bd9d 100644
--- a/guides/assets/images/getting_started/template_is_missing_articles_new.png
+++ b/guides/assets/images/getting_started/template_is_missing_articles_new.png
Binary files differ
diff --git a/guides/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 79da2613d7..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_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 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/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 58ba708a39..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 plain: '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 8322707495..682269163a 100644
--- a/guides/bug_report_templates/action_controller_master.rb
+++ b/guides/bug_report_templates/action_controller_master.rb
@@ -1,28 +1,27 @@
-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'
+ 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__
+ config.hosts << "example.org"
+ 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
@@ -30,18 +29,18 @@ class TestController < ActionController::Base
include Rails.application.routes.url_helpers
def index
- render plain: '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..74b329115d 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", "~> 1.3.6"
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 a86edd9121..780456b7b6 100644
--- a/guides/bug_report_templates/active_record_master.rb
+++ b/guides/bug_report_templates/active_record_master.rb
@@ -1,29 +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 '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..8d71863034
--- /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", "~> 1.3.6"
+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 70cf931f34..ec65fee292 100644
--- a/guides/bug_report_templates/generic_master.rb
+++ b/guides/bug_report_templates/generic_master.rb
@@ -1,18 +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'
+ source "https://rubygems.org"
+
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
+
+ gem "rails", github: "rails/rails"
end
-require 'active_support'
-require 'active_support/core_ext/object/blank'
-require 'minitest/autorun'
+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..a72acdbd06 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"],
+ direction: env_value["DIRECTION"]
+).generate
diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb
index 7618fce2c8..7d4a15962c 100644
--- a/guides/rails_guides/generator.rb
+++ b/guides/rails_guides/generator.rb
@@ -1,249 +1,219 @@
-# ---------------------------------------------------------------------------
-#
-# 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:, direction: "ltr")
+ @edge = edge
+ @version = version
+ @all = all
+ @only = only
+ @kindle = kindle
+ @language = language
+ @direction = direction
- 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
- if kindle?
- Dir.entries("#{source_dir}/kindle").grep(GUIDES_RE).map do |entry|
- next if entry == 'KINDLE.md'
- guides << "kindle/#{entry}"
+ def initialize_markdown_renderer
+ Markdown::Renderer.edge = @edge
+ Markdown::Renderer.version = @version
+ end
+
+ 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)
- def select_only(guides)
- prefixes = ENV['ONLY'].split(",").map(&:strip)
- guides.select do |guide|
- guide.start_with?('kindle'.freeze, *prefixes)
+ if @kindle
+ Dir.entries("#{@source_dir}/kindle").grep(GUIDES_RE).map do |entry|
+ next if entry == "KINDLE.md"
+ guides << "kindle/#{entry}"
+ end
+ end
+
+ @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 copy_assets
+ FileUtils.cp_r(Dir.glob("#{@guides_dir}/assets/*"), @output_dir)
- def output_file_for(guide)
- if guide.end_with?('.md')
- guide.sub(/md\z/, 'html')
- else
- guide.sub(/\.erb\z/, '')
+ if @direction == "rtl"
+ overwrite_css_with_right_to_left_direction
+ end
end
- end
- def output_path_for(output_file)
- File.join(output_dir, File.basename(output_file))
- end
+ def overwrite_css_with_right_to_left_direction
+ FileUtils.mv("#{@output_dir}/stylesheets/main.rtl.css", "#{@output_dir}/stylesheets/main.css")
+ 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_file_for(guide)
+ if guide.end_with?(".md")
+ guide.sub(/md\z/, "html")
+ else
+ guide.sub(/\.erb\z/, "")
+ end
+ 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}", :lang => @lang)
+ 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.with_empty_template_cache.with_view_paths(
+ [@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.to_sym], 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 5bf73da16c..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/#{@lang ? @lang + '/' : ''}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 081afcb09f..8a0361ff4c 100644
--- a/guides/rails_guides/kindle.rb
+++ b/guides/rails_guides/kindle.rb
@@ -1,26 +1,24 @@
#!/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)
@@ -28,57 +26,56 @@ module Kindle
generate_document_metadata(mobi_outfile)
puts "Creating MOBI document with kindlegen. This may take a while."
- cmd = "kindlerb . > #{File.absolute_path logfile} 2>&1"
- puts cmd
- system(cmd)
- puts "MOBI document generated at #{File.expand_path(mobi_outfile, output_dir)}"
+ if Kindlerb.run(output_dir)
+ puts "MOBI document generated at #{File.expand_path(mobi_outfile, output_dir)}"
+ end
end
end
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 69c7cd5136..018f49ffd0 100644
--- a/guides/rails_guides/markdown.rb
+++ b/guides/rails_guides/markdown.rb
@@ -1,21 +1,27 @@
-require 'redcarpet'
-require 'nokogiri'
-require 'rails_guides/markdown/renderer'
+# frozen_string_literal: true
+
+require "redcarpet"
+require "nokogiri"
+require "rails_guides/markdown/renderer"
+require "rails-html-sanitizer"
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)
@raw_body = body
extract_raw_header_and_body
generate_header
+ generate_description
generate_title
generate_body
generate_structure
@@ -47,25 +53,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
@@ -78,6 +84,11 @@ module RailsGuides
@header = engine.render(@raw_header).html_safe
end
+ def generate_description
+ sanitizer = Rails::Html::FullSanitizer.new
+ @description = sanitizer.sanitize(@header).squish
+ end
+
def generate_structure
@headings_for_index = []
if @body.present?
@@ -85,31 +96,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"
@@ -119,7 +134,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
@@ -157,9 +172,10 @@ module RailsGuides
def render_page
@view.content_for(:header_section) { @header }
+ @view.content_for(:description) { @description }
@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 73ca600361..7f14c28bbc 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 %r{https?://api\.rubyonrails\.org}.match?(url)
+ %(<a href="#{api_link(url)}">#{content}</a>)
+ elsif title
+ %(<a href="#{url}" title="#{title}">#{content}</a>)
+ else
+ %(<a href="#{url}">#{content}</a>)
+ end
+ end
+
def header(text, header_level)
# Always increase the heading level by 1, so we can use h1, h2 heading in the document
header_level += 1
- %(<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{https?://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 5e82af5a15..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.
--------------------------------------------------------------------------------
@@ -22,7 +22,7 @@ Rails 2.2 supplies an easy system for internationalization (or i18n, for those o
* More information :
* [Official Rails i18 website](http://rails-i18n.org)
* [Finally. Ruby on Rails gets internationalized](https://web.archive.org/web/20140407075019/http://www.artweb-design.de/2008/7/18/finally-ruby-on-rails-gets-internationalized)
- * [Localizing Rails : Demo application](http://github.com/clemens/i18n_demo_app)
+ * [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..15704acefe 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:
@@ -73,7 +73,7 @@ $ ruby script/plugin install git://github.com/rails/rails_upgrade.git
You can see an example of how that works at [Rails Upgrade is now an Official Plugin](http://omgbloglol.com/post/364624593/rails-upgrade-is-now-an-official-plugin)
-Aside from Rails Upgrade tool, if you need more help, there are people on IRC and [rubyonrails-talk](http://groups.google.com/group/rubyonrails-talk) that are probably doing the same thing, possibly hitting the same issues. Be sure to blog your own experiences when upgrading so others can benefit from your knowledge!
+Aside from Rails Upgrade tool, if you need more help, there are people on IRC and [rubyonrails-talk](https://groups.google.com/group/rubyonrails-talk) that are probably doing the same thing, possibly hitting the same issues. Be sure to blog your own experiences when upgrading so others can benefit from your knowledge!
Creating a Rails 3.0 application
--------------------------------
@@ -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`.
@@ -608,6 +607,6 @@ More Information:
Credits
-------
-See the [full list of contributors to Rails](http://contributors.rubyonrails.org/) for the many people who spent many hours making Rails 3. Kudos to all of them.
+See the [full list of contributors to Rails](https://contributors.rubyonrails.org/) for the many people who spent many hours making Rails 3. Kudos to all of them.
Rails 3.0 Release Notes were compiled by [Mikel Lindsaar](http://lindsaar.net).
diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md
index 327495704a..09faeea614 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
@@ -240,7 +240,7 @@ Action Pack
* Added `config.action_controller.include_all_helpers`. By default `helper :all` is done in `ActionController::Base`, which includes all the helpers by default. Setting `include_all_helpers` to `false` will result in including only application_helper and the helper corresponding to controller (like foo_helper for foo_controller).
-* `url_for` and named url helpers now accept `:subdomain` and `:domain` as options.
+* `url_for` and named URL helpers now accept `:subdomain` and `:domain` as options.
* Added `Base.http_basic_authenticate_with` to do simple http basic authentication with a single class method call.
@@ -291,9 +291,9 @@ Action Pack
end
```
- You can restrict it to some actions by using `:only` or `:except`. Please read the docs at [`ActionController::Streaming`](http://api.rubyonrails.org/v3.1.0/classes/ActionController/Streaming.html) for more information.
+ You can restrict it to some actions by using `:only` or `:except`. Please read the docs at [`ActionController::Streaming`](https://api.rubyonrails.org/v3.1.0/classes/ActionController/Streaming.html) for more information.
-* The redirect route method now also accepts a hash of options which will only change the parts of the url in question, or an object which responds to call, allowing for redirects to be reused.
+* The redirect route method now also accepts a hash of options which will only change the parts of the URL in question, or an object which responds to call, allowing for redirects to be reused.
### Action Dispatch
@@ -556,6 +556,6 @@ Deprecations:
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.
+See the [full list of contributors to Rails](https://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 f16d509f77..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
@@ -156,7 +156,7 @@ Railties
will create indexes for `title` and `author` with the latter being a unique index. Some types such as decimal accept custom options. In the example, `price` will be a decimal column with precision and scale set to 7 and 2 respectively.
-* Turn gem has been removed from default Gemfile.
+* Turn gem has been removed from default `Gemfile`.
* Remove old plugin generator `rails generate plugin` in favor of `rails plugin new` command.
@@ -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
```
diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md
index b9444510ea..e4b1b04681 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,26 +55,26 @@ $ 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.
-* **[Russian doll caching](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)** ([commit](https://github.com/rails/rails/commit/4154bf012d2bec2aae79e4a49aa94a70d3e91d49)) - Cache nested fragments of views. Each fragment expires based on a set of dependencies (a cache key). The cache key is usually a template version number and a model object.
+* **[Russian doll caching](https://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)** ([commit](https://github.com/rails/rails/commit/4154bf012d2bec2aae79e4a49aa94a70d3e91d49)) - Cache nested fragments of views. Each fragment expires based on a set of dependencies (a cache key). The cache key is usually a template version number and a model object.
* **Turbolinks** ([commit](https://github.com/rails/rails/commit/e35d8b18d0649c0ecc58f6b73df6b3c8d0c6bb74)) - Serve only one initial HTML page. When the user navigates to another page, use pushState to update the URL and use AJAX to update the title and body.
* **Decouple ActionView from ActionController** ([commit](https://github.com/rails/rails/commit/78b0934dd1bb84e8f093fb8ef95ca99b297b51cd)) - ActionView was decoupled from ActionPack and will be moved to a separated gem in Rails 4.1.
* **Do not depend on ActiveModel** ([commit](https://github.com/rails/rails/commit/166dbaa7526a96fdf046f093f25b0a134b277a68)) - ActionPack no longer depends on ActiveModel.
@@ -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.
@@ -281,4 +281,4 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/a
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.
+See the [full list of contributors to Rails](https://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.
diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md
index 6bf65757ec..d481a1f49b 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
===============================
@@ -159,7 +159,7 @@ By default, these preview classes live in `test/mailers/previews`.
This can be configured using the `preview_path` option.
See its
-[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Previewing+emails)
+[documentation](https://api.rubyonrails.org/v4.1.0/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Previewing+emails)
for a detailed write up.
### Active Record enums
@@ -231,7 +231,7 @@ extending it with `ActiveSupport::Concern`, then mixing it in to the
`Todo` class.
See its
-[documentation](http://api.rubyonrails.org/v4.1.0/classes/Module/Concerning.html)
+[documentation](https://api.rubyonrails.org/v4.1.0/classes/Module/Concerning.html)
for a detailed write up and the intended use cases.
### CSRF protection from remote `<script>` tags
@@ -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))
@@ -727,6 +727,6 @@ Credits
-------
See the
-[full list of contributors to Rails](http://contributors.rubyonrails.org/) for
+[full list of contributors to Rails](https://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.
diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md
index 73e6c2c05b..6bbd738826 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.
@@ -154,9 +154,9 @@ remove_foreign_key :accounts, column: :owner_id
```
See the API documentation on
-[add_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key)
+[add_foreign_key](https://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key)
and
-[remove_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_foreign_key)
+[remove_foreign_key](https://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_foreign_key)
for a full description.
@@ -179,7 +179,7 @@ change your code to use the explicit form (`render file: "foo/bar"`) instead.
`respond_with` and the corresponding class-level `respond_to` have been moved
to the [responders](https://github.com/plataformatec/responders) gem. Add
-`gem 'responders', '~> 2.0'` to your Gemfile to use it:
+`gem 'responders', '~> 2.0'` to your `Gemfile` to use it:
```ruby
# app/controllers/users_controller.rb
@@ -368,7 +368,7 @@ Please refer to the [Changelog][railties] for detailed changes.
### Notable changes
-* Introduced `web-console` in the default application Gemfile.
+* Introduced `web-console` in the default application `Gemfile`.
([Pull Request](https://github.com/rails/rails/pull/11667))
* Added a `required` option to the model generator for associations.
@@ -446,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`.
@@ -545,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))
@@ -871,13 +871,13 @@ 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
-------
See the
-[full list of contributors to Rails](http://contributors.rubyonrails.org/) for
+[full list of contributors to Rails](https://contributors.rubyonrails.org/) for
the many people who spent many hours making Rails the stable and robust
framework it is today. Kudos to all of them.
diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md
index 45e9b77eea..b090c71a57 100644
--- a/guides/source/5_0_release_notes.md
+++ b/guides/source/5_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 5.0 Release Notes
===============================
@@ -37,24 +37,141 @@ Major Features
--------------
### Action Cable
-[Pull Request](https://github.com/rails/rails/pull/22586)
-ToDo...
+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.
-### Rails API
-[Pull Request](https://github.com/rails/rails/pull/19832)
+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.
-ToDo...
+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
-ToDo...
+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](https://api.rubyonrails.org/v5.0.1/classes/ActiveRecord/Attributes/ClassMethods.html)
+for a detailed write up.
+
### Test Runner
-[Pull Request](https://github.com/rails/rails/pull/19216)
-ToDo...
+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
--------
@@ -89,13 +206,13 @@ Please refer to the [Changelog][railties] for detailed changes.
* Deprecated `config.static_cache_control` in favor of
`config.public_file_server.headers`.
- ([Pull Request](https://github.com/rails/rails/pull/22173))
+ ([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 is renamed to `app:update` and `app:template`.)
+ (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
@@ -125,7 +242,7 @@ Please refer to the [Changelog][railties] for detailed changes.
[Pull Request](https://github.com/rails/rails/pull/22288))
* New applications are generated with the evented file system monitor enabled
- on Linux and Mac OS X. The feature can be opted out by passing
+ on Linux and macOS. The feature can be opted out by passing
`--skip-listen` to the generator.
([commit](https://github.com/rails/rails/commit/de6ad5665d2679944a9ee9407826ba88395a1003),
[commit](https://github.com/rails/rails/commit/94dbc48887bf39c241ee2ce1741ee680d773f202))
@@ -141,6 +258,17 @@ Please refer to the [Changelog][railties] for detailed changes.
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
-----------
@@ -227,6 +355,21 @@ Please refer to the [Changelog][action-pack] for detailed changes.
`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
@@ -270,11 +413,11 @@ Please refer to the [Changelog][action-pack] for detailed changes.
* Changed the `protect_from_forgery` prepend default to `false`.
([commit](https://github.com/rails/rails/commit/39794037817703575c35a75f1961b01b83791191))
-* `ActionController::TestCase` will be moved to it's own gem in Rails 5.1. Use
+* `ActionController::TestCase` will be moved to its own gem in Rails 5.1. Use
`ActionDispatch::IntegrationTest` instead.
([commit](https://github.com/rails/rails/commit/4414c5d1795e815b102571425974a8b1d46d932d))
-* Rails will only generate "weak", instead of strong ETags.
+* 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
@@ -289,10 +432,30 @@ Please refer to the [Changelog][action-pack] for detailed changes.
* Added request encoding and response parsing to integration tests.
([Pull Request](https://github.com/rails/rails/pull/21671))
-* Update default rendering policies when the controller action did
- not explicitly indicate a response.
- ([Pull Request](https://github.com/rails/rails/pull/23827))
+* 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
-------------
@@ -318,7 +481,7 @@ Please refer to the [Changelog][action-view] for detailed changes.
* Changed the default template handler from `ERB` to `Raw`.
([commit](https://github.com/rails/rails/commit/4be859f0fdf7b3059a28d03c279f03f5938efc80))
-* Collection rendering can cache and fetches multiple partials.
+* 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))
@@ -329,9 +492,15 @@ Please refer to the [Changelog][action-view] for detailed changes.
button on submit to prevent double submits.
([Pull Request](https://github.com/rails/rails/pull/21135))
-* Collection rendering can cache and fetch multiple partials at once.
- ([Pull Request](https://github.com/rails/rails/pull/21135))
+* 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
-------------
@@ -418,8 +587,10 @@ Please refer to the [Changelog][active-record] for detailed changes.
[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. It will
- live on in a separate gem for now, but most users should just use `mysql2`.
+* 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))
@@ -427,6 +598,12 @@ Please refer to the [Changelog][active-record] for detailed changes.
* 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
@@ -439,10 +616,6 @@ Please refer to the [Changelog][active-record] for detailed changes.
* Deprecated `ActiveRecord::Base.errors_in_transactional_callbacks=`.
([commit](https://github.com/rails/rails/commit/07d3d402341e81ada0214f2cb2be1da69eadfe72))
-* Deprecated passing of `start` value to `find_in_batches` and `find_each`
- in favour of `begin_at` value.
- ([Pull Request](https://github.com/rails/rails/pull/18961))
-
* Deprecated `Relation#uniq` use `Relation#distinct` instead.
([commit](https://github.com/rails/rails/commit/adfab2dcf4003ca564d78d4425566dd2d9cd8b4f))
@@ -488,6 +661,18 @@ Please refer to the [Changelog][active-record] for detailed changes.
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.
@@ -496,16 +681,13 @@ Please refer to the [Changelog][active-record] for detailed changes.
* New attributes
API. ([commit](https://github.com/rails/rails/commit/8c752c7ac739d5a86d4136ab1e9d0142c4041e58))
-* Added `:enum_prefix`/`:enum_suffix` option to `enum`
- definition. ([Pull Request](https://github.com/rails/rails/pull/19813))
+* 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))
-* Require `belongs_to` by default.
- ([Pull Request](https://github.com/rails/rails/pull/18937)) - Deprecate
- `required` option in favor of `optional` for `belongs_to`
-
* Changed the default `null` value for `timestamps` to `false`.
([commit](https://github.com/rails/rails/commit/a939506f297b667291480f26fa32a373a18ae06a))
@@ -525,16 +707,14 @@ Please refer to the [Changelog][active-record] for detailed changes.
operator to combine WHERE or HAVING clauses.
([commit](https://github.com/rails/rails/commit/b0b37942d729b6bdcd2e3178eda7fa1de203b3d0))
-* Added `:time` option added for `#touch`.
- ([Pull Request](https://github.com/rails/rails/pull/18956))
-
* 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`.
+ 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
@@ -589,6 +769,51 @@ Please refer to the [Changelog][active-record] for detailed changes.
* 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
------------
@@ -605,6 +830,9 @@ Please refer to the [Changelog][active-model] for detailed changes.
[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
@@ -640,6 +868,9 @@ Please refer to the [Changelog][active-model] for detailed changes.
* 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
-----------
@@ -648,11 +879,15 @@ Please refer to the [Changelog][active-job] for detailed changes.
### Notable changes
-* `ActiveJob::Base.deserialize` delegates to the job class. this allows jobs
+* `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))
@@ -707,6 +942,9 @@ Please refer to the [Changelog][active-support] for detailed changes.
* 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`.
@@ -734,8 +972,10 @@ Please refer to the [Changelog][active-support] for detailed changes.
`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`.
+* 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`.
@@ -757,7 +997,7 @@ Please refer to the [Changelog][active-support] for detailed changes.
* New config option
`config.active_support.halt_callback_chains_on_return_false` to specify
- whether ActiveRecord, ActiveModel and ActiveModel::Validations callback
+ 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))
@@ -766,7 +1006,8 @@ Please refer to the [Changelog][active-support] for detailed changes.
* 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/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`.
@@ -774,7 +1015,7 @@ Please refer to the [Changelog][active-support] for detailed changes.
* Added `#prev_day` and `#next_day` counterparts to `#yesterday` and
`#tomorrow` for `Date`, `Time`, and `DateTime`.
- ([Pull Request](httpshttps://github.com/rails/rails/pull/18335))
+ ([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))
@@ -795,7 +1036,7 @@ Please refer to the [Changelog][active-support] for detailed changes.
([commit](https://github.com/rails/rails/commit/a5e507fa0b8180c3d97458a9b86c195e9857d8f6))
* Added `Integer#positive?` and `Integer#negative?` query methods
- in the vein of `Fixnum#zero?`.
+ 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
@@ -817,20 +1058,30 @@ Please refer to the [Changelog][active-support] for detailed changes.
* Added `Array#second_to_last` and `Array#third_to_last` methods.
([Pull Request](https://github.com/rails/rails/pull/23583))
-* Added `#on_weekday?` method to `Date`, `Time`, and `DateTime`.
- ([Pull Request](https://github.com/rails/rails/pull/23687))
-
* Publish `ActiveSupport::Executor` and `ActiveSupport::Reloader` APIs to allow
components and libraries to manage, and participate in, the execution of
application code, and the application reloading process.
([Pull Request](https://github.com/rails/rails/pull/23807))
+* `ActiveSupport::Duration` now supports ISO8601 formatting and parsing.
+ ([Pull Request](https://github.com/rails/rails/pull/16917))
+
+* `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
+[full list of contributors to Rails](https://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.
diff --git a/guides/source/5_1_release_notes.md b/guides/source/5_1_release_notes.md
new file mode 100644
index 0000000000..e885b1e42e
--- /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](https://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..ac247bc3f9
--- /dev/null
+++ b/guides/source/5_2_release_notes.md
@@ -0,0 +1,865 @@
+**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))
+
+* Idle database connections (previously just orphaned connections) are now
+ periodically reaped by the connection pool reaper.
+ ([Commit](https://github.com/rails/rails/pull/31221/commits/9027fafff6da932e6e64ddb828665f4b01fc8902))
+
+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](https://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..6c3091153f
--- /dev/null
+++ b/guides/source/6_0_release_notes.md
@@ -0,0 +1,299 @@
+**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:
+
+* Action Mailbox
+* Action Text
+* Parallel Testing
+* Action Cable 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
+--------------
+
+### Action Mailbox
+
+[Pull Request](https://github.com/rails/rails/pull/34786)
+
+[Action Mailbox](https://github.com/rails/rails/tree/6-0-stable/actionmailbox) allows you
+to route incoming emails to controller-like mailboxes.
+You can read more about Action Mailbox in the [Action Mailbox Basics](action_mailbox_basics.html) guide.
+
+### Action Text
+
+[Pull Request](https://github.com/rails/rails/pull/34873)
+
+[Action Text](https://github.com/rails/rails/tree/6-0-stable/actiontext)
+brings rich text content and editing to Rails. It includes
+the [Trix editor](https://trix-editor.org) that handles everything from formatting
+to links to quotes to lists to embedded images and galleries.
+The rich text content generated by the Trix editor is saved in its own
+RichText model that's associated with any existing Active Record model in the application.
+Any embedded images (or other attachments) are automatically stored using
+Active Storage and associated with the included RichText model.
+
+You can read more about Action Text in the [Action Text Overview](action_text_overview.html) guide.
+
+### 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.
+
+### Action Cable Testing
+
+[Pull Request](https://github.com/rails/rails/pull/33659)
+
+[Action Cable testing tools](testing.html#testing-action-cable) allow you to test your
+Action Cable functionality at any level: connections, channels, broadcasts.
+
+Railties
+--------
+
+Please refer to the [Changelog][railties] for detailed changes.
+
+### Removals
+
+* Remove deprecated `after_bundle` helper inside plugins templates.
+ ([Commit](https://github.com/rails/rails/commit/4d51efe24e461a2a3ed562787308484cd48370c7))
+
+* Remove deprecated support to `config.ru` that uses the application
+ class as argument of `run`.
+ ([Commit](https://github.com/rails/rails/commit/553b86fc751c751db504bcbe2d033eb2bb5b6a0b))
+
+* Remove deprecated `environment` argument from the rails commands.
+ ([Commit](https://github.com/rails/rails/commit/e20589c9be09c7272d73492d4b0f7b24e5595571))
+
+* Remove deprecated `capify!` method in generators and templates.
+ ([Commit](https://github.com/rails/rails/commit/9d39f81d512e0d16a27e2e864ea2dd0e8dc41b17))
+
+* Remove deprecated `config.secret_token`.
+ ([Commit](https://github.com/rails/rails/commit/46ac5fe69a20d4539a15929fe48293e1809a26b0))
+
+### Deprecations
+
+* Deprecate passing Rack server name as a regular argument to `rails server`.
+ ([Pull Request](https://github.com/rails/rails/pull/32058))
+
+* Deprecate support for using `HOST` environment to specify server IP.
+ ([Pull Request](https://github.com/rails/rails/pull/32540))
+
+* Deprecate accessing hashes returned by `config_for` by non-symbol keys.
+ ([Pull Request](https://github.com/rails/rails/pull/35198))
+
+### Notable changes
+
+* Add an explicit option `--using` or `-u` for specifying the server for the
+ `rails server` command.
+ ([Pull Request](https://github.com/rails/rails/pull/32058))
+
+* Add ability to see the output of `rails routes` in expanded format.
+ ([Pull Request](https://github.com/rails/rails/pull/32130))
+
+* Run the seed database task using inline Active Job adapter.
+ ([Pull Request](https://github.com/rails/rails/pull/34953))
+
+* Add a command `rails db:system:change` to change the database of the application.
+ ([Pull Request](https://github.com/rails/rails/pull/34832))
+
+* Add `rails test:channels` command to test only Action Cable channels.
+ ([Pull Request](https://github.com/rails/rails/pull/34947))
+
+* Introduce guard against DNS rebinding attacks.
+ ([Pull Request](https://github.com/rails/rails/pull/33145))
+
+* Add ability to abort on failure while running generator commands.
+ ([Pull Request](https://github.com/rails/rails/pull/34420))
+
+* Make Webpacker the default JavaScript compiler for Rails 6.
+ ([Pull Request](https://github.com/rails/rails/pull/33079))
+
+* Add multiple database support for `rails db:migrate:status` command.
+ ([Pull Request](https://github.com/rails/rails/pull/34137))
+
+* Add ability to use different migration paths from multiple databases in
+ the generators.
+ ([Pull Request](https://github.com/rails/rails/pull/34021))
+
+* Add support for multi environment credentials.
+ ([Pull Request](https://github.com/rails/rails/pull/33521))
+
+* Make `null_store` as default cache store in test environment.
+ ([Pull Request](https://github.com/rails/rails/pull/33773))
+
+Action Cable
+------------
+
+Please refer to the [Changelog][action-cable] for detailed changes.
+
+### Removals
+
+### Deprecations
+
+### Notable changes
+
+* The ActionCable javascript package has been converted from CoffeeScript
+ to ES2015, and we now publish the source code in the npm distribution.
+
+ This allows ActionCable users to depend on the javascript source code
+ rather than the compiled code, which can produce smaller javascript bundles.
+
+ This change includes some breaking changes to optional parts of the
+ ActionCable javascript API:
+
+ - Configuration of the WebSocket adapter and logger adapter have been moved
+ from properties of `ActionCable` to properties of `ActionCable.adapters`.
+
+ - The `ActionCable.startDebugging()` and `ActionCable.stopDebugging()`
+ methods have been removed and replaced with the property
+ `ActionCable.logger.enabled`.
+
+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
+
+* Add a configuration option to customize format of the `ActiveModel::Errors#full_message`.
+ ([Pull Request](https://github.com/rails/rails/pull/32956))
+
+* Add support for configuring attribute name for `has_secure_password`.
+ ([Pull Request](https://github.com/rails/rails/pull/26764))
+
+* Add `#slice!` method to `ActiveModel::Errors`.
+ ([Pull Request](https://github.com/rails/rails/pull/34489))
+
+* Add `ActiveModel::Errors#of_kind?` to check presence of a specific error.
+ ([Pull Request](https://github.com/rails/rails/pull/34866))
+
+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](https://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 f50bcddbe7..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/">Rails 4.2</a>,
-<a href="http://guides.rubyonrails.org/v4.1/">Rails 4.1</a>,
-<a href="http://guides.rubyonrails.org/v4.0/">Rails 4.0</a>,
-<a href="http://guides.rubyonrails.org/v3.2/">Rails 3.2</a>, and
-<a href="http://guides.rubyonrails.org/v2.3/">Rails 2.3</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
index 28578b3369..f1e2a0081f 100644
--- a/guides/source/action_cable_overview.md
+++ b/guides/source/action_cable_overview.md
@@ -1,52 +1,86 @@
+**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
+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 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.
+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.
+
+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.
What is Pub/Sub
---------------
-Pub/Sub, or Publish-Subscribe, refers to a message queue paradigm whereby senders
-of information (publishers), send data to an abstract class of recipients (subscribers),
-without specifying individual recipients. Action Cable uses this approach to communicate
-between the server and many clients.
-
-What is Action Cable
---------------------
-
-Action Cable is a server which can handle multiple connection instances, with one
-client-server connection instance established per WebSocket connection.
+[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
-the cable server is accepting, a Connection object will be instantiated on the server side.
-This instance becomes the parent of all the channel subscriptions that are created from there on.
-The Connection itself does not deal with any specific application logic beyond authentication
-and authorization. The client of a WebSocket connection is called a consumer. An individual
-user will create one consumer-connection pair per browser tab, window, or device they have open.
+*Connections* 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 instantiated via the `ApplicationCable::Connection` class in Ruby.
-In this class, you authorize the incoming connection, and proceed to establish it
-if the user can be identified.
+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
@@ -60,10 +94,10 @@ module ApplicationCable
self.current_user = find_verified_user
end
- protected
+ private
def find_verified_user
- if current_user = User.find_by(id: cookies.signed[:user_id])
- current_user
+ if verified_user = User.find_by(id: cookies.encrypted[:user_id])
+ verified_user
else
reject_unauthorized_connection
end
@@ -73,24 +107,24 @@ 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
+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`.
+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
+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).
+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.
+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
@@ -103,14 +137,14 @@ end
```
Then you would create your own channel classes. For example, you could have a
-**ChatChannel** and an **AppearanceChannel**:
+`ChatChannel` and an `AppearanceChannel`:
```ruby
-# app/channels/application_cable/chat_channel.rb
+# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
end
-# app/channels/application_cable/appearance_channel.rb
+# app/channels/appearance_channel.rb
class AppearanceChannel < ApplicationCable::Channel
end
```
@@ -119,15 +153,15 @@ A consumer could then be subscribed to either or both of these channels.
#### Subscriptions
-When a consumer is subscribed to a channel, they act as a subscriber;
-This connection is called a subscription.
-Incoming messages are then routed to these channel subscriptions based on
-an identifier sent by the cable consumer.
+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/application_cable/chat_channel.rb
+# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
- # Called when the consumer has successfully become a subscriber of this channel
+ # Called when the consumer has successfully
+ # become a subscriber to this channel.
def subscribed
end
end
@@ -138,51 +172,80 @@ end
### Connections
Consumers require an instance of the connection on their side. This can be
-established using the following Javascript, which is generated by default in Rails:
+established using the following JavaScript, which is generated by default by Rails:
#### Connect Consumer
-```coffeescript
-# app/assets/javascripts/cable.coffee
-#= require action_cable
+```js
+// app/javascript/channels/consumer.js
+// 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.
-@App = {}
-App.cable = ActionCable.createConsumer()
+import { createConsumer } from "@rails/actioncable"
+
+export default createConsumer()
```
-This will ready a consumer that'll connect against /cable on your server by default.
+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.
+The consumer can optionally take an argument that specifies the URL to connect to. This
+can be a string, or a function that returns a string that will be called when the
+WebSocket is opened.
+
+```js
+// Specify a different URL to connect to
+createConsumer('https://ws.example.com/cable')
+
+// Use a function to dynamically generate the URL
+createConsumer(getWebSocketURL)
+
+function getWebSocketURL {
+ const token = localStorage.get('auth-token')
+ return `https://ws.example.com/cable?token=${token}`
+}
+```
+
#### Subscriber
-When a consumer is subscribed to a channel, they act as a subscriber. A
-consumer can act as a subscriber to a given channel any number of times.
-For example, a consumer could subscribe to multiple chat rooms at the same time.
-(remember that a physical user may have multiple consumers, one per tab/device open to your connection).
+A consumer becomes a subscriber by creating a subscription to a given channel:
+
+```js
+// app/javascript/channels/chat_channel.js
+import consumer from "./consumer"
-A consumer becomes a subscriber, by creating a subscription to a given channel:
+consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" })
-```coffeescript
-# app/assets/javascripts/cable/subscriptions/chat.coffee
-App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }
+// app/javascript/channels/appearance_channel.js
+import consumer from "./consumer"
-# app/assets/javascripts/cable/subscriptions/appearance.coffee
-App.cable.subscriptions.create { channel: "AppearanceChannel" }
+consumer.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:
+
+```js
+// app/javascript/channels/chat_channel.js
+import consumer from "./consumer"
+
+consumer.subscriptions.create({ channel: "ChatChannel", room: "1st Room" })
+consumer.subscriptions.create({ channel: "ChatChannel", room: "2nd Room" })
+```
+
## Client-Server Interactions
### Streams
-Streams provide the mechanism by which channels route published content
-(broadcasts) to its subscribers.
+*Streams* provide the mechanism by which channels route published content
+(broadcasts) to their subscribers.
```ruby
-# app/channels/application_cable/chat_channel.rb
+# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room]}"
@@ -203,61 +266,78 @@ class CommentsChannel < ApplicationCable::Channel
end
```
-You can then broadcast to this channel using: `CommentsChannel.broadcast_to(@post, @comment)`
+You can then broadcast to this channel like this:
-### Broadcastings
+```ruby
+CommentsChannel.broadcast_to(@post, @comment)
+```
-A broadcasting is a pub/sub link where anything transmitted by a publisher
+### 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.
+
+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'
+WebNotificationsChannel.broadcast_to(
+ current_user,
+ title: 'New things!',
+ body: 'All the news fit to print'
+)
```
The `WebNotificationsChannel.broadcast_to` call places a message in the current
-subscription adapter (Redis by default)'s pubsub queue under a separate
-broadcasting name for each user. For a user with an ID of 1, the broadcasting
-name would be `web_notifications_1`.
+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(data)`
+`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>
- """
+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.
+
+```js
+// app/javascript/channels/chat_channel.js
+// Assumes you've already requested the right to send web notifications
+import consumer from "./consumer"
+
+consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {
+ received(data) {
+ this.appendLine(data)
+ },
+
+ appendLine(data) {
+ const html = this.createLine(data)
+ const element = document.querySelector("[data-chat-room='Best Room']")
+ element.insertAdjacentHTML("beforeend", html)
+ },
+
+ createLine(data) {
+ return `
+ <article class="chat-line">
+ <span class="speaker">${data["sent_by"]}</span>
+ <span class="body">${data["body"]}</span>
+ </article>
+ `
+ }
+})
```
-### Passing Parameters to Channel
+### Passing Parameters to Channels
-You can pass parameters from the client-side to the server-side when
-creating a subscription. For example:
+You can pass parameters from the client side to the server side when creating a
+subscription. For example:
```ruby
# app/channels/chat_channel.rb
@@ -268,37 +348,48 @@ class ChatChannel < ApplicationCable::Channel
end
```
-Pass an object as the first argument to `subscriptions.create`, and that object
-will become your params hash in your cable channel. The keyword `channel` is required.
-
-```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>
- """
+An object passed as the first argument to `subscriptions.create` becomes the
+params hash in the cable channel. The keyword `channel` is required:
+
+```js
+// app/javascript/channels/chat_channel.js
+import consumer from "./consumer"
+
+consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {
+ received(data) {
+ this.appendLine(data)
+ },
+
+ appendLine(data) {
+ const html = this.createLine(data)
+ const element = document.querySelector("[data-chat-room='Best Room']")
+ element.insertAdjacentHTML("beforeend", html)
+ },
+
+ createLine(data) {
+ return `
+ <article class="chat-line">
+ <span class="speaker">${data["sent_by"]}</span>
+ <span class="body">${data["body"]}</span>
+ </article>
+ `
+ }
+})
```
```ruby
-# Somewhere in your app this is called, perhaps from a NewCommentJob
-ChatChannel.broadcast_to "chat_#{room}", sent_by: 'Paul', body: 'This is a cool chat app.'
+# 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
-### Rebroadcasting message
-
-A common use case is to rebroadcast a message sent by one client to any
+A common use case is to *rebroadcast* a message sent by one client to any
other connected clients.
```ruby
@@ -309,38 +400,43 @@ class ChatChannel < ApplicationCable::Channel
end
def receive(data)
- ChatChannel.broadcast_to "chat_#{params[:room]}", 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." }
+```js
+// app/javascript/channels/chat_channel.js
+import consumer from "./consumer"
+
+const chatChannel = consumer.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." })
+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
+## 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)
+ 1. [Setup your connection](#connection-setup).
+ 2. [Setup your parent channel](#parent-channel-setup).
+ 3. [Connect your consumer](#connect-consumer).
+
+### Example 1: User Appearances
-### 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:
+Create the server-side appearance channel:
```ruby
# app/channels/appearance_channel.rb
@@ -354,7 +450,7 @@ class AppearanceChannel < ApplicationCable::Channel
end
def appear(data)
- current_user.appear on: data['appearing_on']
+ current_user.appear(on: data['appearing_on'])
end
def away
@@ -363,63 +459,105 @@ class AppearanceChannel < ApplicationCable::Channel
end
```
-When `#subscribed` callback is invoked by the consumer, a client-side subscription
-is initiated. In this case, we take that opportunity to say "the current user has
-indeed appeared". That appear/disappear API could be backed by Redis, a database,
-or whatever else.
-
-#### Create the client-side Appearance Channel subscription:
-
-```coffeescript
-# app/assets/javascripts/cable/subscriptions/appearance.coffee
-App.cable.subscriptions.create "AppearanceChannel",
- # Called when the subscription is ready for use on the server
- connected: ->
- @install()
- @appear()
-
- # Called when the WebSocket connection is closed
- disconnected: ->
- @uninstall()
-
- # Called when the subscription is rejected by the server
- rejected: ->
- @uninstall()
-
- appear: ->
- # Calls `AppearanceChannel#appear(data)` on the server
- @perform("appear", appearing_on: $("main").data("appearing-on"))
-
- away: ->
- # Calls `AppearanceChannel#away` on the server
- @perform("away")
+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:
+
+```js
+// app/javascript/channels/appearance_channel.js
+import consumer from "./consumer"
+
+consumer.subscriptions.create("AppearanceChannel", {
+ // Called once when the subscription is created.
+ initialized() {
+ this.update = this.update.bind(this)
+ },
+
+ // Called when the subscription is ready for use on the server.
+ connected() {
+ this.install()
+ this.update()
+ },
+
+ // Called when the WebSocket connection is closed.
+ disconnected() {
+ this.uninstall()
+ },
+
+ // Called when the subscription is rejected by the server.
+ rejected() {
+ this.uninstall()
+ },
+
+ update() {
+ this.documentIsActive ? this.appear() : this.away()
+ },
+
+ appear() {
+ // Calls `AppearanceChannel#appear(data)` on the server.
+ this.perform("appear", { appearing_on: this.appearingOn })
+ },
+
+ away() {
+ // Calls `AppearanceChannel#away` on the server.
+ this.perform("away")
+ },
+
+ install() {
+ window.addEventListener("focus", this.update)
+ window.addEventListener("blur", this.update)
+ document.addEventListener("turbolinks:load", this.update)
+ document.addEventListener("visibilitychange", this.update)
+ },
+
+ uninstall() {
+ window.removeEventListener("focus", this.update)
+ window.removeEventListener("blur", this.update)
+ document.removeEventListener("turbolinks:load", this.update)
+ document.removeEventListener("visibilitychange", this.update)
+ },
+
+ get documentIsActive() {
+ return document.visibilityState == "visible" && document.hasFocus()
+ },
+
+ get appearingOn() {
+ const element = document.querySelector("[data-appearing-on]")
+ return element ? element.getAttribute("data-appearing-on") : null
+ }
+})
+```
+##### Client-Server Interaction
- buttonSelector = "[data-behavior~=appear_away]"
+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`.
- install: ->
- $(document).on "page:change.appearance", =>
- @appear()
+2. **Client** subscribes to the appearance channel via
+`consumer.subscriptions.create({ channel: "AppearanceChannel" })`. (`appearance_channel.js`)
- $(document).on "click.appearance", buttonSelector, =>
- @away()
- false
+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`)
- $(buttonSelector).show()
+4. **Client** recognizes that a subscription has been established and calls
+`connected` (`appearance_channel.js`) which in turn calls `install` and `appear`.
+`appear` calls `AppearanceChannel#appear(data)` on the server, and supplies a
+data hash of `{ appearing_on: this.appearingOn }`. 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.
- uninstall: ->
- $(document).off(".appearance")
- $(buttonSelector).hide()
-```
+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`.
-##### Client-Server Interaction
-1. **Client** establishes a connection with the **Server** via `App.cable = ActionCable.createConsumer("ws://cable.example.com")`. [*` cable.coffee`*] The **Server** identified this connection instance by `current_user`.
-2. **Client** initiates a subscription to the `Appearance Channel` for their connection via `App.cable.subscriptions.create "AppearanceChannel"`. [*`appearance.coffee`*]
-3. **Server** recognizes a new subscription has been initiated for `AppearanceChannel` channel performs the `subscribed` callback, which calls the `appear` method on the `current_user`. [*`appearance_channel.rb`*]
-4. **Client** recognizes that a subscription has been established and calls `connected` [*`appearance.coffee`*] which in turn calls `@install` and `@appear`. `@appear` calls`AppearanceChannel#appear(data)` on the server, and supplies a data hash of `appearing_on: $("main").data("appearing-on")`. This is possible because the server-side channel instance will automatically expose the public methods declared on the class (minus the callbacks), so that these can be reached as remote procedure calls via a subscription's `perform` method.
-5. **Server** receives the request for the `appear` action on the `AppearanceChannel` channel for the connection identified by `current_user`. [*`appearance_channel.rb`*] The server retrieves the data with the `appearing_on` key from the data hash and sets it as the value for the `on:` key being passed to `current_user.appear`.
-
-### Example 2: Receiving new web notifications
+### 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
@@ -429,7 +567,7 @@ 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:
+Create the server-side web notifications channel:
```ruby
# app/channels/web_notifications_channel.rb
@@ -440,36 +578,47 @@ class WebNotificationsChannel < ApplicationCable::Channel
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"]
+Create the client-side web notifications channel subscription:
+
+```js
+// app/javascript/channels/web_notifications_channel.js
+// Client-side which assumes you've already requested
+// the right to send web notifications.
+import consumer from "./consumer"
+
+consumer.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
+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'
+WebNotificationsChannel.broadcast_to(
+ current_user,
+ title: 'New things!',
+ body: 'All the news fit to print'
+)
```
The `WebNotificationsChannel.broadcast_to` call places a message in the current
-subscription adapter (Redis by default)'s pubsub queue under a separate
-broadcasting name for each user. For a user with an ID of 1, the broadcasting
-name would be `web_notifications_1`.
+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(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`.
+`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
+### More Complete Examples
-See the [rails/actioncable-examples](http://github.com/rails/actioncable-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
@@ -478,110 +627,137 @@ Action Cable has two required configurations: a subscription adapter and allowed
### Subscription Adapter
-By default, `ActionCable::Server::Base` will look for a configuration file
-in `Rails.root.join('config/cable.yml')`. The file must specify an adapter
-and a URL for each Rails environment. See the "Dependencies" section for
-additional information on adapters.
+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
-production: &production
+development:
+ adapter: async
+
+test:
+ adapter: async
+
+production:
adapter: redis
url: redis://10.10.3.153:6381
-development: &development
- adapter: async
-test: *development
+ channel_prefix: appname_production
```
+#### Adapter Configuration
-This format allows you to specify one configuration per Rails environment.
-You can also change the location of the Action Cable config file in
-a Rails initializer with something like:
+Below is a list of the subscription adapters available for end users.
-```ruby
-Rails.application.paths.add "config/redis/cable", with: "somewhere/else/cable.yml"
-```
+##### 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 match will be performed.
+strings or regular expressions, against which a check for the match will be performed.
```ruby
-Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/]
+config.action_cable.allowed_request_origins = ['https://rubyonrails.com', %r{http://ruby.*}]
```
To disable and allow requests from any origin:
```ruby
-Rails.application.config.action_cable.disable_request_forgery_protection = true
+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.
+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.
+
+### Worker Pool Configuration
+
+The worker pool is used to run connection callbacks and channel actions in
+isolation from the server's main thread. Action Cable allows the application
+to configure the number of simultaneously processed threads in the worker pool.
+
+```ruby
+config.action_cable.worker_pool_size = 4
+```
+
+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 4 database connections available.
+ You can change that in `config/database.yml` through the `pool` attribute.
### Other Configurations
-The other common option to configure is the log tags applied to the per-connection logger. Here's close to what we're using in Basecamp:
+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
-Rails.application.config.action_cable.log_tags = [
- -> request { request.env['bc.account_id'] || "no-account" },
+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 100, so that means you have to make at least that available.
-You can change that in `config/database.yml` through the `pool` attribute.
+For a full list of all configuration options, see the
+`ActionCable::Server::Configuration` class.
-## Running standalone cable servers
+## Running Standalone Cable Servers
### In App
Action Cable can run alongside your Rails application. For example, to
-listen for WebSocket requests on `/websocket`, mount the server at that path:
+listen for WebSocket requests on `/websocket`, specify that path to
+`config.action_cable.mount_path`:
```ruby
-# config/routes.rb
-Example::Application.routes.draw do
- mount ActionCable.server => '/cable'
+# config/application.rb
+class Application < Rails::Application
+ config.action_cable.mount_path = '/websocket'
end
```
-You can use `App.cable = ActionCable.createConsumer()` to connect to the
-cable server if `action_cable_meta_tag` is included in the layout. A custom
-path is specified as first argument to `createConsumer`
-(e.g. `App.cable = ActionCable.createConsumer("/websocket")`).
+You can use `ActionCable.createConsumer()` to connect to the cable
+server if `action_cable_meta_tag` is invoked in the layout. Otherwise, A path is
+specified as first argument to `createConsumer` (e.g. `ActionCable.createConsumer("/websocket")`).
-For every instance of your server you create and for every worker
-your server spawns, you will also have a new instance of ActionCable,
-but the use of Redis keeps messages synced across connections.
+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:
+The cable servers can be separated from your normal application server. It's
+still a Rack application, but it is its own Rack application. The recommended
+basic setup is as follows:
```ruby
# cable/config.ru
-require ::File.expand_path('../../config/environment', __FILE__)
+require_relative '../config/environment'
Rails.application.eager_load!
run ActionCable.server
```
-Then you start the server using a binstub in bin/cable ala:
+Then you start the server using a binstub in `bin/cable` ala:
```
#!/bin/bash
@@ -594,13 +770,13 @@ The above will start a cable server on port 28080.
The WebSocket server doesn't have access to the session, but it has
access to the cookies. This can be used when you need to handle
-authentication. You can see one way of doing that with Devise in this [article](http://www.rubytutorial.io/actioncable-devise-authentication).
+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, evented
-Redis, and non-evented Redis adapters are included. The default adapter
+pubsub internals. By default, asynchronous, inline, PostgreSQL, and Redis
+adapters are included. The default adapter
in new Rails applications is the asynchronous (`async`) adapter.
The Ruby side of things is built on top of [websocket-driver](https://github.com/faye/websocket-driver-ruby),
@@ -617,4 +793,10 @@ The Action Cable server implements the Rack socket hijacking API,
thereby allowing the use of a multithreaded pattern for managing connections
internally, irrespective of whether the application server is multi-threaded or not.
-Accordingly, Action Cable works with all the popular application servers -- Unicorn, Puma and Passenger.
+Accordingly, Action Cable works with popular servers like Unicorn, Puma, and
+Passenger.
+
+## Testing
+
+You can find detailed instructions on how to test your Action Cable functionality in the
+[testing guide](testing.html#testing-action-cable).
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index 848c9caa59..f8367283fc 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](https://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" }
@@ -157,7 +157,7 @@ And, assuming that you're sending the data to `CompaniesController`, it would th
{ name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } }
```
-You can customize the name of the key or specific parameters you want to wrap by consulting the [API documentation](http://api.rubyonrails.org/classes/ActionController/ParamsWrapper.html)
+You can customize the name of the key or specific parameters you want to wrap by consulting the [API documentation](https://api.rubyonrails.org/classes/ActionController/ParamsWrapper.html)
NOTE: Support for parsing XML parameters has been extracted into a gem named `actionpack-xml_parser`.
@@ -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
@@ -211,10 +212,10 @@ class PeopleController < ActionController::Base
end
# This will pass with flying colors as long as there's a person key
- # in the parameters, otherwise it'll raise a
+ # in the parameters, otherwise it'll raise an
# ActionController::ParameterMissing exception, which will get
- # caught by ActionController::Base and turned into 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.
-
-# 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.
-
-development:
- secret_key_base: a75d...
+# aws:
+# access_key_id: 123
+# secret_access_key: 345
-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
@@ -472,14 +469,14 @@ To reset the entire session, use `reset_session`.
The flash is a special part of the session which is cleared with each request. This means that values stored there will only be available in the next request, which is useful for passing error messages etc.
-It is accessed in much the same way as the session, as a hash (it's a [FlashHash](http://api.rubyonrails.org/classes/ActionDispatch/Flash/FlashHash.html) instance).
+It is accessed in much the same way as the session, as a hash (it's a [FlashHash](https://api.rubyonrails.org/classes/ActionDispatch/Flash/FlashHash.html) instance).
Let's use the act of logging out as an example. The controller can send a message which will be displayed to the user on the next request:
```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
@@ -594,7 +591,7 @@ Rails also provides a signed cookie jar and an encrypted cookie jar for storing
sensitive data. The signed cookie jar appends a cryptographic signature on the
cookie values to protect their integrity. The encrypted cookie jar encrypts the
values in addition to signing them, so that they cannot be read by the end user.
-Refer to the [API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Cookies.html)
+Refer to the [API documentation](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html)
for more details.
These special cookie jars use a serializer to serialize the assigned values into
@@ -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
@@ -702,11 +699,14 @@ 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 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 %>
```
@@ -814,7 +814,7 @@ In every controller there are two accessor methods pointing to the request and t
### 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](https://api.rubyonrails.org/classes/ActionDispatch/Request.html) and [Rack Documentation](https://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](https://api.rubyonrails.org/classes/ActionDispatch/Response.html) and [Rack Documentation](https://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
--------------------
@@ -1116,7 +1116,7 @@ Rails default exception handling displays a "500 Server Error" message for all e
### The Default 500 and 404 Templates
-By default a production application will render either a 404 or a 500 error message, in the development environment all unhandled exceptions are raised. These messages are contained in static HTML files in the `public` folder, in `404.html` and `500.html` respectively. You can customize these files to add some extra information and style, but remember that they are static HTML; i.e. you can't use ERB, SCSS, CoffeeScript, or layouts for them.
+By default a production application will render either a 404 or a 500 error message, in the development environment all unhandled exceptions are raised. These messages are contained in static HTML files in the public folder, in `404.html` and `500.html` respectively. You can customize these files to add some extra information and style, but remember that they are static HTML; i.e. you can't use ERB, SCSS, CoffeeScript, or layouts for them.
### `rescue_from`
@@ -1170,7 +1170,7 @@ 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: When running in the production environment, all
`ActiveRecord::RecordNotFound` errors render the 404 error page. Unless you need
@@ -1181,22 +1181,6 @@ NOTE: Certain exceptions are only rescuable from the `ApplicationController` cla
Force HTTPS protocol
--------------------
-Sometime you might want to force a particular controller to only be accessible via an HTTPS protocol for security reasons. You can use the `force_ssl` method in your controller to enforce that:
-
-```ruby
-class DinnerController
- force_ssl
-end
-```
-
-Just like the filter, you could also pass `:only` and `:except` to enforce the secure connection only to specific actions:
-
-```ruby
-class DinnerController
- force_ssl only: :cheeseburger
- # or
- force_ssl except: :cheeseburger
-end
-```
-
-Please note that if you find yourself adding `force_ssl` to many controllers, you may want to force the whole application to use HTTPS instead. In that case, you can set the `config.force_ssl` in your environment file.
+If you'd like to ensure that communication to your controller is only possible
+via HTTPS, you should do so by enabling the `ActionDispatch::SSL` middleware via
+`config.force_ssl` in your environment configuration.
diff --git a/guides/source/action_mailbox_basics.md b/guides/source/action_mailbox_basics.md
new file mode 100644
index 0000000000..c90892d456
--- /dev/null
+++ b/guides/source/action_mailbox_basics.md
@@ -0,0 +1,398 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
+
+Action Mailbox Basics
+=====================
+
+This guide provides you with all you need to get started in receiving
+emails to your application.
+
+After reading this guide, you will know:
+
+* How to receive email within a Rails application.
+* How to configure Action Mailbox.
+* How to generate and route emails to a mailbox.
+* How to test incoming emails.
+
+--------------------------------------------------------------------------------
+
+Introduction
+------------
+
+Action Mailbox routes incoming emails to controller-like mailboxes for
+processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill,
+Postmark, and SendGrid. You can also handle inbound mails directly via the
+built-in Exim, Postfix, and Qmail ingresses.
+
+The inbound emails are turned into `InboundEmail` records using Active Record
+and feature lifecycle tracking, storage of the original email on cloud storage
+via Active Storage, and responsible data handling with
+on-by-default incineration.
+
+These inbound emails are routed asynchronously using Active Job to one or
+several dedicated mailboxes, which are capable of interacting directly
+with the rest of your domain model.
+
+## Setup
+
+Install migrations needed for `InboundEmail` and ensure Active Storage is set up:
+
+```bash
+$ rails action_mailbox:install
+$ rails db:migrate
+```
+
+## Configuration
+
+### Amazon SES
+
+Install the [`aws-sdk-sns`](https://rubygems.org/gems/aws-sdk-sns) gem:
+
+```ruby
+# Gemfile
+gem "aws-sdk-sns", ">= 1.9.0", require: false
+```
+
+Tell Action Mailbox to accept emails from SES:
+
+```ruby
+# config/environments/production.rb
+config.action_mailbox.ingress = :amazon
+```
+
+[Configure SES](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html)
+to deliver emails to your application via POST requests to
+`/rails/action_mailbox/amazon/inbound_emails`. If your application lived at
+`https://example.com`, you would specify the fully-qualified URL
+`https://example.com/rails/action_mailbox/amazon/inbound_emails`.
+
+### Exim
+
+Tell Action Mailbox to accept emails from an SMTP relay:
+
+```ruby
+# config/environments/production.rb
+config.action_mailbox.ingress = :relay
+```
+
+Generate a strong password that Action Mailbox can use to authenticate requests to the relay ingress.
+
+Use `rails credentials:edit` to add the password to your application's encrypted credentials under
+`action_mailbox.ingress_password`, where Action Mailbox will automatically find it:
+
+```yaml
+action_mailbox:
+ ingress_password: ...
+```
+
+Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable.
+
+Configure Exim to pipe inbound emails to `bin/rails action_mailbox:ingress:exim`,
+providing the `URL` of the relay ingress and the `INGRESS_PASSWORD` you
+previously generated. If your application lived at `https://example.com`, the
+full command would look like this:
+
+```shell
+bin/rails action_mailbox:ingress:exim URL=https://example.com/rails/action_mailbox/relay/inbound_emails INGRESS_PASSWORD=...
+```
+
+### Mailgun
+
+Give Action Mailbox your
+[Mailgun API key](https://help.mailgun.com/hc/en-us/articles/203380100-Where-can-I-find-my-API-key-and-SMTP-credentials)
+so it can authenticate requests to the Mailgun ingress.
+
+Use `rails credentials:edit` to add your API key to your application's
+encrypted credentials under `action_mailbox.mailgun_api_key`,
+where Action Mailbox will automatically find it:
+
+```yaml
+action_mailbox:
+ mailgun_api_key: ...
+```
+
+Alternatively, provide your API key in the `MAILGUN_INGRESS_API_KEY` environment
+variable.
+
+Tell Action Mailbox to accept emails from Mailgun:
+
+```ruby
+# config/environments/production.rb
+config.action_mailbox.ingress = :mailgun
+```
+
+[Configure Mailgun](https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages)
+to forward inbound emails to `/rails/action_mailbox/mailgun/inbound_emails/mime`.
+If your application lived at `https://example.com`, you would specify the
+fully-qualified URL `https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime`.
+
+### Mandrill
+
+Give Action Mailbox your Mandrill API key so it can authenticate requests to
+the Mandrill ingress.
+
+Use `rails credentials:edit` to add your API key to your application's
+encrypted credentials under `action_mailbox.mandrill_api_key`,
+where Action Mailbox will automatically find it:
+
+```yaml
+action_mailbox:
+ mandrill_api_key: ...
+```
+
+Alternatively, provide your API key in the `MANDRILL_INGRESS_API_KEY`
+environment variable.
+
+Tell Action Mailbox to accept emails from Mandrill:
+
+```ruby
+# config/environments/production.rb
+config.action_mailbox.ingress = :mandrill
+```
+
+[Configure Mandrill](https://mandrill.zendesk.com/hc/en-us/articles/205583197-Inbound-Email-Processing-Overview)
+to route inbound emails to `/rails/action_mailbox/mandrill/inbound_emails`.
+If your application lived at `https://example.com`, you would specify
+the fully-qualified URL `https://example.com/rails/action_mailbox/mandrill/inbound_emails`.
+
+### Postfix
+
+Tell Action Mailbox to accept emails from an SMTP relay:
+
+```ruby
+# config/environments/production.rb
+config.action_mailbox.ingress = :relay
+```
+
+Generate a strong password that Action Mailbox can use to authenticate requests to the relay ingress.
+
+Use `rails credentials:edit` to add the password to your application's encrypted credentials under
+`action_mailbox.ingress_password`, where Action Mailbox will automatically find it:
+
+```yaml
+action_mailbox:
+ ingress_password: ...
+```
+
+Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable.
+
+[Configure Postfix](https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script)
+to pipe inbound emails to `bin/rails action_mailbox:ingress:postfix`, providing
+the `URL` of the Postfix ingress and the `INGRESS_PASSWORD` you previously
+generated. If your application lived at `https://example.com`, the full command
+would look like this:
+
+```shell
+$ bin/rails action_mailbox:ingress:postfix URL=https://example.com/rails/action_mailbox/relay/inbound_emails INGRESS_PASSWORD=...
+```
+
+### Postmark
+
+Tell Action Mailbox to accept emails from Postmark:
+
+```ruby
+# config/environments/production.rb
+config.action_mailbox.ingress = :postmark
+```
+
+Generate a strong password that Action Mailbox can use to authenticate
+requests to the Postmark ingress.
+
+Use `rails credentials:edit` to add the password to your application's
+encrypted credentials under `action_mailbox.ingress_password`,
+where Action Mailbox will automatically find it:
+
+```yaml
+action_mailbox:
+ ingress_password: ...
+```
+
+Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD`
+environment variable.
+
+[Configure Postmark inbound webhook](https://postmarkapp.com/manual#configure-your-inbound-webhook-url)
+to forward inbound emails to `/rails/action_mailbox/postmark/inbound_emails` with the username `actionmailbox`
+and the password you previously generated. If your application lived at `https://example.com`, you would
+configure Postmark with the following fully-qualified URL:
+
+```
+https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emails
+```
+
+NOTE: When configuring your Postmark inbound webhook, be sure to check the box labeled **"Include raw email content in JSON payload"**.
+Action Mailbox needs the raw email content to work.
+
+### Qmail
+
+Tell Action Mailbox to accept emails from an SMTP relay:
+
+```ruby
+# config/environments/production.rb
+config.action_mailbox.ingress = :relay
+```
+
+Generate a strong password that Action Mailbox can use to authenticate requests to the relay ingress.
+
+Use `rails credentials:edit` to add the password to your application's encrypted credentials under
+`action_mailbox.ingress_password`, where Action Mailbox will automatically find it:
+
+```yaml
+action_mailbox:
+ ingress_password: ...
+```
+
+Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable.
+
+Configure Qmail to pipe inbound emails to `bin/rails action_mailbox:ingress:qmail`,
+providing the `URL` of the relay ingress and the `INGRESS_PASSWORD` you
+previously generated. If your application lived at `https://example.com`, the
+full command would look like this:
+
+```shell
+bin/rails action_mailbox:ingress:qmail URL=https://example.com/rails/action_mailbox/relay/inbound_emails INGRESS_PASSWORD=...
+```
+
+### SendGrid
+
+Tell Action Mailbox to accept emails from SendGrid:
+
+```ruby
+# config/environments/production.rb
+config.action_mailbox.ingress = :sendgrid
+```
+
+Generate a strong password that Action Mailbox can use to authenticate
+requests to the SendGrid ingress.
+
+Use `rails credentials:edit` to add the password to your application's
+encrypted credentials under `action_mailbox.ingress_password`,
+where Action Mailbox will automatically find it:
+
+```yaml
+action_mailbox:
+ ingress_password: ...
+```
+
+Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD`
+environment variable.
+
+[Configure SendGrid Inbound Parse](https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/)
+to forward inbound emails to
+`/rails/action_mailbox/sendgrid/inbound_emails` with the username `actionmailbox`
+and the password you previously generated. If your application lived at `https://example.com`,
+you would configure SendGrid with the following URL:
+
+```
+https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails
+```
+
+NOTE: When configuring your SendGrid Inbound Parse webhook, be sure to check the box labeled **“Post the raw, full MIME message.â€** Action Mailbox needs the raw MIME message to work.
+
+## Examples
+
+Configure basic routing:
+
+```ruby
+# app/mailboxes/application_mailbox.rb
+class ApplicationMailbox < ActionMailbox::Base
+ routing /^save@/i => :forwards
+ routing /@replies\./i => :replies
+end
+```
+
+Then set up a mailbox:
+
+```ruby
+# Generate new mailbox
+$ bin/rails generate mailbox forwards
+```
+
+```ruby
+# app/mailboxes/forwards_mailbox.rb
+class ForwardsMailbox < ApplicationMailbox
+ # Callbacks specify prerequisites to processing
+ before_processing :require_forward
+
+ def process
+ if forwarder.buckets.one?
+ record_forward
+ else
+ stage_forward_and_request_more_details
+ end
+ end
+
+ private
+ def require_forward
+ unless message.forward?
+ # Use Action Mailers to bounce incoming emails back to sender – this halts processing
+ bounce_with Forwards::BounceMailer.missing_forward(
+ inbound_email, forwarder: forwarder
+ )
+ end
+ end
+
+ def forwarder
+ @forwarder ||= Person.where(email_address: mail.from)
+ end
+
+ def record_forward
+ forwarder.buckets.first.record \
+ Forward.new forwarder: forwarder, subject: message.subject, content: mail.content
+ end
+
+ def stage_forward_and_request_more_details
+ Forwards::RoutingMailer.choose_project(mail).deliver_now
+ end
+end
+```
+
+## Incineration of InboundEmails
+
+By default, an InboundEmail that has been successfully processed will be
+incinerated after 30 days. This ensures you're not holding on to people's data
+willy-nilly after they may have canceled their accounts or deleted their
+content. The intention is that after you've processed an email, you should have
+extracted all the data you needed and turned it into domain models and content
+on your side of the application. The InboundEmail simply stays in the system
+for the extra time to provide debugging and forensics options.
+
+The actual incineration is done via the `IncinerationJob` that's scheduled
+to run after `config.action_mailbox.incinerate_after` time. This value is
+by default set to `30.days`, but you can change it in your production.rb
+configuration. (Note that this far-future incineration scheduling relies on
+your job queue being able to hold jobs for that long.)
+
+## Working with Action Mailbox in development
+
+It's helpful to be able to test incoming emails in development without actually
+sending and receiving real emails. To accomplish this, there's a conductor
+controller mounted at `/rails/conductor/action_mailbox/inbound_emails`,
+which gives you an index of all the InboundEmails in the system, their
+state of processing, and a form to create a new InboundEmail as well.
+
+## Testing mailboxes
+
+Example:
+
+```ruby
+class ForwardsMailboxTest < ActionMailbox::TestCase
+ test "directly recording a client forward for a forwarder and forwardee corresponding to one project" do
+ assert_difference -> { people(:david).buckets.first.recordings.count } do
+ receive_inbound_email_from_mail \
+ to: 'save@example.com',
+ from: people(:david).email_address,
+ subject: "Fwd: Status update?",
+ body: <<~BODY
+ --- Begin forwarded message ---
+ From: Frank Holland <frank@microsoft.com>
+
+ What's the status?
+ BODY
+ end
+
+ recording = people(:david).buckets.first.recordings.last
+ assert_equal people(:david), recording.creator
+ assert_equal "Status update?", recording.forward.subject
+ assert_match "What's the status?", recording.forward.content.to_s
+ end
+end
+```
diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md
index 5346b7c32b..f600cf29ce 100644
--- a/guides/source/action_mailer_basics.md
+++ b/guides/source/action_mailer_basics.md
@@ -1,15 +1,15 @@
-**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
====================
-This guide provides you with all you need to get started in sending and
-receiving emails from and to your application, and many internals of Action
+This guide provides you with all you need to get started in sending
+emails from and to your application, and many internals of Action
Mailer. It also covers how to test your mailers.
After reading this guide, you will know:
-* How to send and receive email within a Rails application.
+* How to send email within a Rails application.
* How to generate and edit an Action Mailer class and mailer view.
* How to configure Action Mailer for your environment.
* How to test your Action Mailer classes.
@@ -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,23 +164,23 @@ 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.
+Setting this up is simple.
First, let's create a simple `User` scaffold:
```bash
-$ bin/rails generate scaffold user name email login
-$ bin/rails 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 }
@@ -213,6 +217,8 @@ 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`:
@@ -220,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
```
+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
@@ -261,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')
@@ -331,7 +342,7 @@ with the addresses separated by commas.
```ruby
class AdminMailer < ApplicationMailer
- default to: Proc.new { Admin.pluck(:email) },
+ default to: -> { Admin.pluck(:email) },
from: 'notification@example.com'
def new_registration(user)
@@ -351,8 +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
@@ -372,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',
@@ -394,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
@@ -411,9 +422,24 @@ 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 do cache in mailer views like in application views using `cache` method.
+You can perform fragment caching in mailer views like in application views using the `cache` method.
```
<% cache do %>
@@ -427,6 +453,9 @@ And in order to use this feature, you need to configure your application with th
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
@@ -450,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
@@ -474,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
```
@@ -525,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`.
@@ -544,14 +573,15 @@ web addresses. Thus, you should always use the "_url" variant of named route
helpers.
If you did not configure the `:host` option globally make sure to pass it to the
-url helper.
+URL helper.
```erb
<%= 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
@@ -559,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'
@@ -574,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.
@@ -590,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)
@@ -612,57 +642,17 @@ 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
end
```
-Receiving Emails
-----------------
-
-Receiving and parsing emails with Action Mailer can be a rather complex
-endeavor. Before your email reaches your Rails app, you would have had to
-configure your system to somehow forward emails to your app, which needs to be
-listening for that. So, to receive emails in your Rails app you'll need to:
-
-* Implement a `receive` method in your mailer.
-
-* Configure your email server to forward emails from the address(es) you would
- like your app to receive to `/path/to/app/bin/rails runner
- 'UserMailer.receive(STDIN.read)'`.
-
-Once a method called `receive` is defined in any mailer, Action Mailer will
-parse the raw incoming email into an email object, decode it, instantiate a new
-mailer, and pass the email object to the mailer `receive` instance
-method. Here's an example:
-
-```ruby
-class UserMailer < ApplicationMailer
- def receive(email)
- page = Page.find_by(address: email.to.first)
- page.emails.create(
- subject: email.subject,
- body: email.body
- )
-
- if email.has_attachments?
- email.attachments.each do |attachment|
- page.attachments.create({
- file: attachment,
- description: email.subject
- })
- end
- end
- end
-end
-```
-
Action Mailer Callbacks
----------------------------
+-----------------------
Action Mailer allows for you to specify a `before_action`, `after_action` and
`around_action`.
@@ -673,24 +663,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
@@ -734,11 +743,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.|
+|`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](https://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. 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.).|
@@ -756,7 +765,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
@@ -777,10 +786,11 @@ 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 or
+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
@@ -789,13 +799,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
@@ -819,3 +830,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_text_overview.md b/guides/source/action_text_overview.md
new file mode 100644
index 0000000000..08ec35e52a
--- /dev/null
+++ b/guides/source/action_text_overview.md
@@ -0,0 +1,99 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
+
+Action Text Overview
+====================
+
+This guide provides you with all you need to get started in handling
+rich text content.
+
+After reading this guide, you will know:
+
+* How to configure Action Text.
+* How to handle rich text content.
+* How to style rich text content.
+
+--------------------------------------------------------------------------------
+
+Introduction
+------------
+
+Action Text brings rich text content and editing to Rails. It includes
+the [Trix editor](https://trix-editor.org) that handles everything from formatting
+to links to quotes to lists to embedded images and galleries.
+The rich text content generated by the Trix editor is saved in its own
+RichText model that's associated with any existing Active Record model in the application.
+Any embedded images (or other attachments) are automatically stored using
+Active Storage and associated with the included RichText model.
+
+## Trix compared to other rich text editors
+
+Most WYSIWYG editors are wrappers around HTML’s `contenteditable` and `execCommand` APIs,
+designed by Microsoft to support live editing of web pages in Internet Explorer 5.5,
+and [eventually reverse-engineered](https://blog.whatwg.org/the-road-to-html-5-contenteditable#history)
+and copied by other browsers.
+
+Because these APIs were never fully specified or documented,
+and because WYSIWYG HTML editors are enormous in scope, each
+browser's implementation has its own set of bugs and quirks,
+and JavaScript developers are left to resolve the inconsistencies.
+
+Trix sidesteps these inconsistencies by treating contenteditable
+as an I/O device: when input makes its way to the editor, Trix converts that input
+into an editing operation on its internal document model, then re-renders
+that document back into the editor. This gives Trix complete control over what
+happens after every keystroke, and avoids the need to use execCommand at all.
+
+## Installation
+
+Run `rails action_text:install` to add the Yarn package and copy over the necessary migration.
+
+## Examples
+
+Adding a rich text field to an existing model:
+
+```ruby
+# app/models/message.rb
+class Message < ApplicationRecord
+ has_rich_text :content
+end
+```
+
+Then refer to this field in the form for the model:
+
+```erb
+<%# app/views/messages/_form.html.erb %>
+<%= form_with(model: message) do |form| %>
+ <div class="field">
+ <%= form.label :content %>
+ <%= form.rich_text_area :content %>
+ </div>
+<% end %>
+```
+
+And finally display the sanitized rich text on a page:
+
+```erb
+<%= @message.content %>
+```
+
+To accept the rich text content, all you have to do is permit the referenced attribute:
+
+```ruby
+class MessagesController < ApplicationController
+ def create
+ message = Message.create! params.require(:message).permit(:title, :content)
+ redirect_to message
+ end
+end
+```
+
+## Custom styling
+
+By default, the Action Text editor and content is styled by the Trix defaults.
+If you want to change these defaults, you'll want to remove
+the `app/assets/stylesheets/actiontext.css` linker and base your stylings on
+the [contents of that file](https://raw.githubusercontent.com/basecamp/trix/master/dist/trix.css).
+
+You can also style the HTML used for embedded images and other attachments (known as blobs).
+On installation, Action Text will copy over a partial to
+`app/views/active_storage/blobs/_blob.html.erb`, which you can specialize.
diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md
index d49df23e4a..a1b69edd22 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?
--------------------
-In Rails, web requests are handled by [Action Controller](action_controller_overview.html) and Action View. 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:
@@ -91,7 +91,7 @@ Here are some basic examples:
```ruby
xml.em("emphasized")
xml.em { xml.b("emph & bold") }
-xml.a("A Link", "href" => "http://rubyonrails.org")
+xml.a("A Link", "href" => "https://rubyonrails.org")
xml.target("name" => "compile", "option" => "fast")
```
@@ -100,7 +100,7 @@ which would produce:
```html
<em>emphasized</em>
<em><b>emph &amp; bold</b></em>
-<a href="http://rubyonrails.org">A link</a>
+<a href="https://rubyonrails.org">A link</a>
<target option="fast" name="compile" />
```
@@ -149,10 +149,10 @@ end
#### Jbuilder
[Jbuilder](https://github.com/rails/jbuilder) is a gem that's
-maintained by the Rails team and included in the default Rails Gemfile.
+maintained by the Rails team and included in the default Rails `Gemfile`.
It's similar to Builder, but is used to generate JSON, instead of XML.
-If you don't have it, you can add the following to your Gemfile:
+If you don't have it, you can add the following to your `Gemfile`:
```ruby
gem 'jbuilder'
@@ -254,12 +254,6 @@ 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 (e.g. in a different instance variable or in a local variable).
For example, instead of:
@@ -274,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.
@@ -402,9 +402,9 @@ This will add `app/views/direct` to the end of the lookup paths.
Overview of helpers provided by Action View
-------------------------------------------
-WIP: Not all the helpers are listed here. For a full list see the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html)
+WIP: Not all the helpers are listed here. For a full list see the [API documentation](https://api.rubyonrails.org/classes/ActionView/Helpers.html)
-The following is only a brief overview summary of the helpers available in Action View. It's recommended that you review the [API Documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html), which covers all of the helpers in more detail, but this should serve as a good starting point.
+The following is only a brief overview summary of the helpers available in Action View. It's recommended that you review the [API Documentation](https://api.rubyonrails.org/classes/ActionView/Helpers.html), which covers all of the helpers in more detail, but this should serve as a good starting point.
### AssetTagHelper
@@ -414,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" }) # =>
@@ -453,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
@@ -464,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.
@@ -507,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
@@ -792,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
@@ -839,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
@@ -1132,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
@@ -1143,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
@@ -1160,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
@@ -1192,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
@@ -1297,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
@@ -1419,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
@@ -1439,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
@@ -1476,7 +1446,7 @@ 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="https://rubyonrails.org">Ruby on Rails</a>')
# => Ruby on Rails
```
@@ -1493,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!")
diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md
index d8ea1ee079..0925ad3d74 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,16 +59,16 @@ 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(*guests)
@@ -109,18 +110,18 @@ That's it!
Job Execution
-------------
-For enqueuing and executing jobs in production you need to set up a queuing backend,
+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 back-end. This may be fine for smaller apps or non-critical jobs, but most
+default async backend. This may be fine for smaller apps or non-critical jobs, but most
production apps will need to pick a persistent backend.
### Backends
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).
+Resque, Delayed Job, and others). To get an up-to-date list of the adapters
+see the API Documentation for [ActiveJob::QueueAdapters](https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
### Setting the Backend
@@ -138,6 +139,18 @@ module YourApp
end
```
+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
@@ -149,8 +162,10 @@ 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)
+- [Delayed Job](https://github.com/collectiveidea/delayed_job#active-job)
Queues
------
@@ -159,7 +174,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
@@ -177,7 +192,7 @@ module YourApp
end
# app/jobs/guests_cleanup_job.rb
-class GuestsCleanupJob < ActiveJob::Base
+class GuestsCleanupJob < ApplicationJob
queue_as :low_priority
#....
end
@@ -200,7 +215,7 @@ module YourApp
end
# app/jobs/guests_cleanup_job.rb
-class GuestsCleanupJob < ActiveJob::Base
+class GuestsCleanupJob < ApplicationJob
queue_as :low_priority
#....
end
@@ -222,7 +237,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?
@@ -247,40 +262,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
------------
@@ -297,6 +320,12 @@ 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
--------------------
@@ -311,15 +340,30 @@ UserMailer.welcome(@user).deliver_later # Email will be localized to Esperanto.
```
-GlobalID
---------
+Supported types for arguments
+----------------------------
+
+ActiveJob supports the following types of arguments by default:
+
+ - Basic types (`NilClass`, `String`, `Integer`, `Float`, `BigDecimal`, `TrueClass`, `FalseClass`)
+ - `Symbol`
+ - `Date`
+ - `Time`
+ - `DateTime`
+ - `ActiveSupport::TimeWithZone`
+ - `ActiveSupport::Duration`
+ - `Hash` (Keys should be of `String` or `Symbol` type)
+ - `ActiveSupport::HashWithIndifferentAccess`
+ - `Array`
+
+### GlobalID
Active Job supports GlobalID for parameters. This makes it possible to pass live
Active Record objects to your job instead of class/id pairs, which you then have
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)
@@ -330,7 +374,7 @@ end
Now you can simply do:
```ruby
-class TrashableCleanupJob < ActiveJob::Base
+class TrashableCleanupJob < ApplicationJob
def perform(trashable, depth)
trashable.cleanup(depth)
end
@@ -340,6 +384,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
----------
@@ -348,11 +425,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
@@ -361,6 +438,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](https://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 a8199e5d02..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
===================
@@ -13,7 +13,7 @@ After reading this guide, you will know:
* How an Active Record model behaves.
* How Callbacks and validations work.
* How serializers work.
-* The Rails internationalization (i18n) framework.
+* How Active Model integrates with the Rails internationalization (i18n) framework.
--------------------------------------------------------------------------------
@@ -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 true if any of the attributes have unsaved changes, false otherwise.
+# returns true if any of the attributes have unsaved changes.
person.changed? # => true
# returns a list of attributes that have changed before saving.
person.changed # => ["first_name"]
-# returns a hash of the attributes that have changed with their original values.
+# returns a Hash of the attributes that have changed with their original values.
person.changed_attributes # => {"first_name"=>nil}
-# returns a hash of changes, with the attribute names as the keys, and the values will be an array of the old and new value for that field.
+# returns a Hash of changes, with the attribute names as the keys, and the
+# values as an array of the old and new values for that field.
person.changes # => {"first_name"=>[nil, "First Name"]}
```
@@ -179,7 +180,7 @@ person.first_name # => "First Name"
person.first_name_changed? # => true
```
-Track what was the previous value of the attribute.
+Track the previous value of the attribute.
```ruby
# attr_name_was accessor
@@ -187,7 +188,7 @@ person.first_name_was # => nil
```
Track both previous and current value of the changed attribute. Returns an array
-if changed, else returns nil.
+if changed, otherwise returns nil.
```ruby
# attr_name_change
@@ -197,7 +198,7 @@ person.last_name_change # => nil
### Validations
-The `ActiveModel::Validations` module adds the ability to validate class objects
+The `ActiveModel::Validations` module adds the ability to validate objects
like in Active Record.
```ruby
@@ -225,7 +226,7 @@ person.valid? # => raises ActiveModel::StrictValidationFa
### Naming
-`ActiveModel::Naming` adds a number of class methods which make the naming and routing
+`ActiveModel::Naming` adds a number of class methods which make naming and routing
easier to manage. The module defines the `model_name` class method which
will define a number of accessors using some `ActiveSupport::Inflector` methods.
@@ -248,7 +249,7 @@ Person.model_name.singular_route_key # => "person"
### Model
-`ActiveModel::Model` adds the ability to a class to work with Action Pack and
+`ActiveModel::Model` adds the ability for a class to work with Action Pack and
Action View right out of the box.
```ruby
@@ -293,7 +294,7 @@ objects.
### Serialization
`ActiveModel::Serialization` provides basic serialization for your object.
-You need to declare an attributes hash which contains the attributes you want to
+You need to declare an attributes Hash which contains the attributes you want to
serialize. Attributes must be strings, not symbols.
```ruby
@@ -308,7 +309,7 @@ class Person
end
```
-Now you can access a serialized hash of your object using the `serializable_hash`.
+Now you can access a serialized Hash of your object using the `serializable_hash` method.
```ruby
person = Person.new
@@ -319,13 +320,14 @@ person.serializable_hash # => {"name"=>"Bob"}
#### ActiveModel::Serializers
-Rails provides an `ActiveModel::Serializers::JSON` serializer.
-This module automatically include the `ActiveModel::Serialization`.
+Active Model also provides the `ActiveModel::Serializers::JSON` module
+for JSON serializing / deserializing. This module automatically includes the
+previously discussed `ActiveModel::Serialization` module.
##### ActiveModel::Serializers::JSON
-To use the `ActiveModel::Serializers::JSON` you only need to change from
-`ActiveModel::Serialization` to `ActiveModel::Serializers::JSON`.
+To use `ActiveModel::Serializers::JSON` you only need to change the
+module you are including from `ActiveModel::Serialization` to `ActiveModel::Serializers::JSON`.
```ruby
class Person
@@ -339,7 +341,8 @@ class Person
end
```
-With the `as_json` method you have a hash representing the model.
+The `as_json` method, similar to `serializable_hash`, provides a Hash representing
+the model.
```ruby
person = Person.new
@@ -348,8 +351,8 @@ person.name = "Bob"
person.as_json # => {"name"=>"Bob"}
```
-From a JSON string you define the attributes of the model.
-You need to have the `attributes=` method defined on your class:
+You can also define the attributes for a model from a JSON string.
+However, you need to define the `attributes=` method on your class:
```ruby
class Person
@@ -369,7 +372,7 @@ class Person
end
```
-Now it is possible to create an instance of person and set the attributes using `from_json`.
+Now it is possible to create an instance of `Person` and set attributes using `from_json`.
```ruby
json = { name: 'Bob' }.to_json
@@ -389,8 +392,8 @@ class Person
end
```
-With the `human_attribute_name` you can transform attribute names into a more
-human format. The human format is defined in your locale file.
+With the `human_attribute_name` method, you can transform attribute names into a
+more human-readable format. The human-readable format is defined in your locale file(s).
* config/locales/app.pt-BR.yml
@@ -411,16 +414,15 @@ Person.human_attribute_name('name') # => "Nome"
`ActiveModel::Lint::Tests` allows you to test whether an object is compliant with
the Active Model API.
-* app/models/person.rb
+* `app/models/person.rb`
```ruby
class Person
include ActiveModel::Model
-
end
```
-* test/models/person_test.rb
+* `test/models/person_test.rb`
```ruby
require 'test_helper'
@@ -455,19 +457,20 @@ 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 [`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:
+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.
+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
@@ -476,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
@@ -493,7 +498,24 @@ person.valid? # => false
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 d9e9466a33..7f17b19a13 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
@@ -29,7 +29,7 @@ Object Relational Mapping system.
### The Active Record Pattern
-[Active Record was described by Martin Fowler](http://www.martinfowler.com/eaaCatalog/activeRecord.html)
+[Active Record was described by Martin Fowler](https://www.martinfowler.com/eaaCatalog/activeRecord.html)
in his book _Patterns of Enterprise Application Architecture_. In
Active Record, objects carry both persistent data and behavior which
operates on that data. Active Record takes the opinion that ensuring
@@ -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: Basic knowledge of relational database management systems (RDBMS) and structured query language (SQL) is helpful in order to fully understand Active Record. Please refer to [this tutorial](https://www.w3schools.com/sql/default.asp) (or [this one](http://www.sqlcourse.com/)) or study them by other means if you would like to learn more.
+
### 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 |
| ---------------- | -------------- |
@@ -103,9 +105,9 @@ depending on the purpose of these columns.
fields that Active Record will look for when you create associations between
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
- automatically created.
+ `id` as the table's primary key (`bigint` for Postgres and MYSQL, `integer`
+ for SQLite). When using [Active Record 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
to Active Record instances:
@@ -113,12 +115,12 @@ 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
+ locking](https://api.rubyonrails.org/classes/ActiveRecord/Locking.html) to
a model.
* `type` - Specifies that the model uses [Single Table
- Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance).
+ Inheritance](https://api.rubyonrails.org/classes/ActiveRecord/Base.html#class-ActiveRecord::Base-label-Single+table+inheritance).
* `(association_name)_type` - Stores the type for
[polymorphic associations](association_basics.html#polymorphic-associations).
* `(table_name)_count` - Used to cache the number of belonging objects on
@@ -142,7 +144,7 @@ end
This will create a `Product` model, mapped to a `products` table at the
database. By doing this you'll also have the ability to map the columns of each
row in that table with the attributes of the instances of your model. Suppose
-that the `products` table was created using an SQL statement like:
+that the `products` table was created using an SQL (or one of its extensions) statement like:
```sql
CREATE TABLE products (
@@ -152,8 +154,9 @@ CREATE TABLE products (
);
```
-Following the table schema above, you would be able to write code like the
-following:
+Schema above declares a table with two columns: `id` and `name`. Each row of
+this table represents a certain product with these two parameters. Thus, you
+would be able to write code like the following:
```ruby
p = Product.new
@@ -199,6 +202,8 @@ class Product < ApplicationRecord
end
```
+NOTE: Active Record does not support using non-primary key columns named `id`.
+
CRUD: Reading and Writing Data
------------------------------
@@ -208,7 +213,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.
@@ -304,18 +309,29 @@ user = User.find_by(name: 'David')
user.destroy
```
+If you'd like to delete several records in bulk, you may use `destroy_by`
+or `destroy_all` method:
+
+```ruby
+# find and delete all users named David
+User.destroy_by(name: 'David')
+
+# 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:
@@ -339,7 +355,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
@@ -373,5 +389,5 @@ provides rollback features. To actually create the table, you'd run `rails db:mi
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 fb5d2065d3..614737c342 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
=======================
@@ -36,7 +36,7 @@ class User < ApplicationRecord
before_validation :ensure_login_has_a_value
- protected
+ private
def ensure_login_has_a_value
if login.nil?
self.login = email unless email.blank?
@@ -66,7 +66,7 @@ class User < ApplicationRecord
# :on takes an array as well
after_validation :set_location, on: [ :create, :update ]
- protected
+ private
def normalize_name
self.name = name.downcase.titleize
end
@@ -77,7 +77,7 @@ class User < ApplicationRecord
end
```
-It is considered good practice to declare callback methods as protected or private. If left public, they can be called from outside of the model and violate the principle of object encapsulation.
+It is considered good practice to declare callback methods as private. If left public, they can be called from outside of the model and violate the principle of object encapsulation.
Available Callbacks
-------------------
@@ -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.
@@ -180,9 +184,9 @@ class Company < ApplicationRecord
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!`
@@ -236,14 +239,12 @@ Skipping Callbacks
Just as with validations, it is also possible to skip callbacks by using the following methods:
-* `decrement`
+* `decrement!`
* `decrement_counter`
* `delete`
* `delete_all`
-* `increment`
+* `increment!`
* `increment_counter`
-* `toggle`
-* `touch`
* `update_column`
* `update_columns`
* `update_all`
@@ -256,9 +257,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:
-WARNING. Any exception that is not `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception.
+```ruby
+throw :abort
+```
+
+WARNING. Any exception that is not `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` may break code that does not expect methods like `save` and `update` (which normally try to return `true` or `false`) to raise an exception.
Relational Callbacks
--------------------
@@ -290,7 +295,7 @@ Article destroyed
Conditional Callbacks
---------------------
-As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a string, a `Proc` or an `Array`. You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option.
+As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a `Proc` or an `Array`. You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option.
### Using `:if` and `:unless` with a `Symbol`
@@ -302,24 +307,22 @@ class Order < ApplicationRecord
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:
+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 < ApplicationRecord
- before_save :normalize_card_number, if: "paid_with_card?"
+ 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 < ApplicationRecord
- before_save :normalize_card_number,
- if: Proc.new { |order| order.paid_with_card? }
+ before_save :normalize_card_number, if: Proc.new { paid_with_card? }
end
```
@@ -334,6 +337,20 @@ class Comment < ApplicationRecord
end
```
+### Combining Callback Conditions
+
+When multiple conditions define whether or not a callback should happen, an `Array` can be used. Moreover, you can apply both `:if` and `:unless` to the same callback.
+
+```ruby
+class Comment < ApplicationRecord
+ after_create :send_email_to_author,
+ if: [Proc.new { |c| c.user.allow_send_email? }, :author_wants_emails?],
+ unless: Proc.new { |c| c.article.ignore_comments? }
+end
+```
+
+The callback only runs when all the `:if` conditions and none of the `:unless` conditions are evaluated to `true`.
+
Callback Classes
----------------
@@ -399,7 +416,7 @@ By using the `after_commit` callback we can account for this case.
```ruby
class PictureFile < ApplicationRecord
- after_commit :delete_picture_file_from_disk, on: [:destroy]
+ after_commit :delete_picture_file_from_disk, on: :destroy
def delete_picture_file_from_disk
if File.exist?(filepath)
@@ -409,10 +426,10 @@ class PictureFile < ApplicationRecord
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.
-Since using `after_commit` callback only on create, update or delete is
+Since using `after_commit` callback only on create, update, or delete is
common, there are aliases for those operations:
* `after_create_commit`
@@ -431,4 +448,35 @@ class PictureFile < ApplicationRecord
end
```
-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.
+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 cd6b7fdd67..270e4a3bf9 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 bin/rails 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.
@@ -123,10 +123,10 @@ 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:
+This will create an appropriately named empty migration:
```ruby
class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
@@ -135,12 +135,17 @@ class AddPartNumberToProducts < ActiveRecord::Migration[5.0]
end
```
-If the migration name is of the form "AddXXXToYYY" or "RemoveXXXFromYYY" and is
-followed by a list of column names and types then a migration containing the
-appropriate `add_column` and `remove_column` statements will be created.
+This generator can do much more than append a timestamp to the file name.
+Based on naming conventions and additional (optional) arguments it can
+also start fleshing out the migration.
+
+If the migration name is of the form "AddColumnToTable" or
+"RemoveColumnFromTable" and is 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
@@ -156,7 +161,7 @@ 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
@@ -174,7 +179,7 @@ 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
@@ -190,7 +195,7 @@ 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
@@ -209,7 +214,7 @@ 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
@@ -229,11 +234,11 @@ 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
@@ -241,17 +246,18 @@ generates
```ruby
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](https://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:
@@ -275,7 +281,7 @@ 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
@@ -303,7 +309,7 @@ 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
@@ -312,7 +318,7 @@ will produce a migration that looks like this
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,8 +358,14 @@ 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
@@ -435,7 +447,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.
@@ -453,11 +465,13 @@ number of digits after the decimal point.
* `default` Allows to set a default value on the column. Note that if you
are using a dynamic value (such as a date), the default will only be calculated
the first time (i.e. on the date the migration is applied).
-* `index` Adds an index for the column.
+* `comment` Adds a comment for the column.
Some adapters may support additional options; see the adapter specific API docs
for further information.
+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
@@ -469,7 +483,7 @@ add_foreign_key :articles, :authors
This adds a new foreign key to the `author_id` column of the `articles`
table. The key references the `id` column of the `authors` table. If the
-column names can not be derived from the table names, you can use the
+column names cannot be derived from the table names, you can use the
`:column` and `:primary_key` options.
Rails will generate a name for every foreign key starting with
@@ -505,12 +519,12 @@ Product.connection.execute("UPDATE products SET price = 'free' WHERE 1=1")
For more details and examples of individual methods, check the API documentation.
In particular the documentation for
-[`ActiveRecord::ConnectionAdapters::SchemaStatements`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html)
+[`ActiveRecord::ConnectionAdapters::SchemaStatements`](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html)
(which provides the methods available in the `change`, `up` and `down` methods),
-[`ActiveRecord::ConnectionAdapters::TableDefinition`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html)
+[`ActiveRecord::ConnectionAdapters::TableDefinition`](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html)
(which provides the methods available on the object yielded by `create_table`)
and
-[`ActiveRecord::ConnectionAdapters::Table`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html)
+[`ActiveRecord::ConnectionAdapters::Table`](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html)
(which provides the methods available on the object yielded by `change_table`).
### Using the `change` Method
@@ -550,7 +564,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`
@@ -717,15 +731,15 @@ you will have to use `structure.sql` as dump method. See
Running Migrations
------------------
-Rails provides a set of bin/rails 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 bin/rails task you will use will probably be
+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
@@ -734,7 +748,7 @@ is the numerical prefix on the migration's filename. For example, to migrate
to version 20080906120000 run:
```bash
-$ bin/rails db:migrate VERSION=20080906120000
+$ rails db:migrate VERSION=20080906120000
```
If version 20080906120000 is greater than the current version (i.e., it is
@@ -751,7 +765,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/rails db:rollback
+$ rails db:rollback
```
This will rollback the latest migration, either by reverting the `change`
@@ -759,31 +773,31 @@ method or by running the `down` method. If you need to undo
several migrations you can provide a `STEP` parameter:
```bash
-$ bin/rails 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/rails db:migrate:redo STEP=3
+$ rails db:migrate:redo STEP=3
```
-Neither of these bin/rails 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 `rails 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 `rails db:reset` task will drop the database and set it up again. This is
+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
@@ -794,28 +808,28 @@ contents of the current `db/schema.rb` or `db/structure.sql` file. If a migratio
### 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/rails 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 `bin/rails 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/rails db:migrate RAILS_ENV=test
+$ rails db:migrate RAILS_ENV=test
```
### Changing the Output of Running Migrations
@@ -886,7 +900,7 @@ 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
migration again: Rails thinks it has already run the migration and so will do
nothing when you run `rails db:migrate`. You must rollback the migration (for
-example with `bin/rails db:rollback`), edit your migration and then run
+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
@@ -907,38 +921,33 @@ Schema Dumping and You
### What are Schema Files for?
Migrations, mighty as they may be, are not the authoritative source for your
-database schema. That role falls to either `db/schema.rb` or an SQL file which
-Active Record generates by examining the database. They are not designed to be
-edited, they just represent the current state of the database.
+database schema. Your database remains the authoritative source. By default,
+Rails generates `db/schema.rb` which attempts to capture the current state of
+your database schema.
-There is no need (and it is error prone) to deploy a new instance of an app by
-replaying the entire migration history. It is much simpler and faster to just
-load into the database a description of the current schema.
-
-For example, this is how the test database is created: the current development
-database is dumped (either to `db/schema.rb` or `db/structure.sql`) and then
-loaded into the test database.
+It tends to be faster and less error prone to create a new instance of your
+application's database by loading the schema file via `rails db:schema:load`
+than it is to replay the entire migration history.
+[Old migrations](#old-migrations) may fail to apply correctly if those
+migrations use changing external dependencies or rely on application code which
+evolves separately from your migrations.
Schema files are also useful if you want a quick look at what attributes an
Active Record object has. This information is not in the model's code and is
frequently spread across several migrations, but the information is nicely
-summed up in the schema file. The
-[annotate_models](https://github.com/ctran/annotate_models) gem automatically
-adds and updates comments at the top of each model summarizing the schema if
-you desire that functionality.
+summed up in the schema file.
### Types of Schema Dumps
-There are two ways to dump the schema. This is set in `config/application.rb`
-by the `config.active_record.schema_format` setting, which may be either `:sql`
-or `:ruby`.
+The format of the schema dump generated by Rails is controlled by the
+`config.active_record.schema_format` setting in `config/application.rb`. By
+default, the format is `:ruby`, but can also be set to `:sql`.
If `:ruby` is selected, then the schema is stored in `db/schema.rb`. If you look
-at this file you'll find that it looks an awful lot like one very big
-migration:
+at this file you'll find that it looks an awful lot like one very big migration:
```ruby
-ActiveRecord::Schema.define(version: 20080906171750) do
+ActiveRecord::Schema.define(version: 2008_09_06_171750) do
create_table "authors", force: true do |t|
t.string "name"
t.datetime "created_at"
@@ -947,46 +956,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`
-rails task) into `db/structure.sql`. For example, for PostgreSQL, the `pg_dump`
-utility is used. For MySQL, this file will contain the output of
-`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
---------------------------------------
@@ -1009,10 +1014,10 @@ such features, the `execute` method can be used to execute arbitrary SQL.
Migrations and Seed Data
------------------------
-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.
+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[5.0]
@@ -1028,10 +1033,10 @@ class AddInitialProducts < ActiveRecord::Migration[5.0]
end
```
-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
+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
@@ -1042,3 +1047,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 5eb19f5214..12115d01ef 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 9.1
+In order to use the PostgreSQL adapter you need to have at least version 9.3
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
@@ -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
@@ -83,8 +83,8 @@ Book.where("array_length(ratings, 1) >= 3")
### Hstore
-* [type definition](http://www.postgresql.org/docs/current/static/hstore.html)
-* [functions and operators](http://www.postgresql.org/docs/current/static/hstore.html#AEN167712)
+* [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.
@@ -111,19 +111,24 @@ profile.settings = {"color" => "yellow", "resolution" => "1280x1024"}
profile.save!
Profile.where("settings->'color' = ?", "yellow")
-#=> #<ActiveRecord::Relation [#<Profile id: 1, settings: {"color"=>"yellow", "resolution"=>"1280x1024"}>]>
+# => #<ActiveRecord::Relation [#<Profile id: 1, settings: {"color"=>"yellow", "resolution"=>"1280x1024"}>]>
```
-### JSON
+### 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 < ApplicationRecord
@@ -142,10 +147,10 @@ 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.
+This type is mapped to Ruby [`Range`](https://ruby-doc.org/core-2.2.2/Range.html) objects.
```ruby
# db/migrate/20130923065404_create_events.rb
@@ -177,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:
@@ -217,7 +222,7 @@ 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:
@@ -255,7 +260,7 @@ article.status = "published"
article.save!
```
-To add a new value before/after existing one you should use [ALTER TYPE](http://www.postgresql.org/docs/current/static/sql-altertype.html):
+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
@@ -269,9 +274,9 @@ def up
end
```
-NOTE: ENUM values can't be dropped currently. You can read why [here](http://www.postgresql.org/message-id/29F36C7C98AB09499B1A209D48EAA615B7653DBC8A@mail2a.alliedtesting.com).
+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 `bin/rails db` or `psql` console:
+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,
@@ -284,9 +289,9 @@ SELECT n.nspname AS enum_schema,
### 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`
extension to use uuid.
@@ -335,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
@@ -344,7 +349,7 @@ create_table :users, force: true do |t|
t.column :settings, "bit(8)"
end
-# app/models/device.rb
+# app/models/user.rb
class User < ApplicationRecord
end
@@ -359,10 +364,10 @@ 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)
+[`IPAddr`](https://ruby-doc.org/stdlib-2.2.2/libdoc/ipaddr/rdoc/IPAddr.html)
objects. The `macaddr` type is mapped to normal text.
```ruby
@@ -394,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.
@@ -422,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
@@ -435,7 +440,7 @@ 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 < ApplicationRecord
@@ -452,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:
@@ -503,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 9d349691b4..e40f16e62d 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
=============================
@@ -50,7 +50,7 @@ class Role < ApplicationRecord
end
```
-Active Record will perform queries on the database for you and is compatible with most database systems (MySQL, PostgreSQL and SQLite to name a few). Regardless of which database system you're using, the Active Record method format will always be the same.
+Active Record will perform queries on the database for you and is compatible with most database systems, including MySQL, MariaDB, PostgreSQL, and SQLite. Regardless of which database system you're using, the Active Record method format will always be the same.
Retrieving Objects from the Database
------------------------------------
@@ -59,11 +59,13 @@ To retrieve objects from the database, Active Record provides several finder met
The methods are:
+* `annotate`
* `find`
* `create_with`
* `distinct`
* `eager_load`
* `extending`
+* `extract_associated`
* `from`
* `group`
* `having`
@@ -74,17 +76,18 @@ The methods are:
* `lock`
* `none`
* `offset`
+* `optimizer_hints`
* `order`
* `preload`
* `readonly`
* `references`
* `reorder`
+* `reselect`
* `reverse_order`
* `select`
-* `distinct`
* `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:
@@ -119,7 +122,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">]
```
@@ -151,7 +154,7 @@ 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">
@@ -190,7 +193,7 @@ If your [default scope](active_record_querying.html#applying-a-default-scope) co
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">,
@@ -204,7 +207,7 @@ 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`.
+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
@@ -241,7 +244,7 @@ If your [default scope](active_record_querying.html#applying-a-default-scope) co
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">,
@@ -255,7 +258,7 @@ 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`.
+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
@@ -314,7 +317,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
@@ -328,7 +331,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|
@@ -336,7 +339,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|
@@ -344,11 +349,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`, `:start` and `:finish`, are available as well.
+##### Options for `find_each`
**`:batch_size`**
@@ -362,12 +372,12 @@ end
**`:start`**
-By default, records are fetched in ascending order of the primary key, which must be an integer. 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.
+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(start: 2000, batch_size: 5000) do |user|
+User.find_each(start: 2000) do |user|
NewsMailer.weekly(user).deliver_now
end
```
@@ -375,12 +385,12 @@ end
**`:finish`**
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`
+This would be useful, for example, if you wanted to run a batch process using a subset of records based on `:start` and `:finish`.
-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(start: 2000, finish: 10000, batch_size: 5000) do |user|
+User.find_each(start: 2000, finish: 10000) do |user|
NewsMailer.weekly(user).deliver_now
end
```
@@ -389,20 +399,36 @@ 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`, `:start` and `:finish` options as `find_each`.
+The `find_in_batches` method accepts the same options as `find_each`.
Conditions
----------
@@ -464,7 +490,7 @@ This makes for clearer readability if you have a large number of variable condit
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
@@ -472,6 +498,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
@@ -485,8 +517,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
@@ -517,13 +547,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
-Article.where.not(author: author)
+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.
+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
+Client.where(locked: true).or(Client.where(orders_count: [1,3,5]))
+```
+
+```sql
+SELECT * FROM clients WHERE (clients.locked = 1 OR clients.orders_count IN (1,3,5))
+```
Ordering
--------
@@ -569,6 +616,8 @@ Client.order("orders_count ASC").order("created_at DESC")
# SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC
```
+WARNING: In most database systems, on selecting fields with `distinct` 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
-------------------------
@@ -579,6 +628,8 @@ To select only a subset of fields from the result set, you can specify the subse
For example, to select only `viewable_by` and `locked` columns:
```ruby
+Client.select(:viewable_by, :locked)
+# OR
Client.select("viewable_by, locked")
```
@@ -757,8 +808,34 @@ 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
+
+```
+
+### `reselect`
+The `reselect` method overrides an existing select statement. For example:
+
+```ruby
+Post.select(:title, :body).reselect(:created_at)
+```
+
+The SQL that would be executed:
+
+```sql
+SELECT `posts`.`created_at` FROM `posts`
+```
+
+In case the `reselect` clause is not used,
+
+```ruby
+Post.select(:title, :body).select(:created_at)
+```
+
+the SQL executed would be:
+
+```sql
+SELECT `posts`.`title`, `posts`.`body`, `posts`.`created_at` FROM `posts`
```
### `reorder`
@@ -776,14 +853,14 @@ Article.find(10).comments.reorder('name')
The SQL that would be executed:
```sql
-SELECT * FROM articles WHERE id = 10
+SELECT * FROM articles WHERE id = 10 LIMIT 1
SELECT * FROM comments WHERE article_id = 10 ORDER BY name
```
In the case where the `reorder` clause is not used, the SQL executed would be:
```sql
-SELECT * FROM articles WHERE id = 10
+SELECT * FROM articles WHERE id = 10 LIMIT 1
SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC
```
@@ -980,13 +1057,13 @@ There are multiple ways to use the `joins` method.
You can just supply the raw SQL specifying the `JOIN` clause to `joins`:
```ruby
-Author.joins("INNER JOIN posts ON posts.author_id = author.id AND posts.published = 't'")
+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 INNER JOIN posts ON posts.author_id = author.id AND posts.published = 't'
+SELECT authors.* FROM authors INNER JOIN posts ON posts.author_id = authors.id AND posts.published = 't'
```
#### Using Array/Hash of Named Associations
@@ -1047,7 +1124,7 @@ This produces:
```sql
SELECT articles.* FROM articles
- INNER JOIN categories ON articles.category_id = categories.id
+ INNER JOIN categories ON categories.id = articles.category_id
INNER JOIN comments ON comments.article_id = articles.id
```
@@ -1111,7 +1188,7 @@ 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).uniq.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id')
+Author.left_outer_joins(:posts).distinct.select('authors.*, COUNT(posts.*) AS posts_count').group('authors.id')
```
Which produces:
@@ -1217,12 +1294,13 @@ 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
------
-Scoping allows you to specify commonly-used queries which can be referenced as method calls on the association objects or models. With these scopes, you can use every method previously covered such as `where`, `joins` and `includes`. All scope methods will return an `ActiveRecord::Relation` object which will allow for further methods (such as other scopes) to be called on it.
+Scoping allows you to specify commonly-used queries which can be referenced as method calls on the association objects or models. With these scopes, you can use every method previously covered such as `where`, `joins` and `includes`. All scope bodies should return an `ActiveRecord::Relation` or `nil` to allow for further methods (such as other scopes) to be called on it.
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:
@@ -1232,16 +1310,6 @@ class Article < ApplicationRecord
end
```
-This is exactly the same as defining a class method, and which you use is a matter of personal preference:
-
-```ruby
-class Article < ApplicationRecord
- def self.published
- where(published: true)
- end
-end
-```
-
Scopes are also chainable within scopes:
```ruby
@@ -1347,8 +1415,9 @@ class Client < ApplicationRecord
end
```
-NOTE: The `default_scope` is also applied while creating/building a record.
-It is not applied while updating a record. E.g.:
+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
@@ -1359,6 +1428,17 @@ 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.
@@ -1477,12 +1557,12 @@ book.available? # => false
```
Read the full documentation about enums
-[in the Rails API docs](http://api.rubyonrails.org/classes/ActiveRecord/Enum.html).
+[in the Rails API docs](https://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
@@ -1510,7 +1590,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
@@ -1655,10 +1735,13 @@ Client.find_by_sql("SELECT * FROM clients
### `select_all`
-`find_by_sql` has a close relative called `connection#select_all`. `select_all` will retrieve objects from the database using custom SQL just like `find_by_sql` but will not instantiate them. Instead, you will get an array of hashes where each hash indicates a record.
+`find_by_sql` has a close relative called `connection#select_all`. `select_all` will retrieve
+objects from the database using custom SQL just like `find_by_sql` but will not instantiate them.
+This method will return an instance of `ActiveRecord::Result` class and calling `to_a` 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_a
# => [
# {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
# {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
@@ -1720,6 +1803,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:
@@ -1814,14 +1903,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:
@@ -1833,9 +1922,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
@@ -1915,7 +2004,7 @@ EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`
2 rows in set (0.00 sec)
```
-under MySQL.
+under MySQL and MariaDB.
Active Record performs a pretty printing that emulates that of the
corresponding database shell. So, the same query running with the
@@ -1975,15 +2064,17 @@ 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
Interpretation of the output of EXPLAIN is beyond the scope of this guide. The
following pointers may be helpful:
-* SQLite3: [EXPLAIN QUERY PLAN](http://www.sqlite.org/eqp.html)
+* SQLite3: [EXPLAIN QUERY PLAN](https://www.sqlite.org/eqp.html)
+
+* MySQL: [EXPLAIN Output Format](https://dev.mysql.com/doc/refman/5.7/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 baaebd21c8..e68f34dd5d 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
=========================
@@ -87,7 +87,7 @@ 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?
@@ -278,12 +278,6 @@ 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. If
-the field does exist in your database, the `accept` option must be set to
-`true` or else the validation will not run.
-
```ruby
class Person < ApplicationRecord
validates :terms_of_service, acceptance: true
@@ -292,16 +286,31 @@ 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 < 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
@@ -383,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"_.
@@ -418,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"_.
@@ -479,9 +490,6 @@ If you set `:only_integer` to `true`, then it will use the
regular expression to validate the attribute's value. Otherwise, it will try to
convert the value to a number using `Float`.
-WARNING. Note that the regular expression above allows a trailing newline
-character.
-
```ruby
class Player < ApplicationRecord
validates :points, numericality: true
@@ -530,7 +538,8 @@ 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 < ApplicationRecord
@@ -630,7 +639,7 @@ class Holiday < ApplicationRecord
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.7/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](https://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
@@ -737,7 +746,7 @@ class Person < ApplicationRecord
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.
@@ -759,6 +768,9 @@ class Coffee < ApplicationRecord
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
@@ -783,9 +795,10 @@ 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.
+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: a message key for i18n, and
+A `Proc` `:message` value is given two arguments: the object being validated, and
a hash with `:model`, `:attribute`, and `:value` key-value pairs.
```ruby
@@ -801,10 +814,10 @@ class Person < ApplicationRecord
# Proc
validates :username,
uniqueness: {
- # key = "activerecord.errors.models.person.attributes.username.taken"
+ # object = person object being validated
# data = { model: "Person", attribute: "Username", value: <username> }
- message: ->(key, data) do
- "#{data[:value]} taken! Try again #{Time.zone.tomorrow}"
+ message: ->(object, data) do
+ "Hey #{object.name}!, #{data[:value]} is taken already! Try again #{Time.zone.tomorrow}"
end
}
end
@@ -832,9 +845,9 @@ class Person < ApplicationRecord
end
```
-You can also use `on:` to define custom context.
-Custom contexts need to be triggered explicitly
-by passing name of the context to `valid?`, `invalid?` or `save`.
+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
@@ -842,14 +855,32 @@ class Person < ApplicationRecord
validates :age, numericality: true, on: :account_setup
end
-person = Person.new
+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. And `person.save(context: :account_setup)`
-validates `person` in `account_setup` context before saving.
-On explicit triggers, model is validated by
-validations of only that context and validations without context.
+`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
------------------
@@ -880,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.
@@ -901,21 +932,9 @@ class Order < ApplicationRecord
end
```
-### Using a String with `:if` and `:unless`
-
-You can also use a string that will be evaluated using `eval` and needs to
-contain valid Ruby code. You should use this option only when the string
-represents a really short condition.
-
-```ruby
-class Person < ApplicationRecord
- validates :surname, presence: true, if: "name.nil?"
-end
-```
-
### Using a Proc with `:if` and `:unless`
-Finally, it's possible to associate `:if` and `:unless` with a `Proc` object
+It is possible to associate `:if` and `:unless` with a `Proc` object
which will be called. Using a `Proc` object gives you the ability to write an
inline condition instead of a separate method. This option is best suited for
one-liners.
@@ -927,6 +946,13 @@ class Account < ApplicationRecord
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
@@ -953,7 +979,7 @@ should happen, an `Array` can be used. Moreover, you can apply both `:if` and
```ruby
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
```
@@ -1018,7 +1044,7 @@ own custom validators.
You can also create methods that verify the state of your models and add
messages to the `errors` collection when they are invalid. You must then
register these methods by using the `validate`
-([API](http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validate))
+([API](https://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validate))
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
@@ -1133,24 +1159,6 @@ 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 < ApplicationRecord
- 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
@@ -1217,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)"]
```
diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md
new file mode 100644
index 0000000000..615f576797
--- /dev/null
+++ b/guides/source/active_storage_overview.md
@@ -0,0 +1,770 @@
+**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](https://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_limit: [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
+ require("@rails/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 "@rails/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 "@rails/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 RemoveUploadedFiles
+ def after_teardown
+ super
+ remove_uploaded_files
+ end
+
+ private
+
+ def remove_uploaded_files
+ FileUtils.rm_rf(Rails.root.join('tmp', 'storage'))
+ end
+end
+
+module ActionDispatch
+ class IntegrationTest
+ prepend RemoveUploadedFiles
+ 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 d7f9201b26..1a057832d4 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,37 +135,35 @@ 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`.
@@ -252,7 +250,7 @@ Note that `try` will swallow no-method errors, returning nil instead. If you wan
```ruby
@number.try(:nest) # => nil
-@number.try!(:nest) # NoMethodError: undefined method `nest' for 1:Fixnum
+@number.try!(:nest) # NoMethodError: undefined method `nest' for 1:Integer
```
NOTE: Defined in `active_support/core_ext/object/try.rb`.
@@ -368,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')
@@ -511,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 `ActionDispatch::IntegrationTest#process` this way in `test/test_helper.rb`:
-
-```ruby
-ActionDispatch::IntegrationTest.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('GET', path, params: nil, headers: nil, env: nil, xhr: false)
- params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten]
- original_process('GET', path, params: params)
- 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
-ActionDispatch::IntegrationTest.class_eval do
- def process_with_stringified_params(...)
- params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten]
- process_without_stringified_params(method, path, params: params)
- 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
-ActionDispatch::IntegrationTest.class_eval do
- def process_with_stringified_params(...)
- params = Hash[*params.map {|k, v| [k, v.to_s]}.flatten]
- process_without_stringified_params(method, path, params: params)
- end
- alias_method_chain :process, :stringified_params
-end
-```
-
-NOTE: Defined in `active_support/core_ext/module/aliasing.rb`.
-
### Attributes
#### `alias_attribute`
@@ -642,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
@@ -655,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
@@ -678,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
@@ -701,108 +649,12 @@ module X
end
M = X::Y::Z
-X::Y::Z.parents # => [X::Y, X, Object]
-M.parents # => [X::Y, X, Object]
+X::Y::Z.module_parents # => [X::Y, X, Object]
+M.module_parents # => [X::Y, X, Object]
```
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:
@@ -836,7 +688,6 @@ end
m = Object.send(:remove_const, :M)
-m.reachable? # => false
m.anonymous? # => false
```
@@ -846,6 +697,8 @@ 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:
@@ -926,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`
---------------------
@@ -997,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
```
@@ -1012,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?`.
@@ -1021,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`
@@ -1031,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
```
@@ -1040,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
@@ -1319,6 +1201,8 @@ The `inquiry` method converts a string into a `StringInquirer` object making equ
"active".inquiry.inactive? # => false
```
+NOTE: Defined in `active_support/core_ext/string/inquiry.rb`.
+
### `starts_with?` and `ends_with?`
Active Support defines 3rd person aliases of `String#start_with?` and `String#end_with?`:
@@ -1457,7 +1341,7 @@ The method `pluralize` returns the plural of its receiver:
"equipment".pluralize # => "equipment"
```
-As the previous example shows, Active Support knows some irregular plurals and uncountable nouns. Built-in rules can be extended in `config/initializers/inflections.rb`. That file is generated by the `rails` command and has instructions in comments.
+As the previous example shows, Active Support knows some irregular plurals and uncountable nouns. Built-in rules can be extended in `config/initializers/inflections.rb`. This file is generated by default, by the `rails new` command and has instructions in comments.
`pluralize` can also take an optional `count` parameter. If `count == 1` the singular form will be returned. For any other value of `count` the plural form will be returned:
@@ -1661,19 +1545,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`
@@ -1742,7 +1613,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
@@ -1794,7 +1665,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"
@@ -1872,7 +1743,7 @@ The methods `to_date`, `to_time`, and `to_datetime` are basically convenience wr
"2010-07-27 23:42:00".to_time(:local) # => 2010-07-27 23:42:00 +0200
```
-Default is `:utc`.
+Default is `:local`.
Please refer to the documentation of `Date._parse` for further details.
@@ -1916,7 +1787,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:
@@ -1925,14 +1796,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
@@ -2067,6 +1940,28 @@ The method `ordinalize` returns the ordinal string corresponding to the receiver
NOTE: Defined in `active_support/core_ext/integer/inflections.rb`.
+### Time
+
+Enables the use of time calculations and declarations, like `4.months + 5.years`.
+
+These methods use Time#advance for precise date calculations when using from_now, ago, etc.
+as well as adding or subtracting their results from a Time object. For example:
+
+```ruby
+# equivalent to Time.current.advance(months: 1)
+1.month.from_now
+
+# equivalent to Time.current.advance(years: 2)
+2.years.from_now
+
+# equivalent to Time.current.advance(months: 4, years: 5)
+(4.months + 5.years).from_now
+```
+
+WARNING. For other durations please refer to the time extensions to `Numeric`.
+
+NOTE: Defined in `active_support/core_ext/integer/time.rb`.
+
Extensions to `BigDecimal`
--------------------------
### `to_s`
@@ -2074,19 +1969,19 @@ Extensions to `BigDecimal`
The method `to_s` provides a default specifier of "F". This means that a simple call to `to_s` will result in floating point representation instead of engineering notation:
```ruby
-BigDecimal.new(5.00, 6).to_s # => "5.0"
+BigDecimal(5.00, 6).to_s # => "5.0"
```
and that symbol specifiers are also supported:
```ruby
-BigDecimal.new(5.00, 6).to_s(:db) # => "5.0"
+BigDecimal(5.00, 6).to_s(:db) # => "5.0"
```
Engineering notation is still supported:
```ruby
-BigDecimal.new(5.00, 6).to_s("e") # => "0.5E1"
+BigDecimal(5.00, 6).to_s("e") # => "0.5E1"
```
Extensions to `Enumerable`
@@ -2106,7 +2001,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 # => [:a, 1, :b, 2, :c, 3]
```
The sum of an empty collection is zero by default, but this is customizable:
@@ -2146,6 +2041,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`:
@@ -2224,29 +2134,18 @@ The methods `second`, `third`, `fourth`, and `fifth` return the corresponding el
NOTE: Defined in `active_support/core_ext/array/access.rb`.
-### Adding Elements
-
-#### `prepend`
-
-This method is an alias of `Array#unshift`.
-
-```ruby
-%w(a b c d).prepend('e') # => ["e", "a", "b", "c", "d"]
-[].prepend(10) # => [10]
-```
-
-NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`.
-
-#### `append`
+### Extracting
-This method is an alias of `Array#<<`.
+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
-%w(a b c d).append('e') # => ["a", "b", "c", "d", "e"]
-[].append([1,2]) # => [[1, 2]]
+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/prepend_and_append.rb`.
+NOTE: Defined in `active_support/core_ext/array/extract.rb`.
### Options Extraction
@@ -2349,7 +2248,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":
@@ -2444,7 +2343,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:
@@ -2611,8 +2510,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",
@@ -2726,55 +2624,13 @@ There's also the bang variant `except!` that removes keys in the very receiver.
NOTE: Defined in `active_support/core_ext/hash/except.rb`.
-#### `transform_keys` and `transform_keys!`
-
-The method `transform_keys` accepts a block and returns a hash that has applied the block operations to each of the keys in the receiver:
-
-```ruby
-{nil => nil, 1 => 1, a: :a}.transform_keys { |key| key.to_s.upcase }
-# => {"" => nil, "A" => :a, "1" => 1}
-```
-
-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:
-
-```ruby
-{"a" => 1, a: 2}.transform_keys { |key| key.to_s.upcase }
-# The result could either be
-# => {"A"=>2}
-# or
-# => {"A"=>1}
-```
-
-This method may be useful for example to build specialized conversions. For instance `stringify_keys` and `symbolize_keys` use `transform_keys` to perform their key conversions:
-
-```ruby
-def stringify_keys
- transform_keys { |key| key.to_s }
-end
-...
-def symbolize_keys
- transform_keys { |key| key.to_sym rescue key }
-end
-```
-
-There's also the bang variant `transform_keys!` that applies the block operations to keys in the very receiver.
-
-Besides that, one can use `deep_transform_keys` and `deep_transform_keys!` to perform the block operation on all the keys in the given hash and all the hashes nested into it. An example of the result is:
-
-```ruby
-{nil => nil, 1 => 1, nested: {a: 3, 5 => 5}}.deep_transform_keys { |key| key.to_s.upcase }
-# => {""=>nil, "1"=>1, "NESTED"=>{"A"=>3, "5"=>5}}
-```
-
-NOTE: Defined in `active_support/core_ext/hash/keys.rb`.
-
#### `stringify_keys` and `stringify_keys!`
The method `stringify_keys` returns a hash that has a stringified version of the keys in the receiver. It does so by sending `to_s` to them:
```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:
@@ -2816,7 +2672,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.
@@ -2873,42 +2729,9 @@ 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, b: 2, c: 3}.slice(:b, :X)
-# => {:b=>2} # non-existing keys are ignored
-```
-
-If the receiver responds to `convert_key` keys are normalized:
-
-```ruby
-{a: 1, b: 2}.with_indifferent_access.slice("a")
-# => {:a=>1}
-```
-
-NOTE. Slicing may come in handy for sanitizing option hashes with a white list of keys.
-
-There's also `slice!` which in addition to perform a slice in place returns what's removed:
+The method `slice!` replaces the hash with only the given keys and returns a hash containing the removed key/value pairs.
```ruby
hash = {a: 1, b: 2}
@@ -2948,16 +2771,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`
----------------------
@@ -2976,10 +2789,10 @@ Regexp.new('.', Regexp::MULTILINE).multiline? # => true
Rails uses this method in a single place, also in the routing code. Multiline regexps are disallowed for route requirements and this flag eases enforcing that constraint.
```ruby
-def assign_route_options(segments, defaults, requirements)
+def verify_regexp_requirements(requirements)
...
if requirement.multiline?
- raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
+ raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
end
...
end
@@ -3006,9 +2819,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
@@ -3017,18 +2830,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?`
@@ -3047,8 +2865,6 @@ 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`
@@ -3057,69 +2873,9 @@ Active Support defines `Date.current` to be today in the current time zone. That
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`
+NOTE: Defined in `active_support/core_ext/date/calculations.rb`.
-In Ruby 1.9 `prev_year` and `next_year` return a date with the same day/month in the last or next year:
-
-```ruby
-d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
-d.prev_year # => Fri, 08 May 2009
-d.next_year # => Sun, 08 May 2011
-```
-
-If date is the 29th of February of a leap year, you obtain the 28th:
-
-```ruby
-d = Date.new(2000, 2, 29) # => Tue, 29 Feb 2000
-d.prev_year # => Sun, 28 Feb 1999
-d.next_year # => Wed, 28 Feb 2001
-```
-
-`prev_year` is aliased to `last_year`.
-
-##### `prev_month`, `next_month`
-
-In Ruby 1.9 `prev_month` and `next_month` return the date with the same day in the last or next month:
-
-```ruby
-d = Date.new(2010, 5, 8) # => Sat, 08 May 2010
-d.prev_month # => Thu, 08 Apr 2010
-d.next_month # => Tue, 08 Jun 2010
-```
-
-If such a day does not exist, the last day of the corresponding month is returned:
-
-```ruby
-Date.new(2000, 5, 31).prev_month # => Sun, 30 Apr 2000
-Date.new(2000, 3, 31).prev_month # => Tue, 29 Feb 2000
-Date.new(2000, 5, 31).next_month # => Fri, 30 Jun 2000
-Date.new(2000, 1, 31).next_month # => Tue, 29 Feb 2000
-```
-
-`prev_month` is aliased to `last_month`.
-
-##### `prev_quarter`, `next_quarter`
-
-Same as `prev_month` and `next_month`. It returns the date with the same day in the previous or next quarter:
-
-```ruby
-t = Time.local(2010, 5, 8) # => Sat, 08 May 2010
-t.prev_quarter # => Mon, 08 Feb 2010
-t.next_quarter # => Sun, 08 Aug 2010
-```
-
-If such a day does not exist, the last day of the corresponding month is returned:
-
-```ruby
-Time.local(2000, 7, 31).prev_quarter # => Sun, 30 Apr 2000
-Time.local(2000, 5, 31).prev_quarter # => Tue, 29 Feb 2000
-Time.local(2000, 10, 31).prev_quarter # => Mon, 30 Oct 2000
-Time.local(2000, 11, 31).next_quarter # => Wed, 28 Feb 2001
-```
-
-`prev_quarter` is aliased to `last_quarter`.
+#### Named dates
##### `beginning_of_week`, `end_of_week`
@@ -3138,6 +2894,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
@@ -3155,6 +2913,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.
@@ -3177,6 +2937,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:
@@ -3189,6 +2951,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:
@@ -3201,6 +2965,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:
@@ -3213,6 +2979,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`
@@ -3238,6 +3006,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:
@@ -3254,6 +3026,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:
@@ -3263,6 +3039,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:
@@ -3291,6 +3069,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:
@@ -3307,6 +3087,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:
@@ -3349,6 +3131,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):
@@ -3367,6 +3151,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):
@@ -3387,6 +3173,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:
@@ -3403,6 +3191,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
@@ -3414,37 +3204,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
-on_weekday?
-on_weekend?
-```
+The class `DateTime` is a subclass of `Date` so by loading `active_support/core_ext/date/calculations.rb` you inherit these methods and their aliases, except that they will always return datetimes.
The following methods are reimplemented so you do **not** need to load `active_support/core_ext/date/calculations.rb` for these ones:
@@ -3470,6 +3230,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`
@@ -3481,6 +3243,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.
@@ -3492,6 +3256,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:
@@ -3502,6 +3268,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.
@@ -3533,6 +3301,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`:
@@ -3565,6 +3335,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:
@@ -3590,50 +3362,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
-on_weekday?
-on_weekend?
-```
-
They are analogous. Please refer to their documentation above and take into account the following differences:
* `change` accepts an additional `:usec` option.
@@ -3658,6 +3386,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.
@@ -3686,6 +3416,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`:
@@ -3709,7 +3517,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
```
@@ -3772,9 +3580,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 03af3cf819..e5ed283c45 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.
@@ -169,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
@@ -197,6 +197,21 @@ INFO. Additional keys may be added by the caller.
}
```
+### unpermitted_parameters.action_controller
+
+| Key | Value |
+| ------- | ---------------- |
+| `:keys` | Unpermitted keys |
+
+Action Dispatch
+---------------
+
+### process_middleware.action_dispatch
+
+| Key | Value |
+| ------------- | ---------------------- |
+| `:middleware` | Name of the middleware |
+
Action View
-----------
@@ -226,17 +241,39 @@ 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` |
-| `:binds` | Bind parameters |
+| Key | Value |
+| -------------------- | ---------------------------------------- |
+| `:sql` | SQL statement |
+| `:name` | Name of the operation |
+| `:connection_id` | Object ID of the connection object |
+| `:connection` | Connection object |
+| `:binds` | Bind parameters |
+| `:type_casted_binds` | Typecasted bind parameters |
+| `:statement_name` | SQL Statement name |
+| `:cached` | `true` is added when cached queries used |
INFO. The adapters will add their own data as well.
@@ -245,7 +282,10 @@ INFO. The adapters will add their own data as well.
sql: "SELECT \"posts\".* FROM \"posts\" ",
name: "Post Load",
connection_id: 70307250813140,
- binds: []
+ connection: #<ActiveRecord::ConnectionAdapters::SQLite3Adapter:0x00007f9f7a838850>,
+ binds: [#<ActiveModel::Attribute::WithCastValue:0x00007fe19d15dc00>],
+ type_casted_binds: [11],
+ statement_name: nil
}
```
@@ -266,55 +306,47 @@ INFO. The adapters will add their own data as well.
Action Mailer
-------------
-### receive.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
}
```
-### deliver.action_mailer
+### process.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 |
+| `:action` | The action |
+| `:args` | The arguments |
```ruby
{
mailer: "Notification",
- message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
- subject: "Rails Guides",
- to: ["users@rails.com", "ddh@rails.com"],
- from: ["me@rails.com"],
- date: Sat, 10 Mar 2012 14:18:09 +0100,
- mail: "..." # omitted for brevity
+ action: "welcome_email",
+ args: []
}
```
@@ -401,7 +433,7 @@ INFO. Cache stores may add their own keys
```
Active Job
---------
+----------
### enqueue_at.active_job
@@ -417,6 +449,15 @@ Active Job
| `: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 |
@@ -431,6 +472,115 @@ Active Job
| `: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
--------
@@ -462,7 +612,7 @@ The block receives the following arguments:
* The name of the event
* Time when it started
* Time when it finished
-* A unique ID for this event
+* A unique ID for the instrumenter that fired the event
* The payload (described in previous sections)
```ruby
@@ -487,6 +637,18 @@ ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*a
end
```
+You may also pass block with only one argument, it will yield an event object to the block:
+
+```ruby
+ActiveSupport::Notifications.subscribe "process_action.action_controller" do |event|
+ event.name # => "process_action.action_controller"
+ event.duration # => 10 (in milliseconds)
+ event.payload # => {:extra=>information}
+
+ Rails.logger.info "#{event} Received!"
+end
+```
+
Most times you only care about the data itself. Here is a shortcut to just get the data.
```ruby
@@ -511,7 +673,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:
@@ -529,5 +692,16 @@ ActiveSupport::Notifications.subscribe "my.custom.event" do |name, started, fini
end
```
+You also have the option to call instrument without passing a block. This lets you leverage the
+instrumentation infrastructure for other messaging uses.
+
+```ruby
+ActiveSupport::Notifications.instrument "my.custom.event", this: :data
+
+ActiveSupport::Notifications.subscribe "my.custom.event" do |name, started, finished, unique_id, data|
+ puts data.inspect # {:this=>:data}
+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 8dba914923..b8b6cb7874 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
=====================================
@@ -18,14 +17,14 @@ 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 HTML that communicates with the server
@@ -66,9 +65,9 @@ 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
@@ -77,10 +76,8 @@ Handled at the middleware layer:
- 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)
+ [`stale?`](https://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.
@@ -96,12 +93,12 @@ 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
for three kinds of HTTP authentication.
@@ -109,7 +106,7 @@ Handled at the Action Pack layer:
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: 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
@@ -151,7 +148,7 @@ This will do three main things for you:
`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
@@ -181,7 +178,7 @@ To render debugging information preserving the response format, use the value `:
config.debug_exception_response_format = :api
```
-By default, `config.debug_exception_response_format` is set to `: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:
@@ -204,16 +201,17 @@ An API application comes with the following middleware by default:
- `Rack::Sendfile`
- `ActionDispatch::Static`
-- `ActionDispatch::LoadInterlock`
+- `ActionDispatch::Executor`
- `ActiveSupport::Cache::Strategy::LocalCache::Middleware`
- `Rack::Runtime`
- `ActionDispatch::RequestId`
+- `ActionDispatch::RemoteIp`
- `Rails::Rack::Logger`
- `ActionDispatch::ShowExceptions`
- `ActionDispatch::DebugExceptions`
-- `ActionDispatch::RemoteIp`
- `ActionDispatch::Reloader`
- `ActionDispatch::Callbacks`
+- `ActiveRecord::Migration::CheckPending`
- `Rack::Head`
- `Rack::ConditionalGet`
- `Rack::ETag`
@@ -289,9 +287,9 @@ environment's configuration file.
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).
+documentation](https://www.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
@@ -341,7 +339,7 @@ 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`
@@ -361,7 +359,7 @@ middleware set, you can remove it with:
config.middleware.delete ::Rack::Sendfile
```
-Keep in mind that removing these middleware will remove support for certain
+Keep in mind that removing these middlewares will remove support for certain
features in Action Controller.
Choosing Controller Modules
@@ -375,11 +373,8 @@ controller modules by default:
- `AbstractController::Rendering` and `ActionController::ApiRendering`: Basic support for rendering.
- `ActionController::Renderers::All`: Support for `render :json` and friends.
- `ActionController::ConditionalGet`: Support for `stale?`.
-- `ActionController::BasicImplicitRender`: Makes sure to return an empty response
- if there's not an explicit one.
-- `ActionController::StrongParameters`: Support for parameters white-listing in
- combination with Active Model mass assignment.
-- `ActionController::ForceSSL`: Support for `force_ssl`.
+- `ActionController::BasicImplicitRender`: Makes sure to return an empty response, if there isn't an explicit one.
+- `ActionController::StrongParameters`: Support for parameters filtering in combination with Active Model mass assignment.
- `ActionController::DataStreaming`: Support for `send_file` and `send_data`.
- `AbstractController::Callbacks`: Support for `before_action` and
similar helpers.
@@ -388,15 +383,23 @@ controller modules by default:
hooks defined by Action Controller (see [the instrumentation
guide](active_support_instrumentation.html#action-controller) for
more information regarding this).
-- `ActionController::ParamsWrapper`: Wraps the parameters hash into a nested hash
- so you don't have to specify root elements sending POST requests for instance.
+- `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
@@ -409,9 +412,11 @@ 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.
diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md
index cd208c2e13..a7ffa4fbd4 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
============================
@@ -15,20 +15,20 @@ After reading this guide, you will know:
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
+The [Rails API documentation](https://api.rubyonrails.org) is generated with
+[RDoc](https://ruby.github.io/rdoc/). To generate it, make sure you are
in the rails root directory, run `bundle install` and execute:
```bash
- ./bin/rails rdoc
+ bundle exec rake rdoc
```
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,12 +82,12 @@ 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](http://en.wikipedia.org/wiki/Serial_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
@@ -120,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
@@ -281,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
@@ -333,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
@@ -354,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 cc3da47db9..454613e733 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,10 +20,9 @@ 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. For example, jquery-rails includes a copy of jquery.js
-and enables AJAX features in Rails.
+from other gems.
The asset pipeline is implemented by the
[sprockets-rails](https://github.com/rails/sprockets-rails) gem,
@@ -35,7 +34,7 @@ rails new appname --skip-sprockets
```
Rails automatically adds the `sass-rails`, `coffee-rails` and `uglifier`
-gems to your Gemfile, which are used by Sprockets for asset compression:
+gems to your `Gemfile`, which are used by Sprockets for asset compression:
```ruby
gem 'sass-rails'
@@ -44,8 +43,8 @@ gem 'coffee-rails'
```
Using the `--skip-sprockets` option will prevent Rails from adding
-them to your Gemfile, so if you later want to enable
-the asset pipeline you will have to add those gems to your Gemfile. Also,
+them to your `Gemfile`, so if you later want to enable
+the asset pipeline you will have to add those gems to your `Gemfile`. Also,
creating an application with the `--skip-sprockets` option will generate
a slightly different `config/application.rb` file, with a require statement
for the sprockets railtie that is commented-out. You will have to remove
@@ -65,7 +64,7 @@ config.assets.js_compressor = :uglifier
```
NOTE: The `sass-rails` gem is automatically used for CSS compression if included
-in the Gemfile and no `config.assets.css_compressor` option is set.
+in the `Gemfile` and no `config.assets.css_compressor` option is set.
### Main Features
@@ -78,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,
@@ -106,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`
```
@@ -127,7 +126,7 @@ The query string strategy has several disadvantages:
1. **Not all caches will reliably cache content where the filename only differs by
query parameters**
- [Steve Souders recommends](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/),
+ [Steve Souders recommends](https://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/),
"...avoiding a querystring for cacheable resources". He found that in this
case 5-20% of requests will not be cached. Query strings in particular do not
work at all with some CDNs for cache invalidation.
@@ -154,7 +153,7 @@ environments. You can enable or disable it in your configuration through the
More reading:
-* [Optimize caching](http://code.google.com/speed/page-speed/docs/caching.html)
+* [Optimize caching](https://developers.google.com/speed/docs/insights/LeverageBrowserCaching)
* [Revving Filenames: don't use querystring](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/)
@@ -181,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.
@@ -202,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
@@ -225,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.
@@ -235,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
@@ -283,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,
@@ -335,7 +328,7 @@ an asset has been updated and if so loads it into the page:
<%= javascript_include_tag "application", "data-turbolinks-track" => "reload" %>
```
-In regular views you can access images in the `public/assets/images` directory
+In regular views you can access images in the `app/assets/images` directory
like this:
```erb
@@ -346,9 +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`,
@@ -383,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.
@@ -435,11 +428,11 @@ 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 application includes a default
@@ -447,15 +440,15 @@ For example, a new Rails application includes a default
```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.
@@ -485,7 +478,7 @@ which contains these lines:
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
@@ -496,7 +489,7 @@ one, requiring all stylesheets from the current directory.
In this example, `require_self` is used. This puts the CSS contained within the
file (if any) at the precise location of the `require_self` call.
-NOTE. If you want to use multiple Sass files, you should generally use the [Sass `@import` rule](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import)
+NOTE. If you want to use multiple Sass files, you should generally use the [Sass `@import` rule](https://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import)
instead of these Sprockets directives. When using Sprockets directives, Sass files exist within
their own scope, making variables or mixins only available within the document they were defined in.
@@ -572,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
@@ -641,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.
@@ -675,20 +667,20 @@ content changes.
### Precompiling Assets
-Rails comes bundled with a 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 task is:
+The command is:
```bash
-$ RAILS_ENV=production bin/rails assets:precompile
+$ RAILS_ENV=production rails assets:precompile
```
Capistrano (v2.15.1 and above) includes a recipe to handle this in deployment.
@@ -700,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
@@ -724,34 +716,36 @@ 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 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
`config.assets.prefix` ('/assets' by default).
-NOTE: If there are missing precompiled files in production you will get an
+NOTE: If there are missing precompiled files in production you will get a
`Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError`
exception indicating the name of the missing file(s).
@@ -837,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
@@ -845,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
@@ -908,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:
```
@@ -917,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
@@ -967,7 +961,7 @@ is present.
##### CDN Header Debugging
One way to check the headers are cached properly in your CDN is by using [curl](
-http://explainshell.com/explain?cmd=curl+-I+http%3A%2F%2Fwww.example.com). You
+https://explainshell.com/explain?cmd=curl+-I+http%3A%2F%2Fwww.example.com). You
can request the headers from both your server and your CDN to verify they are
the same:
@@ -1015,7 +1009,7 @@ such as `X-Cache` or for any additional headers they may add.
##### CDNs and the Cache-Control Header
The [cache control
-header](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9) is a W3C
+header](https://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
@@ -1024,7 +1018,7 @@ 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.public_file_server.headers = {
@@ -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,22 @@ 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 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.
+
+
+### GZipping your 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
+```
+
+Refer to your web server's documentation for instructions on how to serve gzipped assets.
### Using Your Own Compressor
@@ -1152,7 +1154,7 @@ The X-Sendfile header is a directive to the web server to ignore the response
from the application, and instead serve a specified file from disk. This option
is off by default, but can be enabled if your server supports it. When enabled,
this passes responsibility for serving the file to the web server, which is
-faster. Have a look at [send_file](http://api.rubyonrails.org/classes/ActionController/DataStreaming.html#method-i-send_file)
+faster. Have a look at [send_file](https://api.rubyonrails.org/classes/ActionController/DataStreaming.html#method-i-send_file)
on how to use this feature.
Apache and NGINX support this option, which can be enabled in
@@ -1170,7 +1172,7 @@ and any other environments you define with production behavior (not
TIP: For further details have a look at the docs of your production web server:
- [Apache](https://tn123.org/mod_xsendfile/)
-- [NGINX](http://wiki.nginx.org/XSendfile)
+- [NGINX](https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/)
Assets Cache Store
------------------
@@ -1198,105 +1200,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`:
-
-```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"
-```
-
-In `development.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
-# Expands the lines which load the assets
-config.assets.debug = true
+Sprockets.register_preprocessor 'text/css', AddComment
```
-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 and above no longer set 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 your `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 4977d4f30e..e076f10ece 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
==========================
@@ -94,9 +94,9 @@ class Book < ApplicationRecord
end
```
-![belongs_to Association Diagram](images/belongs_to.png)
+![belongs_to Association Diagram](images/association_basics/belongs_to.png)
-NOTE: `belongs_to` associations _must_ use the singular term. If you used the pluralized form in the above example for the `author` association in the `Book` model, you would be told that there was an "uninitialized constant Book::Authors". This is because Rails automatically infers the class name from the association name. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too.
+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:
@@ -109,7 +109,7 @@ class CreateBooks < ActiveRecord::Migration[5.0]
end
create_table :books do |t|
- t.belongs_to :author, index: true
+ t.belongs_to :author
t.datetime :published_at
t.timestamps
end
@@ -127,7 +127,7 @@ class Supplier < ApplicationRecord
end
```
-![has_one Association Diagram](images/has_one.png)
+![has_one Association Diagram](images/association_basics/has_one.png)
The corresponding migration might look like this:
@@ -140,7 +140,7 @@ class CreateSuppliers < ActiveRecord::Migration[5.0]
end
create_table :accounts do |t|
- t.belongs_to :supplier, index: true
+ t.belongs_to :supplier
t.string :account_number
t.timestamps
end
@@ -154,7 +154,7 @@ case, the column definition might look like this:
```ruby
create_table :accounts do |t|
- t.belongs_to :supplier, index: true, unique: true, foreign_key: true
+ t.belongs_to :supplier, index: { unique: true }, foreign_key: true
# ...
end
```
@@ -171,7 +171,7 @@ end
NOTE: The name of the other model is pluralized when declaring a `has_many` association.
-![has_many Association Diagram](images/has_many.png)
+![has_many Association Diagram](images/association_basics/has_many.png)
The corresponding migration might look like this:
@@ -184,7 +184,7 @@ class CreateAuthors < ActiveRecord::Migration[5.0]
end
create_table :books do |t|
- t.belongs_to :author, index: true
+ t.belongs_to :author
t.datetime :published_at
t.timestamps
end
@@ -213,7 +213,7 @@ class Patient < ApplicationRecord
end
```
-![has_many :through Association Diagram](images/has_many_through.png)
+![has_many :through Association Diagram](images/association_basics/has_many_through.png)
The corresponding migration might look like this:
@@ -231,8 +231,8 @@ class CreateAppointments < ActiveRecord::Migration[5.0]
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
end
@@ -299,7 +299,7 @@ class AccountHistory < ApplicationRecord
end
```
-![has_one :through Association Diagram](images/has_one_through.png)
+![has_one :through Association Diagram](images/association_basics/has_one_through.png)
The corresponding migration might look like this:
@@ -312,13 +312,13 @@ class CreateAccountHistories < ActiveRecord::Migration[5.0]
end
create_table :accounts do |t|
- t.belongs_to :supplier, index: true
+ t.belongs_to :supplier
t.string :account_number
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
end
@@ -340,7 +340,7 @@ class Part < ApplicationRecord
end
```
-![has_and_belongs_to_many Association Diagram](images/habtm.png)
+![has_and_belongs_to_many Association Diagram](images/association_basics/habtm.png)
The corresponding migration might look like this:
@@ -358,8 +358,8 @@ class CreateAssembliesAndParts < ActiveRecord::Migration[5.0]
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
@@ -387,7 +387,7 @@ The corresponding migration might look like this:
class CreateSuppliers < ActiveRecord::Migration[5.0]
def change
create_table :suppliers do |t|
- t.string :name
+ t.string :name
t.timestamps
end
@@ -439,7 +439,7 @@ 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
@@ -487,14 +487,14 @@ class CreatePictures < ActiveRecord::Migration[5.0]
def change
create_table :pictures do |t|
t.string :name
- t.references :imageable, polymorphic: true, index: true
+ 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
@@ -505,7 +505,7 @@ class Employee < ApplicationRecord
has_many :subordinates, class_name: "Employee",
foreign_key: "manager_id"
- belongs_to :manager, class_name: "Employee"
+ belongs_to :manager, class_name: "Employee", optional: true
end
```
@@ -517,7 +517,7 @@ In your migrations/schema, you will add a references column to the model itself.
class CreateEmployees < ActiveRecord::Migration[5.0]
def change
create_table :employees do |t|
- t.references :manager, index: true
+ t.references :manager
t.timestamps
end
end
@@ -550,8 +550,8 @@ But what if you want to reload the cache, because data might have been changed b
```ruby
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
+author.books.reload.empty? # discards the cached copy of books
+ # and goes back to the database
```
### Avoiding Name Collisions
@@ -572,27 +572,35 @@ class Book < ApplicationRecord
end
```
-This declaration needs to be backed up by the proper foreign key declaration on the books table:
+This declaration needs to be backed up by a corresponding foreign key column in the books table. For a brand new table, the migration might look something like this:
```ruby
class CreateBooks < ActiveRecord::Migration[5.0]
def change
create_table :books do |t|
- t.datetime :published_at
- t.string :book_number
- t.integer :author_id
+ t.datetime :published_at
+ t.string :book_number
+ t.references :author
end
+ end
+end
+```
+
+Whereas for an existing table, it might look like this:
- add_index :books, :author_id
+```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 book of the class names. So a join between author and book models will give the default join table name of "authors_books" because "a" outranks "b" in lexical ordering.
+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).
@@ -647,11 +655,11 @@ By default, associations look for objects only within the current module's scope
module MyApplication
module Business
class Supplier < ApplicationRecord
- has_one :account
+ has_one :account
end
class Account < ApplicationRecord
- belongs_to :supplier
+ belongs_to :supplier
end
end
end
@@ -663,13 +671,13 @@ This will work fine, because both the `Supplier` and the `Account` class are def
module MyApplication
module Business
class Supplier < ApplicationRecord
- has_one :account
+ has_one :account
end
end
module Billing
class Account < ApplicationRecord
- belongs_to :supplier
+ belongs_to :supplier
end
end
end
@@ -681,14 +689,14 @@ To associate a model with a model in a different namespace, you must specify the
module MyApplication
module Business
class Supplier < ApplicationRecord
- has_one :account,
+ has_one :account,
class_name: "MyApplication::Billing::Account"
end
end
module Billing
class Account < ApplicationRecord
- belongs_to :supplier,
+ belongs_to :supplier,
class_name: "MyApplication::Business::Supplier"
end
end
@@ -709,55 +717,64 @@ class Book < ApplicationRecord
end
```
-By default, Active Record doesn't know about the connection between these associations. This can lead to two copies of an object getting out of sync:
+Active Record will attempt to automatically identify that these two models share a bi-directional association based on the association name. In this way, Active Record will only load one copy of the `Author` object, making your application more efficient and preventing inconsistent data:
```ruby
a = Author.first
b = a.books.first
a.first_name == b.author.first_name # => true
-a.first_name = 'Manny'
-a.first_name == b.author.first_name # => false
+a.first_name = 'David'
+a.first_name == b.author.first_name # => true
```
-This happens because `a` and `b.author` are two different in-memory representations of the same data, and neither one is automatically refreshed from changes to the other. Active Record provides the `:inverse_of` option so that you can inform it of these relations:
+Active Record supports automatic identification for most associations with standard names. However, Active Record will not automatically identify bi-directional associations that contain a scope or any of the following options:
+
+* `:through`
+* `:foreign_key`
+
+For example, consider the following model declarations:
```ruby
class Author < ApplicationRecord
- has_many :books, inverse_of: :author
+ has_many :books
end
class Book < ApplicationRecord
- belongs_to :author, inverse_of: :books
+ belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
```
-With these changes, Active Record will only load one copy of the author object, preventing inconsistencies and making your application more efficient:
+Active Record will no longer automatically recognize the bi-directional association:
```ruby
a = Author.first
b = a.books.first
-a.first_name == b.author.first_name # => true
-a.first_name = 'Manny'
-a.first_name == b.author.first_name # => true
+a.first_name == b.writer.first_name # => true
+a.first_name = 'David'
+a.first_name == b.writer.first_name # => false
```
-There are a few limitations to `inverse_of` support:
+Active Record provides the `:inverse_of` option so you can explicitly declare bi-directional associations:
-* 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.
+```ruby
+class Author < ApplicationRecord
+ has_many :books, inverse_of: 'writer'
+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:
+class Book < ApplicationRecord
+ belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
+end
+```
-* `:conditions`
-* `:through`
-* `:polymorphic`
-* `:foreign_key`
+By including the `:inverse_of` option in the `has_many` association declaration, Active Record will now recognize the bi-directional association:
+
+```ruby
+a = Author.first
+b = a.books.first
+a.first_name == b.writer.first_name # => true
+a.first_name = 'David'
+a.first_name == b.writer.first_name # => true
+```
Detailed Association Reference
------------------------------
@@ -770,13 +787,14 @@ The `belongs_to` association creates a one-to-one match with another model. In d
#### Methods Added by `belongs_to`
-When you declare a `belongs_to` association, the declaring class automatically gains five methods related to the association:
+When you declare a `belongs_to` association, the declaring class automatically gains 6 methods related to the association:
* `association`
* `association=(associate)`
* `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:
@@ -794,6 +812,7 @@ 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.
@@ -806,10 +825,10 @@ The `association` method returns the associated object, if any. If no associated
@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), call `#reload` on the parent object.
+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
+@author = @book.reload_author
```
##### `association=(associate)`
@@ -849,7 +868,7 @@ While Rails uses intelligent defaults that will work well in most situations, th
```ruby
class Book < ApplicationRecord
- belongs_to :author, dependent: :destroy,
+ belongs_to :author, touch: :books_updated_at,
counter_cache: true
end
```
@@ -870,7 +889,7 @@ The `belongs_to` association supports these options:
##### `:autosave`
-If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object.
+If you set the `:autosave` option to `true`, Rails will save any loaded association members and destroy members that are marked for destruction whenever you save the parent object. Setting `:autosave` to `false` is not the same as not setting the `:autosave` option. If the `:autosave` option is not present, then new associated objects will be saved, but updated associated objects will not be saved.
##### `:class_name`
@@ -926,19 +945,18 @@ class Author < ApplicationRecord
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`.
##### `:dependent`
-Controls what happens to associated objects when their owner is destroyed:
+If you set the `:dependent` option to:
-* `:destroy` causes the associated objects to also be destroyed.
-* `:delete_all` causes the associated objects to be deleted directly from the database (callbacks are not executed).
-* `:nullify` causes the foreign keys to be set to `NULL` (callbacks are not executed).
-* `:restrict_with_exception` causes an exception to be raised if there are associated records.
-* `:restrict_with_error` causes an error to be added to the owner if there are associated objects.
+* `:destroy`, when the object is destroyed, `destroy` will be called on its
+associated objects.
+* `:delete`, when the object is destroyed, all its associated objects will be
+deleted directly from the database without calling their `destroy` method.
WARNING: You should not specify this option on a `belongs_to` association that is connected with a `has_many` association on the other class. Doing so can lead to orphaned records in your database.
@@ -977,7 +995,7 @@ When we execute `@user.todos.create` then the `@todo` record will have its
##### `:inverse_of`
-The `:inverse_of` option specifies the name of the `has_many` or `has_one` association that is the inverse of this association. Does not work in combination with the `:polymorphic` options.
+The `:inverse_of` option specifies the name of the `has_many` or `has_one` association that is the inverse of this association.
```ruby
class Author < ApplicationRecord
@@ -1007,7 +1025,7 @@ class Author < ApplicationRecord
end
```
-In this case, saving or destroying an book will update the timestamp on the associated author. 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 Book < ApplicationRecord
@@ -1030,8 +1048,7 @@ There may be times when you wish to customize the query used by `belongs_to`. Su
```ruby
class Book < ApplicationRecord
- belongs_to :author, -> { where active: true },
- dependent: :destroy
+ belongs_to :author, -> { where active: true }
end
```
@@ -1047,7 +1064,7 @@ You can use any of the standard [querying methods](active_record_querying.html)
The `where` method lets you specify the conditions that the associated object must meet.
```ruby
-class book < ApplicationRecord
+class Book < ApplicationRecord
belongs_to :author, -> { where active: true }
end
```
@@ -1057,13 +1074,13 @@ 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 < ApplicationRecord
+class Chapter < ApplicationRecord
belongs_to :book
end
class Book < ApplicationRecord
belongs_to :author
- has_many :line_items
+ has_many :chapters
end
class Author < ApplicationRecord
@@ -1071,16 +1088,16 @@ class Author < ApplicationRecord
end
```
-If you frequently retrieve authors directly from line items (`@line_item.book.author`), then you can make your code somewhat more efficient by including authors in the association from line items to books:
+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 < ApplicationRecord
+class Chapter < ApplicationRecord
belongs_to :book, -> { includes :author }
end
class Book < ApplicationRecord
belongs_to :author
- has_many :line_items
+ has_many :chapters
end
class Author < ApplicationRecord
@@ -1120,13 +1137,14 @@ The `has_one` association creates a one-to-one match with another model. In data
#### Methods Added by `has_one`
-When you declare a `has_one` association, the declaring class automatically gains five methods related to the association:
+When you declare a `has_one` association, the declaring class automatically gains 6 methods related to the association:
* `association`
* `association=(associate)`
* `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:
@@ -1144,6 +1162,7 @@ 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.
@@ -1156,10 +1175,10 @@ 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), call `#reload` on the parent object.
+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
+@account = @supplier.reload_account
```
##### `association=(associate)`
@@ -1220,7 +1239,7 @@ Setting the `:as` option indicates that this is a polymorphic association. Polym
##### `:autosave`
-If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object.
+If you set the `:autosave` option to `true`, Rails will save any loaded association members and destroy members that are marked for destruction whenever you save the parent object. Setting `:autosave` to `false` is not the same as not setting the `:autosave` option. If the `:autosave` option is not present, then new associated objects will be saved, but updated associated objects will not be saved.
##### `:class_name`
@@ -1238,8 +1257,8 @@ 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
+* `:nullify` causes the foreign key to be set to `NULL`. Polymorphic type column is also nullified on polymorphic associations. Callbacks are not executed.
+* `: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
@@ -1262,7 +1281,7 @@ TIP: In any case, Rails will not create foreign key columns for you. You need to
##### `:inverse_of`
-The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. Does not work in combination with the `:through` or `:as` options.
+The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association.
```ruby
class Supplier < ApplicationRecord
@@ -1383,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
@@ -1391,7 +1410,7 @@ The `has_many` association creates a one-to-many relationship with another model
#### Methods Added by `has_many`
-When you declare a `has_many` association, the declaring class automatically gains 16 methods related to the association:
+When you declare a `has_many` association, the declaring class automatically gains 17 methods related to the association:
* `collection`
* `collection<<(object, ...)`
@@ -1409,6 +1428,7 @@ 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:
@@ -1437,11 +1457,12 @@ books.exists?(...)
books.build(attributes = {}, ...)
books.create(attributes = {})
books.create!(attributes = {})
+books.reload
```
##### `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
@books = @author.books
@@ -1477,7 +1498,7 @@ WARNING: Objects will _always_ be removed from the database, ignoring the `:depe
##### `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`
@@ -1489,7 +1510,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`
@@ -1522,10 +1543,11 @@ The `collection.size` method returns the number of objects in the collection.
##### `collection.find(...)`
-The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`.
+The `collection.find` method finds objects within the collection. It uses the same syntax and options as
+[`ActiveRecord::Base.find`](https://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find).
```ruby
-@available_books = @author.books.find(1)
+@available_book = @author.books.find(1)
```
##### `collection.where(...)`
@@ -1541,7 +1563,7 @@ The `collection.where` method finds objects within the collection based on the c
The `collection.exists?` method checks whether an object meeting the supplied
conditions exists in the collection. It uses the same syntax and options as
-[`ActiveRecord::Base.exists?`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-exists-3F).
+[`ActiveRecord::Base.exists?`](https://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-exists-3F).
##### `collection.build(attributes = {}, ...)`
@@ -1575,6 +1597,14 @@ 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:
@@ -1606,7 +1636,7 @@ Setting the `:as` option indicates that this is a polymorphic association, as di
##### `:autosave`
-If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object.
+If you set the `:autosave` option to `true`, Rails will save any loaded association members and destroy members that are marked for destruction whenever you save the parent object. Setting `:autosave` to `false` is not the same as not setting the `:autosave` option. If the `:autosave` option is not present, then new associated objects will be saved, but updated associated objects will not be saved.
##### `:class_name`
@@ -1628,10 +1658,12 @@ 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
+* `:nullify` causes the foreign key to be set to `NULL`. Polymorphic type column is also nullified on polymorphic associations. Callbacks are not executed.
+* `: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:
@@ -1646,7 +1678,7 @@ TIP: In any case, Rails will not create foreign key columns for you. You need to
##### `:inverse_of`
-The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association. Does not work in combination with the `:through` or `:as` options.
+The `:inverse_of` option specifies the name of the `belongs_to` association that is the inverse of this association.
```ruby
class Author < ApplicationRecord
@@ -1748,8 +1780,8 @@ The `group` method supplies an attribute name to group the result set by, using
```ruby
class Author < ApplicationRecord
- has_many :line_items, -> { group 'books.id' },
- through: :books
+ has_many :chapters, -> { group 'books.id' },
+ through: :books
end
```
@@ -1764,27 +1796,27 @@ end
class Book < ApplicationRecord
belongs_to :author
- has_many :line_items
+ has_many :chapters
end
-class LineItem < ApplicationRecord
+class Chapter < ApplicationRecord
belongs_to :book
end
```
-If you frequently retrieve line items directly from authors (`@author.books.line_items`), then you can make your code somewhat more efficient by including line items in the association from authors to books:
+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 Author < ApplicationRecord
- has_many :books, -> { includes :line_items }
+ has_many :books, -> { includes :chapters }
end
class Book < ApplicationRecord
belongs_to :author
- has_many :line_items
+ has_many :chapters
end
-class LineItem < ApplicationRecord
+class Chapter < ApplicationRecord
belongs_to :book
end
```
@@ -1797,7 +1829,7 @@ The `limit` method lets you restrict the total number of objects that will be fe
class Author < ApplicationRecord
has_many :recent_books,
-> { order('published_at desc').limit(100) },
- class_name: "Book",
+ class_name: "Book"
end
```
@@ -1841,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
@@ -1860,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
@@ -1913,7 +1945,7 @@ The `has_and_belongs_to_many` association creates a many-to-many relationship wi
#### Methods Added by `has_and_belongs_to_many`
-When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 16 methods related to the association:
+When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 17 methods related to the association:
* `collection`
* `collection<<(object, ...)`
@@ -1931,6 +1963,7 @@ 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:
@@ -1959,6 +1992,7 @@ assemblies.exists?(...)
assemblies.build(attributes = {}, ...)
assemblies.create(attributes = {})
assemblies.create!(attributes = {})
+assemblies.reload
```
##### Additional Column Methods
@@ -1970,7 +2004,7 @@ WARNING: The use of extra attributes on the join table in a `has_and_belongs_to_
##### `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
@@ -1994,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)
@@ -2006,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`
@@ -2018,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`
@@ -2044,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`](https://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-find).
```ruby
@assembly = @part.assemblies.find(1)
@@ -2052,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)
@@ -2062,7 +2095,7 @@ The `collection.where` method finds objects within the collection based on the c
The `collection.exists?` method checks whether an object meeting the supplied
conditions exists in the collection. It uses the same syntax and options as
-[`ActiveRecord::Base.exists?`](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-exists-3F).
+[`ActiveRecord::Base.exists?`](https://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html#method-i-exists-3F).
##### `collection.build(attributes = {})`
@@ -2084,6 +2117,14 @@ 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:
@@ -2121,7 +2162,7 @@ end
##### `:autosave`
-If you set the `:autosave` option to `true`, Rails will save any loaded members and destroy members that are marked for destruction whenever you save the parent object.
+If you set the `:autosave` option to `true`, Rails will save any loaded association members and destroy members that are marked for destruction whenever you save the parent object. Setting `:autosave` to `false` is not the same as not setting the `:autosave` option. If the `:autosave` option is not present, then new associated objects will be saved, but updated associated objects will not be saved.
##### `:class_name`
@@ -2309,6 +2350,17 @@ end
If a `before_add` callback throws an exception, the object does not get added to the collection. Similarly, if a `before_remove` callback throws an exception, the object does not get removed from the collection.
+NOTE: These callbacks are called only when the associated objects are added or removed through the association collection:
+
+```ruby
+# Triggers `before_add` callback
+author.books << book
+author.books = [book, book2]
+
+# Does not trigger the `before_add` callback
+book.update(author_id: 1)
+```
+
### Association Extensions
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:
@@ -2351,7 +2403,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 de0fa2fdc0..7dfc39e192 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
@@ -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
@@ -775,7 +798,7 @@ to load a file using the current [loading mechanism](#loading-mechanism), and
keeping track of constants defined in that file as if they were autoloaded to
have them reloaded as needed.
-`require_dependency` is rarely needed, but see a couple of use-cases in
+`require_dependency` is rarely needed, but see a couple of use cases in
[Autoloading and STI](#autoloading-and-sti) and [When Constants aren't
Triggered](#when-constants-aren-t-missed).
@@ -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 < ApplicationRecord
+# 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 ec4444eb43..f6612ba338 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_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.**
Caching with Rails: An Overview
===============================
@@ -32,7 +32,7 @@ Basic Caching
This is an introduction to three types of caching techniques: page, action and
fragment caching. By default Rails provides fragment caching. In order to use
page and action caching you will need to add `actionpack-page_caching` and
-`actionpack-action_caching` to your Gemfile.
+`actionpack-action_caching` to your `Gemfile`.
By default, caching is only enabled in your production environment. To play
around with caching locally you'll want to enable caching in your local
@@ -51,10 +51,10 @@ For instance, it will not impact low-level caching, that we address
### Page Caching
Page caching is a Rails mechanism which allows the request for a generated page
-to be fulfilled by the webserver (i.e. Apache or NGINX) without having to go
+to be fulfilled by the web server (i.e. Apache or NGINX) without having to go
through the entire Rails stack. While this is super fast it can't be applied to
every situation (such as pages that need authentication). Also, because the
-webserver is serving a file directly from the filesystem you will need to
+web server is serving a file directly from the filesystem you will need to
implement cache expiration.
INFO: Page Caching has been removed from Rails 4. See the [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching).
@@ -63,7 +63,7 @@ INFO: Page Caching has been removed from Rails 4. See the [actionpack-page_cachi
Page Caching cannot be used for actions that have before filters - for example, pages that require authentication. This is where Action Caching comes in. Action Caching works like Page Caching except the incoming web request hits the Rails stack so that before filters can be run on it before the cache is served. This allows authentication and other restrictions to be run while still serving the result of the output from a cached copy.
-INFO: Action Caching has been removed from Rails 4. See the [actionpack-action_caching gem](https://github.com/rails/actionpack-action_caching). See [DHH's key-based cache expiration overview](http://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) for the newly-preferred method.
+INFO: Action Caching has been removed from Rails 4. See the [actionpack-action_caching gem](https://github.com/rails/actionpack-action_caching). See [DHH's key-based cache expiration overview](https://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) for the newly-preferred method.
### Fragment Caching
@@ -100,9 +100,9 @@ called key-based expiration.
Cache fragments will also be expired when the view fragment changes (e.g., the
HTML in the view changes). The string of characters at the end of the key is a
-template tree digest. It is an md5 hash computed based on the contents of the
-view fragment you are caching. If you change the view fragment, the md5 hash
-will change, expiring the existing file.
+template tree digest. It is a hash digest computed based on the contents of the
+view fragment you are caching. If you change the view fragment, the digest will
+change, expiring the existing file.
TIP: Cache stores like Memcached will automatically delete old cache files.
@@ -175,10 +175,28 @@ class Game < ApplicationRecord
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
@@ -198,11 +216,11 @@ render "comments/comments"
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")
```
On the other hand, some calls need to be changed to make caching work properly.
@@ -258,7 +276,7 @@ comment format anywhere in the template, like:
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
+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
@@ -270,21 +288,21 @@ simply be explicit in a comment, like:
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 < ApplicationRecord
def competing_price
- Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do
+ Rails.cache.fetch("#{cache_key_with_version}/competing_price", expires_in: 12.hours) do
Competitor::API.find_price(id)
end
end
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
@@ -344,15 +362,15 @@ 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.
@@ -381,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.
@@ -396,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
@@ -420,6 +444,78 @@ The `write` and `fetch` methods on this cache accept two additional options that
config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"
```
+### ActiveSupport::Cache::RedisCacheStore
+
+The Redis cache store takes advantage of Redis support for automatic eviction
+when it reaches max memory, allowing it to behave much like a Memcached cache server.
+
+Deployment note: Redis doesn't expire keys by default, so take care to use a
+dedicated Redis cache server. Don't fill up your persistent-Redis server with
+volatile cache data! Read the
+[Redis cache server setup guide](https://redis.io/topics/lru-cache) in detail.
+
+For a cache-only Redis server, set `maxmemory-policy` to one of the variants of allkeys.
+Redis 4+ supports least-frequently-used eviction (`allkeys-lfu`), an excellent
+default choice. Redis 3 and earlier should use least-recently-used eviction (`allkeys-lru`).
+
+Set cache read and write timeouts relatively low. Regenerating a cached value
+is often faster than waiting more than a second to retrieve it. Both read and
+write timeouts default to 1 second, but may be set lower if your network is
+consistently low-latency.
+
+By default, the cache store will not attempt to reconnect to Redis if the
+connection fails during a request. If you experience frequent disconnects you
+may wish to enable reconnect attempts.
+
+Cache reads and writes never raise exceptions; they just return `nil` instead,
+behaving as if there was nothing in the cache. To gauge whether your cache is
+hitting exceptions, you may provide an `error_handler` to report to an
+exception gathering service. It must accept three keyword arguments: `method`,
+the cache store method that was originally called; `returning`, the value that
+was returned to the user, typically `nil`; and `exception`, the exception that
+was rescued.
+
+To get started, add the redis gem to your Gemfile:
+
+```ruby
+gem 'redis'
+```
+
+You can enable support for the faster [hiredis](https://github.com/redis/hiredis)
+connection library by additionally adding its ruby wrapper to your Gemfile:
+
+```ruby
+gem 'hiredis'
+```
+
+Redis cache store will automatically require & use hiredis if available. No further
+configuration is needed.
+
+Finally, add the configuration in the relevant `config/environments/*.rb` file:
+
+```ruby
+config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
+```
+
+A more complex, production Redis cache store may look something like this:
+
+```ruby
+cache_servers = %w(redis://cache-01:6379/0 redis://cache-02:6379/0)
+config.cache_store = :redis_cache_store, { url: cache_servers,
+
+ connect_timeout: 30, # Defaults to 20 seconds
+ read_timeout: 0.2, # Defaults to 1 second
+ write_timeout: 0.2, # Defaults to 1 second
+ reconnect_attempts: 1, # Defaults to 0
+
+ error_handler: -> (method:, returning:, exception:) {
+ # Report errors to Sentry as warnings
+ Raven.capture_exception exception, level: 'warning',
+ tags: { method: method, returning: returning }
+ }
+}
+```
+
### ActiveSupport::Cache::NullStore
This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with `Rails.cache` but caching may interfere with being able to see the results of code changes. With this cache store, all `fetch` and `read` operations will result in a miss.
@@ -467,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
@@ -481,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
@@ -512,12 +608,76 @@ class ProductsController < ApplicationController
end
```
-### A note on weak ETags
+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.
-Etags generated by Rails are weak 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. If you absolutely need to generate a strong etag, it can be assigned to the header directly.
+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
- response.add_header "ETag", Digest::MD5.hexdigest(response.body)
+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 62d742fc28..a83724f1bb 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`
-* `bin/rails`
+* `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.
@@ -61,9 +100,9 @@ With no further work, `rails server` will run our new shiny Rails app:
```bash
$ cd commandsapp
-$ bin/rails server
+$ rails server
=> Booting Puma
-=> Rails 5.0.0 application starting in development on http://0.0.0.0:3000
+=> Rails 5.1.0 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.0.2 (ruby 2.3.0-p0), codename: Plethora of Penguin Pinatas
@@ -80,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.
@@ -92,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]
...
@@ -102,6 +141,7 @@ Please choose a generator below.
Rails:
assets
+ channel
controller
generator
...
@@ -117,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]
...
@@ -143,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
@@ -153,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`):
@@ -182,7 +221,7 @@ 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
+$ rails server
=> Booting Puma...
```
@@ -193,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]
@@ -209,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](https://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
@@ -241,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
@@ -252,28 +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 `bin/rails db:migrate` command. We'll talk more about bin/rails 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/rails 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. Please visit
-[the testing guide](http://guides.rubyonrails.org/testing.html) for an in-depth
+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!)
@@ -287,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>
```
@@ -303,7 +344,7 @@ irb(main):001:0>
Inside the `rails console` you have access to the `app` and `helper` instances.
-With the `app` method you can access url and path helpers, as well as do requests.
+With the `app` method you can access URL and path helpers, as well as do requests.
```bash
>> app.root_path
@@ -326,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 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`.
@@ -335,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`.
@@ -343,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`
@@ -359,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
@@ -368,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
@@ -377,158 +418,146 @@ $ bin/rails destroy model Oops
remove test/fixtures/oops.yml
```
-bin/rails
----------
-
-Since Rails 5.0+ has rake commands built into the rails executable, `bin/rails` is the new default for running commands.
-
-You can get a list of bin/rails tasks available to you, which will often depend on your current directory, by typing `bin/rails --help`. Each task has a description, and should help you find the thing you need.
-
-```bash
-$ bin/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.dump file
-db:schema:cache:dump Creates a db/schema_cache.dump 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 ...
-```
-INFO: You can also use `bin/rails -T` to get the list of tasks.
-
-### `about`
+### `rails about`
-`bin/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.
+`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/rails 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, ActionDispatch::LoadInterlock, #<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, 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 `bin/rails assets:precompile`, and remove older compiled assets using `bin/rails 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 `bin/rails 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:` bin/rails namespace are `migrate` and `create`, and it will pay off to try out all of the migration bin/rails tasks (`up`, `down`, `redo`, `reset`). `bin/rails 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`
+
+`rails notes` searches through your code for comments beginning with a specific keyword. You can refer to `rails notes --help` for information about usage.
-`bin/rails 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.
+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/rails 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 `bin/rails 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/rails 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 `bin/rails 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/rails 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
-By default, `rails 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`.
+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.
+
+```ruby
+config.annotations.register_extensions("scss", "sass") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ }
+```
```bash
-$ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor'
-$ bin/rails 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`
`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:
* `rails tmp:cache:clear` clears `tmp/cache`.
* `rails tmp:sockets:clear` clears `tmp/sockets`.
-* `rails tmp:clear` clears all cache and sockets files.
-* `rails tmp:create` creates tmp directories for cache, sockets and pids.
+* `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
@@ -540,7 +569,7 @@ The `tmp:` namespaced tasks will help you clear and create the `Rails.root/tmp`
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"
@@ -572,12 +601,12 @@ end
Invocation of the tasks will look like:
```bash
-$ bin/rails task_name
-$ bin/rails "task_name[value 1]" # entire argument string should be quoted
-$ bin/rails 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 you 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
-------------------------------
@@ -623,9 +652,9 @@ $ cat config/database.yml
#
# 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
@@ -635,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 e80f994deb..e1c9fad232 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,7 +32,7 @@ Configuring Rails Components
In general, the work of configuring Rails means configuring the components of Rails, as well as configuring Rails itself. The configuration file `config/application.rb` and environment-specific configuration files (such as `config/environments/production.rb`) allow you to specify the various settings that you want to pass down to all of the components.
-For example, the `config/application.rb` file includes this setting:
+For example, you could add this setting to `config/application.rb` file:
```ruby
config.time_zone = 'Central Time (US & Canada)'
@@ -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,27 +86,31 @@ 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.disable_sandbox` controls whether or not someone can start a console in sandbox mode. This is helpful to avoid a long running session of sandbox console, that could lead a database server to run out of memory. Defaults to false.
-* `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` 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_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.debug_exception_response_format` sets the format used in responses when errors occur in development mode.
+* `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.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`. Parameters filter works by partial matching regular expression.
+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 served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://edgeapi.rubyonrails.org/classes/ActionDispatch/SSL.html) for details.
+* `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](https://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`. 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_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`,
@@ -116,31 +118,34 @@ 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` is the logger that will be used for `Rails.logger` and any related Rails logging such as `ActiveRecord::Base.logger`. It defaults to an instance of `ActiveSupport::TaggedLogging` that wraps an instance of `ActiveSupport::Logger` which outputs a log to the `log/` directory. You can supply a custom logger, to get full compatability you must follow these guidelines:
- * To support a formatter you must manually assign a formatter from the `config.log_formatter` value to the logger.
- * To support tagged loggs the log instance must be wrapped with `ActiveSupport::TaggedLogging`.
- * To support silencing the logger must include `LoggerSilence` and `ActiveSupport::LoggerThreadSafeLevel` modules. The `ActiveSupport::Logger` class already includes these modules.
+* `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::LoggerThreadSafeLevel
- include LoggerSilence
+ include ActiveSupport::LoggerSilence
end
mylogger = MyLogger.new(STDOUT)
mylogger.formatter = config.log_formatter
- config.logger = ActiveSupport::TaggedLogging.new(mylogger)
+ 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`.
+* `config.credentials.content_path` configures lookup path for encrypted credentials.
-* `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.credentials.key_path` configures lookup path for encryption key.
-* `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:
+* `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.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` 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
@@ -153,31 +158,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.
+* `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.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
@@ -196,8 +205,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
@@ -206,7 +214,7 @@ The full set of methods that can be used in this block are as follows:
* `stylesheets` turns on the hook for stylesheets in generators. Used in Rails for when the `scaffold` generator is run, but this hook can be used in other generates as well. Defaults to `true`.
* `stylesheet_engine` configures the stylesheet engine (for eg. sass) to be used when generating assets. Defaults to `:css`.
* `scaffold_stylesheet` creates `scaffold.css` when generating a scaffolded resource. Defaults to `true`.
-* `test_framework` defines which test framework to use. Defaults to `false` and will use Minitest by default.
+* `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
@@ -215,7 +223,7 @@ Every Rails application comes with a standard set of middleware which it uses in
* `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::LoadInterlock` 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.
+* `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.
@@ -224,8 +232,6 @@ 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.
@@ -244,6 +250,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
@@ -266,7 +278,7 @@ config.middleware.delete Rack::MethodOverride
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`.
@@ -274,6 +286,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_customize_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:
@@ -281,8 +319,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.
@@ -290,17 +328,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.error_on_ignored_order_or_limit` specifies if an error should be raised if the order or limit of a query is ignored during a batch query. The options are true (raise error) or false (warn). Default is false.
+* `config.active_record.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.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`.
@@ -308,17 +350,17 @@ 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
@@ -328,19 +370,38 @@ All these configuration options are delegated to the `I18n` library.
* `config.active_record.warn_on_records_fetched_greater_than` allows setting a
warning threshold for query result size. If the number of records returned
by a query exceeds the threshold, a warning is logged. This can be used to
- identify queries which might be causing memory bloat.
+ identify queries which might be causing a memory bloat.
* `config.active_record.index_nested_attribute_errors` allows errors for nested
- has_many relationships to be displayed with an index as well as the error.
- Defaults to false.
+ `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::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns in a MySQL database to be booleans and is true by default.
+* `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns as booleans. Defaults to `true`.
+
+The PostgreSQL adapter adds one additional configuration option:
+
+* `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 schema dumper adds one additional configuration option:
+The schema dumper adds two additional configuration options:
-* `ActiveRecord::SchemaDumper.ignore_tables` accepts an array of tables that should _not_ be included in any generated schema file. This setting is ignored unless `config.active_record.schema_format == :ruby`.
+* `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
@@ -348,11 +409,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 the caching features provided by the Action Controller component 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.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.
@@ -364,13 +425,31 @@ The schema dumper adds one additional configuration option:
* `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.
+
* `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
@@ -382,7 +461,10 @@ The schema dumper adds one additional configuration option:
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
- 'X-Content-Type-Options' => 'nosniff'
+ 'X-Content-Type-Options' => 'nosniff',
+ 'X-Download-Options' => 'noopen',
+ 'X-Permitted-Cross-Domain-Policies' => 'none',
+ 'Referrer-Policy' => 'strict-origin-when-cross-origin'
}
```
@@ -390,6 +472,10 @@ The schema dumper adds one additional configuration option:
* `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'`.
@@ -397,34 +483,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
}
```
@@ -432,14 +540,14 @@ 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.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
@@ -459,7 +567,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
@@ -475,12 +583,41 @@ encrypted cookies salt value. Defaults to `'signed encrypted cookie'`.
The default setting is `true`, which uses the partial at `/admin/articles/_article.erb`. Setting the value to `false` would render `/articles/_article.erb`, which is the same behavior as rendering from a non-namespaced controller such as `ArticlesController`.
* `config.action_view.raise_on_missing_translations` determines whether an
- error should be raised for missing translations.
+ error should be raised for missing translations. This defaults to `false`.
* `config.action_view.automatically_disable_submit_tag` determines whether
- submit_tag should automatically disable on click, this defaults to true.
+ `submit_tag` should automatically disable on click, this defaults to `true`.
+
+* `config.action_view.debug_missing_translation` determines whether to wrap the missing translations key in a `<span>` tag or not. This defaults to `true`.
+
+* `config.action_view.form_with_generates_remote_forms` determines whether `form_with` generates remote forms or not. This defaults to `true`.
+
+* `config.action_view.form_with_generates_ids` determines whether `form_with` generates ids on inputs. This defaults to `true`.
+
+* `config.action_view.default_enforce_utf8` determines whether forms are generated with a hidden tag that forces older versions of Internet Explorer to submit forms encoded in UTF-8. This defaults to `false`.
+
+
+### Configuring Action Mailbox
+
+`config.action_mailbox` provides the following configuration options:
+
+* `config.action_mailbox.logger` contains the logger used by Action Mailbox. It accepts a logger conforming to the interface of Log4r or the default Ruby Logger class. The default is `Rails.logger`.
+
+ ```ruby
+ config.action_mailbox.logger = ActiveSupport::Logger.new(STDOUT)
+ ```
+
+* `config.action_mailbox.incinerate_after` accepts an `ActiveSupport::Duration` indicating how long after processing `ActionMailbox::InboundEmail` records should be destroyed. It defaults to `30.days`.
+
+ ```ruby
+ # Incinerate inbound emails 14 days after processing.
+ config.action_mailbox.incinerate_after = 14.days
+ ```
+
+* `config.action_mailbox.queues.incineration` accepts a symbol indicating the Active Job queue to use for incineration jobs. It defaults to `:action_mailbox_incineration`.
+
+* `config.action_mailbox.queues.routing` accepts a symbol indicating the Active Job queue to use for routing jobs. It defaults to `:action_mailbox_routing`.
-* `config.action_view.debug_missing_translation` determins whether to wrap the missing translations key in a `<span>` tag or not. This defaults to true.
### Configuring Action Mailer
@@ -495,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](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:
@@ -535,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
@@ -550,7 +696,9 @@ There are a number of settings available on `config.action_mailer`:
* `config.action_mailer.deliver_later_queue_name` specifies the queue name for
mailers. By default this is `mailers`.
-* `config.action_mailer.perform_caching` specifies whether the mailer templates should perform fragment caching or not. By default this is false in all environments.
+* `config.action_mailer.perform_caching` specifies whether the mailer templates should perform fragment caching or not. By default this is `false` in all environments.
+
+* `config.action_mailer.delivery_job` specifies delivery job for mail. Defaults to `ActionMailer::DeliveryJob`.
### Configuring Active Support
@@ -559,7 +707,7 @@ 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`.
@@ -567,7 +715,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`.
-* `ActiveSupport.halt_callback_chains_on_return_false` specifies whether Active Record and Active Model callback chains can be halted by returning `false` in a 'before' callback. When set to `false`, callback chains are halted only when explicitly done so with `throw(:abort)`. When set to `true`, callback chains are halted when a callback returns false (the previous behavior before Rails 5) and a deprecation warning is given. Defaults to `true` during the deprecation period. New Rails 5 apps generate an initializer file called `callback_terminator.rb` which sets the value to `false`. This file is *not* added when running `rails app:update`, so returning `false` will still work on older apps ported to Rails 5 and display a deprecation warning to prompt users to update their code.
+* `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.
* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`.
@@ -577,13 +727,13 @@ There are a few configuration options available in Active Support:
* `ActiveSupport::Deprecation.silence` takes a block in which all deprecation warnings are silenced.
-* `ActiveSupport::Deprecation.silenced` sets whether or not to display deprecation warnings.
+* `ActiveSupport::Deprecation.silenced` sets whether or not to display deprecation warnings. The default is `false`.
### Configuring Active Job
`config.active_job` provides the following configuration options:
-* `config.active_job.queue_adapter` sets the adapter for the queueing backend. The default adapter is `:async`. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
+* `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](https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
```ruby
# Be sure to have the adapter's gem in your Gemfile
@@ -632,6 +782,10 @@ 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 `[]`.
+
+* `config.active_job.return_false_on_aborted_enqueue` change the return value of `#enqueue` to false instead of the job instance when the enqueuing is aborted. Defaults to `false`.
+
### Configuring Action Cable
* `config.action_cable.url` accepts a string for the URL for where
@@ -643,6 +797,100 @@ main application.
You can set this as nil to not mount Action Cable as part of your
normal Rails server.
+You can find more detailed configuration options in the
+[Action Cable Overview](action_cable_overview.html#configuration).
+
+
+### 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/pjpeg image/tiff 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.queues.analysis` accepts a symbol indicating the Active Job queue to use for analysis jobs. When this option is `nil`, analysis jobs are sent to the default Active Job queue (see `config.active_job.default_queue_name`).
+
+ ```ruby
+ config.active_storage.queues.analysis = :low_priority
+ ```
+
+* `config.active_storage.queues.purge` accepts a symbol indicating the Active Job queue to use for purge jobs. When this option is `nil`, purge jobs are sent to the default Active Job queue (see `config.active_job.default_queue_name`).
+
+ ```ruby
+ config.active_storage.queues.purge = :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`
+
+### Results of `load_defaults`
+
+#### With '5.0':
+
+- `config.action_controller.per_form_csrf_tokens`: `true`
+- `config.action_controller.forgery_protection_origin_check`: `true`
+- `ActiveSupport.to_time_preserves_timezone`: `true`
+- `config.active_record.belongs_to_required_by_default`: `true`
+- `config.ssl_options`: `{ hsts: { subdomains: true } }`
+
+#### With '5.1':
+
+- `config.assets.unknown_asset_fallback`: `false`
+- `config.action_view.form_with_generates_remote_forms`: `true`
+
+#### With '5.2':
+
+- `config.active_record.cache_versioning`: `true`
+- `action_dispatch.use_authenticated_cookie_encryption`: `true`
+- `config.active_support.use_authenticated_message_encryption`: `true`
+- `config.active_support.use_sha1_digests`: `true`
+- `config.action_controller.default_protect_from_forgery`: `true`
+- `config.action_view.form_with_generates_ids`: `true`
+
+#### With '6.0':
+
+- `config.action_view.default_enforce_utf8`: `false`
+- `config.action_dispatch.use_cookies_with_metadata`: `true`
+- `config.action_mailer.delivery_job`: `"ActionMailer::MailDeliveryJob"`
+- `config.active_job.return_false_on_aborted_enqueue`: `true`
+- `config.active_storage.queues.analysis`: `:active_storage_analysis`
+- `config.active_storage.queues.purge`: `:active_storage_purge`
+
### 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`.
@@ -721,8 +969,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']`.
@@ -738,8 +994,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.
@@ -754,8 +1018,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.
@@ -784,16 +1056,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
@@ -801,7 +1073,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
@@ -815,12 +1097,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:
@@ -843,9 +1126,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:
@@ -876,10 +1159,10 @@ 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)
+### Deploy to a subdirectory (relative URL root)
By default Rails expects that your application is running at the root
(eg. `/`). This section explains how to run your application inside a directory.
@@ -898,7 +1181,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
@@ -906,17 +1189,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 {
@@ -938,7 +1221,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
@@ -958,9 +1241,11 @@ Using Initializer Files
After loading the framework and any gems in your application, Rails turns to loading initializers. An initializer is any Ruby file stored under `config/initializers` in your application. You can use initializers to hold configuration settings that should be made after all of the frameworks and gems are loaded, such as options to configure settings for these parts.
+NOTE: There is no guarantee that your initializers will run after all the gem initializers, so any initialization code that depends on a given gem having been initialized should go into a `config.after_initialize` block.
+
NOTE: You can use subfolders to organize your initializers if you like, because Rails will look into the whole file hierarchy from the initializers folder on down.
-TIP: 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
---------------------
@@ -1023,110 +1308,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.
+* `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_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.
+* `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.
+* `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.
+* `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.
+* `bootstrap_hook`: Runs all configured `before_initialize` blocks.
-* `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.
+* `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.
-* `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.
+* `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.
-* `active_support.initialize_time_zone` Sets the default time zone for the application based on the `config.time_zone` setting, which defaults to "UTC".
+* `active_support.initialize_time_zone`: Sets the default time zone for the application based on the `config.time_zone` setting, which defaults to "UTC".
-* `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`.
+* `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`.
-* `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.
+* `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.
-* `action_dispatch.configure` Configures the `ActionDispatch::Http::URL.tld_length` to be set to the value of `config.action_dispatch.tld_length`.
+* `action_dispatch.configure`: Configures the `ActionDispatch::Http::URL.tld_length` to be set to the value of `config.action_dispatch.tld_length`.
-* `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_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.assets_config` Initializes the `config.actions_controller.assets_dir` to the app's public directory if not explicitly configured
+* `action_controller.assets_config`: Initializes the `config.actions_controller.assets_dir` to the app's public directory if not explicitly configured.
-* `action_controller.set_helpers_path` Sets Action Controller's helpers_path to the application's helpers_path
+* `action_controller.set_helpers_path`: Sets Action Controller's `helpers_path` to the application's `helpers_path`.
-* `action_controller.parameters_config` Configures strong parameters options for `ActionController::Parameters`
+* `action_controller.parameters_config`: Configures strong parameters options for `ActionController::Parameters`.
-* `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_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_controller.compile_config_methods` Initializes methods for the config settings specified so that they are quicker to access.
+* `action_controller.compile_config_methods`: Initializes methods for the config settings specified so that they are quicker to access.
-* `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.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.logger` Sets `ActiveRecord::Base.logger` - if it's not already set - to `Rails.logger`.
+* `active_record.logger`: Sets `ActiveRecord::Base.logger` - if it's not already set - to `Rails.logger`.
-* `active_record.migration_error` Configures middleware to check for pending migrations
+* `active_record.migration_error`: Configures middleware to check for pending migrations.
-* `active_record.check_schema_cache_dump` Loads the schema cache dump if configured and available
+* `active_record.check_schema_cache_dump`: Loads the schema cache dump if configured and available.
-* `active_record.warn_on_records_fetched_greater_than` Enables warnings when queries return large numbers of records
+* `active_record.warn_on_records_fetched_greater_than`: Enables warnings when queries return large numbers of records.
-* `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_reloader_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_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 paths specified by `config.load_paths` and all autoload 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`, `config.eager_load_paths` and `config.autoload_once_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
----------------
@@ -1141,7 +1426,7 @@ 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. The database connection pool is initially empty. As demand for connections increases it will create them 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. 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.
@@ -1162,21 +1447,25 @@ NOTE. If you are running in a multi-threaded environment, there could be a chanc
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.hi`), and just
+`config` for _single level_ configuration (ex: `config.hello`).
```ruby
- config.payment_processing.schedule = :daily
- config.payment_processing.retries = 3
+ config.x.payment_processing.schedule = :daily
+ config.x.payment_processing.retries = 3
config.super_debugger = true
```
These configuration points are then available through the configuration object:
```ruby
- Rails.configuration.payment_processing.schedule # => :daily
- Rails.configuration.payment_processing.retries # => 3
- Rails.configuration.super_debugger # => true
- Rails.configuration.super_debugger.not_set # => nil
+ Rails.configuration.x.payment_processing.schedule # => :daily
+ Rails.configuration.x.payment_processing.retries # => 3
+ 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:
@@ -1210,13 +1499,13 @@ 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: *
@@ -1231,18 +1520,18 @@ 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:
+`false`:
```ruby
group :development do
- gem 'listen', '~> 3.0.4'
+ 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 Mac OS X no additional gems are needed, but some are required
+On Linux and macOS no additional gems are needed, but some are required
[for *BSD](https://github.com/guard/listen#on-bsd) and
[for Windows](https://github.com/guard/listen#on-windows).
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index 12d0280116..569f52652f 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,19 +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/).
+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](https://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
@@ -37,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`).
@@ -51,21 +53,21 @@ You can then share your executable test case as a [gist](https://gist.github.com
### Special Treatment for Security Issues
-WARNING: Please do not report security vulnerabilities with public GitHub issue reports. The [Rails security policy page](http://rubyonrails.org/security) details the procedure to follow for security issues.
+WARNING: Please do not report security vulnerabilities with public GitHub issue reports. The [Rails security policy page](https://rubyonrails.org/security) details the procedure to follow for security issues.
### What about Feature Requests?
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.
@@ -82,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
@@ -90,19 +94,19 @@ 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 https://github.com/JohnSmith/rails.git
@@ -128,37 +132,27 @@ 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 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 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.
+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.
-Docrails is merged with master regularly, so you are effectively editing the Ruby on Rails documentation.
-
-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.
+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.
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).
-NOTE: As explained earlier, ordinary code patches should have proper documentation coverage. Docrails is only used for isolated documentation improvements.
-
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.
-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.
-
Translating Rails Guides
------------------------
-We are happy to have people volunteer to translate the Rails guides into their own language.
-If you want to translate the Rails guides in your own language, follows these steps:
+We are happy to have people volunteer to translate the Rails guides. Just follow these steps:
-* Fork the project (rails/rails).
+* 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.
+
To generate the guides in HTML format cd into the *guides* directory then run (eg. for it-IT):
```bash
@@ -173,11 +167,11 @@ NOTE: The instructions are for Rails > 4. The Redcarpet Gem doesn't work with JR
Translation efforts we know about (various versions):
* **Italian**: [https://github.com/rixlabs/docrails](https://github.com/rixlabs/docrails)
-* **Spanish**: [http://wiki.github.com/gramos/docrails](http://wiki.github.com/gramos/docrails)
-* **Polish**: [http://github.com/apohllo/docrails/tree/master](http://github.com/apohllo/docrails/tree/master)
-* **French** : [http://github.com/railsfrance/docrails](http://github.com/railsfrance/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/tree/master](https://github.com/ujk/docrails/tree/master)
+* **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)
@@ -189,11 +183,11 @@ 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
@@ -245,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
@@ -260,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
@@ -328,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:
@@ -337,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:
@@ -387,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:
@@ -411,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*
```
@@ -423,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:
@@ -506,18 +527,10 @@ 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 https://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 https://github.com/rails/rails.git
@@ -534,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
-```
-
-If you want to update another branch:
-
-```bash
-$ git checkout branch_name
-$ git rebase rails/branch_name
-$ git push origin branch_name
+$ git push fork master
+$ git push fork my_new_branch
```
-
### Issue a Pull Request
Navigate to the Rails repository you just pushed to (e.g.
@@ -580,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
@@ -600,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
@@ -638,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
```
@@ -662,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
@@ -677,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
```
@@ -690,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](https://contributors.rubyonrails.org).
diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb
deleted file mode 100644
index 1d995581fa..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 a Rails developer at <a href="http://marketplacer.com">Marketplacer</a> and has been working with Rails since 2006. He's the author of <a href="https://leanpub.com/multi-tenancy-rails">Multi Tenancy With Rails</a> and co-author of <a href="http://manning.com/bigg2">Rails 4 in Action</a>. He's written many gems which can be seen on <a href="https://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>.
-<% end %>
-
-<%= author('Oscar Del Ben', 'oscardelben', 'oscardelben.jpg') do %>
-Oscar Del Ben is a software engineer at <a href="http://www.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 877c87e9fa..77513c3a84 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
============================
@@ -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|unknown)` 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
@@ -312,16 +350,14 @@ For example:
```bash
=> Booting Puma
-=> Rails 5.0.0 application starting in development on http://0.0.0.0:3000
+=> Rails 5.1.0 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
-* Version 3.0.2 (ruby 2.3.0-p0), codename: Plethora of Penguin Pinatas
+* 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
@@ -337,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)
```
@@ -347,11 +382,45 @@ by asking the debugger for help. Type: `help`
```
(byebug) help
- h[elp][ <cmd>[ <subcmd>]]
+ 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
- help -- prints this help.
- help <cmd> -- prints help on command <cmd>.
- help <cmd> <subcmd> -- prints help on <cmd>'s subcommand <subcmd>.
+(byebug)
```
To see the previous ten lines you should type `list-` (or `l-`).
@@ -369,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=
@@ -391,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)
```
@@ -413,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 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.
@@ -492,9 +558,9 @@ current context:
12: format.json { render json: @articles }
(byebug) instance_variables
-[:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request,
- :@_response, :@_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
@@ -504,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
@@ -523,29 +590,35 @@ And then ask again for the instance_variables:
```
(byebug) instance_variables
-[:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request,
- :@_response, :@_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
@@ -563,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.
```
@@ -581,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
@@ -591,32 +664,23 @@ 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, 6] in /PathToProject/app/models/article.rb
1: class Article < ApplicationRecord
- 2:
- 3: def self.find_recent(limit = 10)
- 4: byebug
-=> 5: where('created_at > ?', 1.week.ago).limit(limit)
- 6: end
- 7:
- 8: end
+ 2: def self.find_recent(limit = 10)
+ 3: byebug
+=> 4: where('created_at > ?', 1.week.ago).limit(limit)
+ 5: end
+ 6: end
(byebug)
```
@@ -628,11 +692,7 @@ method.
```
(byebug) next
-
-Next advances to the next line (line 6: `end`), which returns to the next line
-of the caller method:
-
-[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,23 +713,24 @@ 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
@@ -678,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
@@ -703,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.
@@ -728,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
@@ -746,24 +807,22 @@ 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
@@ -777,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>
-You can see the full list by using `help set`. Use `help set _subcommand_` to
-learn about a particular `set` command.
+ 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 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
```
@@ -831,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.
@@ -901,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 cc24e6f666..e84e5561f2 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,270 +19,240 @@ 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.
-* [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 everything through ports (these packages are located under the
+`databases` folder).
-Active Record's test suite runs three times: once for SQLite3, once for MySQL, and once for PostgreSQL. We are going to see now how to set up the environment for them.
+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 libmysqlclient-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
+$ 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 03943d0f25..25e4fdb4e6 100644
--- a/guides/source/documents.yaml
+++ b/guides/source/documents.yaml
@@ -65,29 +65,50 @@
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.
+ description: This guide describes how to use Action Mailer to send emails.
+ -
+ name: Action Mailbox Basics
+ work_in_progress: true
+ url: action_mailbox_basics.html
+ description: This guide describes how to use Action Mailbox to receive emails.
+ -
+ name: Action Text Overview
+ work_in_progress: true
+ url: action_text_overview.html
+ description: This guide describes how to use Action Text to handle rich text content.
-
name: Active Job Basics
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 +122,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.
-
@@ -131,11 +152,6 @@
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.
@@ -153,7 +169,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).
-
@@ -161,8 +177,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
@@ -173,11 +194,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
@@ -191,34 +212,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 eafac4828c..0b17137270 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
@@ -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
@@ -312,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:
@@ -345,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
@@ -423,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 `bin/rails 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
@@ -457,7 +464,7 @@ 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
@@ -465,7 +472,7 @@ From the application root, run the model generator. Tell it to generate a
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:
@@ -485,7 +492,7 @@ called `Blorgh::Comment`. Now run the migration to create our blorgh_comments
table:
```bash
-$ bin/rails db:migrate
+$ rails db:migrate
```
To show the comments on an article, edit `app/views/blorgh/articles/show.html.erb` and
@@ -533,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 %>
```
@@ -559,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:
@@ -649,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
@@ -691,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 run the following command from the `test/dummy` directory of your Rails engine:
+application run the following command from the application's root:
```bash
-$ bin/rails blorgh:install:migrations
+$ rails blorgh:install:migrations
```
If you have multiple engines that need migrations copied over, use
`railties:install:migrations` instead:
```bash
-$ bin/rails railties:install:migrations
+$ rails railties:install:migrations
```
This command, when run for the first time, will copy over all the migrations
@@ -719,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 `bin/rails
+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
@@ -730,14 +737,14 @@ If you would like to run migrations only from one engine, you can do it by
specifying `SCOPE`:
```bash
-bin/rails 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
-bin/rails db:migrate SCOPE=blorgh VERSION=0
+rails db:migrate SCOPE=blorgh VERSION=0
```
### Using a Class Provided by the Application
@@ -764,7 +771,7 @@ application:
rails g model user name:string
```
-The `bin/rails 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
@@ -779,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>
```
@@ -824,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
@@ -836,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
-$ bin/rails blorgh:install:migrations
+$ rails blorgh:install:migrations
```
Notice that only _one_ migration was copied over here. This is because the first
@@ -851,7 +858,7 @@ Copied migration [timestamp]_add_author_id_to_blorgh_articles.blorgh.rb from blo
Run the migration using:
```bash
-$ bin/rails db:migrate
+$ rails db:migrate
```
Now with all the pieces in place, an action will take place that will associate
@@ -917,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`.
@@ -978,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
@@ -994,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
@@ -1016,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
@@ -1084,16 +1091,15 @@ main Rails application.
Engine model and controller classes can be extended by open classing them in the
main Rails application (since model and controller classes are just Ruby classes
that inherit Rails specific functionality). Open classing an Engine class
-redefines it for use in the main application. This is usually implemented by
-using the decorator pattern.
+redefines it for use in the main application.
For simple class modifications, use `Class#class_eval`. For complex class
modifications, consider using `ActiveSupport::Concern`.
-#### A note on Decorators and Loading Code
+#### A note on Overriding and Loading Code
-Because these decorators are not referenced by your Rails application itself,
-Rails' autoloading system will not kick in and load your decorators. This means
+Because these overrides are not referenced by your Rails application itself,
+Rails' autoloading system will not kick in and load your overrides. This means
that you need to require them yourself.
Here is some sample code to do this:
@@ -1105,7 +1111,7 @@ module Blorgh
isolate_namespace Blorgh
config.to_prepare do
- Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
+ Dir.glob(Rails.root + "app/overrides/**/*_override*.rb").each do |c|
require_dependency(c)
end
end
@@ -1113,15 +1119,15 @@ module Blorgh
end
```
-This doesn't apply to just Decorators, but anything that you add in an engine
+This doesn't apply to just overrides, but anything that you add in an engine
that isn't referenced by your main application.
-#### Implementing Decorator Pattern Using Class#class_eval
+#### Reopening existing classes using Class#class_eval
**Adding** `Article#time_since_created`:
```ruby
-# MyApp/app/decorators/models/blorgh/article_decorator.rb
+# MyApp/app/overrides/models/blorgh/article_override.rb
Blorgh::Article.class_eval do
def time_since_created
@@ -1142,7 +1148,7 @@ end
**Overriding** `Article#summary`:
```ruby
-# MyApp/app/decorators/models/blorgh/article_decorator.rb
+# MyApp/app/overrides/models/blorgh/article_override.rb
Blorgh::Article.class_eval do
def summary
@@ -1162,11 +1168,11 @@ class Article < ApplicationRecord
end
```
-#### Implementing Decorator Pattern Using ActiveSupport::Concern
+#### Reopening existing classes using ActiveSupport::Concern
Using `Class#class_eval` is great for simple adjustments, but for more complex
class modifications, you might want to consider using [`ActiveSupport::Concern`]
-(http://api.rubyonrails.org/classes/ActiveSupport/Concern.html).
+(https://api.rubyonrails.org/classes/ActiveSupport/Concern.html).
ActiveSupport::Concern manages load order of interlinked dependent modules and
classes at run time allowing you to significantly modularize your code.
@@ -1318,7 +1324,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
@@ -1357,14 +1363,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 `bin/rails 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
```
@@ -1410,3 +1416,124 @@ 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` |
+| `ActionCable::Channel::Base` | `action_cable_channel` |
+| `ActionCable::Connection::Base` | `action_cable_connection` |
+| `ActionController::API` | `action_controller_api` |
+| `ActionController::API` | `action_controller` |
+| `ActionController::Base` | `action_controller_base` |
+| `ActionController::Base` | `action_controller` |
+| `ActionController::TestCase` | `action_controller_test_case` |
+| `ActionDispatch::IntegrationTest` | `action_dispatch_integration_test` |
+| `ActionDispatch::SystemTestCase` | `action_dispatch_system_test_case` |
+| `ActionMailbox::Base` | `action_mailbox` |
+| `ActionMailbox::InboundEmail` | `action_mailbox_inbound_email` |
+| `ActionMailbox::TestCase` | `action_mailbox_test_case` |
+| `ActionMailer::Base` | `action_mailer` |
+| `ActionMailer::TestCase` | `action_mailer_test_case` |
+| `ActionText::Content` | `action_text_content` |
+| `ActionText::RichText` | `action_text_rich_text` |
+| `ActionView::Base` | `action_view` |
+| `ActionView::TestCase` | `action_view_test_case` |
+| `ActiveJob::Base` | `active_job` |
+| `ActiveJob::TestCase` | `active_job_test_case` |
+| `ActiveRecord::Base` | `active_record` |
+| `ActiveStorage::Attachment` | `active_storage_attachment` |
+| `ActiveStorage::Blob` | `active_storage_blob` |
+| `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 048fe190e8..6418005921 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.
@@ -17,32 +17,30 @@ After reading this guide, you will know:
--------------------------------------------------------------------------------
-NOTE: This guide is not intended to be a complete documentation of available form helpers and their arguments. Please visit [the Rails API documentation](http://api.rubyonrails.org/) for a complete reference.
+NOTE: This guide is not intended to be a complete documentation of available form helpers and their arguments. Please visit [the Rails API documentation](https://api.rubyonrails.org/) for a complete reference.
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 an `input` element with type `hidden`. This `input` is important, because the form cannot be successfully submitted without it. The hidden input element with the name `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their action is "GET" or "POST".
-
-The second 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 [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
@@ -53,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") %>
@@ -66,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.
-
-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: 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.
-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:
+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.
-```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
@@ -110,7 +89,7 @@ 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](https://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html).
#### Checkboxes
@@ -142,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:
@@ -151,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"`.
@@ -164,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") %>
@@ -208,14 +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 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
--------------------------
@@ -233,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.
@@ -244,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`:
@@ -254,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" %>
@@ -267,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 %>
@@ -299,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
@@ -315,62 +299,59 @@ The Article model is directly available to users of the application, so - follow
resources :articles
```
-TIP: Declaring a resource has a number of side effects. See [Rails Routing From the Outside In](routing.html#resource-routing-the-rails-default) for more information on setting up and using resources.
+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>
@@ -378,6 +359,8 @@ output:
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
-----------------------------
@@ -389,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>
```
@@ -401,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.
@@ -427,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.
@@ -478,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
@@ -509,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) %>
@@ -518,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`](https://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](https://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 %>
@@ -558,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)
@@ -583,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
@@ -598,72 +604,64 @@ When this is passed to `Person.new` (or `update`), Active Record spots that thes
### Common Options
-Both families of helpers use the same core set of functions to generate the individual select tags and so both accept largely the same options. In particular, by default Rails will generate year options 5 years either side of the current year. If this is not an appropriate range, the `:start_year` and `:end_year` options override this. For an exhaustive list of the available options, refer to the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html).
+Both families of helpers use the same core set of functions to generate the individual select tags and so both accept largely the same options. In particular, by default Rails will generate year options 5 years either side of the current year. If this is not an appropriate range, the `:start_year` and `:end_year` options override this. For an exhaustive list of the available options, refer to the [API documentation](https://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html).
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`](https://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`](https://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 %>
```
@@ -671,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 %>
```
@@ -686,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
```
@@ -701,13 +699,12 @@ 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.
@@ -754,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 %>
@@ -785,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" />
@@ -810,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 %>
```
@@ -818,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| %>
@@ -836,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 %>
```
@@ -847,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 %>
```
@@ -871,7 +856,7 @@ 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
@@ -879,7 +864,7 @@ Active Record provides model level support via the `accepts_nested_attributes_fo
```ruby
class Person < ApplicationRecord
- has_many :addresses
+ has_many :addresses, inverse_of: :person
accepts_nested_attributes_for :addresses
end
@@ -888,14 +873,14 @@ class Address < ApplicationRecord
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| %>
@@ -918,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
```
@@ -946,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
@@ -977,17 +962,17 @@ class Person < ApplicationRecord
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 %>
...
@@ -997,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
@@ -1022,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 a615751eb5..e486c53fe3 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,16 +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 by default. 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
@@ -46,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.
@@ -68,7 +59,7 @@ 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
----------------------------
@@ -86,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:
```bash
$ ruby -v
-ruby 2.3.0p0
+ruby 2.5.0
```
-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/).
+Rails requires Ruby version 2.5.0 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
@@ -127,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
@@ -148,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`.
@@ -164,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, update, 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.|
+|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!
-------------
@@ -192,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`
@@ -201,8 +208,8 @@ 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
@@ -216,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
@@ -245,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.
@@ -264,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
```
@@ -298,15 +303,12 @@ Open the file `config/routes.rb` in your editor.
Rails.application.routes.draw do
get 'welcome/index'
- # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
-
- # Serve websocket cable requests in-process
- # mount ActionCable.server => '/cable'
+ # 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.
Edit this file by adding the line of code `root 'welcome#index'`.
@@ -316,10 +318,6 @@ It should look something like the following:
Rails.application.routes.draw do
get 'welcome/index'
- # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
-
- # Serve websocket cable requests in-process
- # mount ActionCable.server => '/cable'
root 'welcome#index'
end
```
@@ -328,9 +326,9 @@ end
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`
@@ -341,13 +339,13 @@ 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
@@ -356,6 +354,7 @@ resource. You need to add the _article resource_ to the
```ruby
Rails.application.routes.draw do
+ get 'welcome/index'
resources :articles
@@ -363,23 +362,24 @@ Rails.application.routes.draw do
end
```
-If you run `bin/rails 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/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
+$ 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
@@ -391,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
@@ -407,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`
@@ -459,40 +459,33 @@ 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 request formats: text/html
-That's quite a lot of text! Let's quickly go through and understand what each
-part of it means.
+>NOTE!
+>Unless told otherwise, Rails expects an action to render a template with the same name, contained in a folder named after its controller. If this controller is an API responding with 204 (No Content), which does not require a template, then this error will occur when trying to access it via browser, since we expect an HTML template to be rendered for such requests. If that's the case, carry on.
-The first part identifies which template is missing. In this case, it's the
+The message identifies which template is missing. In this case, it's the
`articles/new` template. Rails will first look for this template. If not found,
-then it will attempt to load a template called `application/new`. It looks for
-one here because the `ArticlesController` inherits from `ApplicationController`.
-
-The next part of the message contains 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.
+then it will attempt to load a template called `application/new`, because the
+`ArticlesController` inherits from `ApplicationController`.
+
+Next the message contains `request.formats` which specifies the format of
+template to be served in response. It is set to `text/html` as we requested
+this page via browser, so Rails is looking for an HTML template.
The simplest template that would work in this case would be one located at
`app/views/articles/new.html.erb`. The extension of this file name is important:
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.
@@ -505,43 +498,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`
@@ -550,33 +543,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/rails routes`:
+`rails routes`:
```bash
-$ bin/rails 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
@@ -595,6 +589,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
@@ -611,9 +609,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
@@ -635,8 +635,7 @@ 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
<ActionController::Parameters {"title"=>"First Article!", "text"=>"This is my first article."} permitted: false>
@@ -654,7 +653,7 @@ 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 an `Article` model, together
@@ -673,7 +672,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
@@ -684,7 +683,7 @@ 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[5.0]
+class CreateArticles < ActiveRecord::Migration[6.0]
def change
create_table :articles do |t|
t.string :title
@@ -703,13 +702,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 bin/rails command to run the migration:
+At this point, you can use a rails command to run the migration:
```bash
-$ bin/rails db:migrate
+$ rails db:migrate
```
Rails will execute this migration command and tell you it created the Articles
@@ -726,7 +725,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/rails db:migrate RAILS_ENV=production`.
+invoking the command: `rails db:migrate RAILS_ENV=production`.
### Saving data in the controller
@@ -775,10 +774,11 @@ 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))
@@ -805,7 +805,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
@@ -813,7 +813,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/rails 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:
```
@@ -830,7 +830,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:
@@ -875,7 +875,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/rails routes` is:
+The route for this as per output of `rails routes` is:
```
articles GET /articles(.:format) articles#index
@@ -912,6 +912,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| %>
@@ -957,7 +958,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 %>
@@ -1068,7 +1069,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">
@@ -1085,17 +1086,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 %>
@@ -1116,10 +1117,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>:
@@ -1158,9 +1159,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">
@@ -1177,17 +1178,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 %>
@@ -1198,16 +1199,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]
+(https://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`.
@@ -1304,7 +1306,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">
@@ -1321,29 +1323,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]
+(https://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:
@@ -1370,7 +1372,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/rails routes` is:
+deleting articles as per output of `rails routes` is:
```ruby
DELETE /articles/:id(.:format) articles#destroy
@@ -1490,17 +1492,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
@@ -1516,11 +1518,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:
@@ -1546,14 +1548,14 @@ 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. You can get a better understanding after analyzing the
-`db/schema.rb` file below.
+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[5.0]
+class CreateComments < ActiveRecord::Migration[6.0]
def change
create_table :comments do |t|
t.string :commenter
@@ -1571,7 +1573,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/rails db:migrate
+$ rails db:migrate
```
Rails is smart enough to only execute the migrations that have not already been
@@ -1647,10 +1649,10 @@ 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:
+This creates four files and one empty directory:
| File/Directory | Purpose |
| -------------------------------------------- | ---------------------------------------- |
@@ -1658,8 +1660,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
@@ -1682,17 +1683,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 %>
@@ -1701,7 +1702,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`:
@@ -1763,17 +1764,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 %>
@@ -1829,17 +1830,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 %>
@@ -1851,7 +1852,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
@@ -1859,17 +1860,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 %>
```
@@ -2054,13 +2055,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 0edfa072f8..dab73bfbc2 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:
@@ -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:
@@ -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,18 +107,17 @@ 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 `config/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 `config/application.rb`:
@@ -125,23 +128,31 @@ 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
```
-### Managing the Locale across Requests
+Note that appending directly to `I18n.load_paths` instead of to the application's configured i18n will _not_ override translations from external gems.
-The default locale is used for all translations unless `I18n.locale` is explicitly set.
+### Managing the Locale across Requests
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 default locale is used for all translations unless `I18n.locale=` or `I18n.with_locale` is used.
+
+`I18n.locale` can leak into subsequent requests served by the same thread/process if it is not consistently set in every controller. For example executing `I18n.locale = :es` in one POST requests will have effects for all later requests to controllers that don't set the locale, but only in that particular thread/process. For that reason, instead of `I18n.locale =` you can use `I18n.with_locale` which does not have this leak issue.
+
+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 +172,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,13 +216,13 @@ 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.
Getting the locale from `params` and setting it accordingly is not hard; including it in every URL and thus **passing it through the requests** is. To include an explicit option in every URL, e.g. `link_to(books_url(locale: I18n.locale))`, would be tedious and probably impossible, of course.
-Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its [`ApplicationController#default_url_options`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Base.html#method-i-default_url_options), which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for) and helper methods dependent on it (by implementing/overriding `default_url_options`).
+Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its [`ApplicationController#default_url_options`](https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Base.html#method-i-default_url_options), which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](https://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for) and helper methods dependent on it (by implementing/overriding `default_url_options`).
We can include something like this in our `ApplicationController` then:
@@ -225,7 +237,7 @@ Every helper method dependent on `url_for` (e.g. helpers for named routes like `
You may be satisfied with this. It does impact the readability of URLs, though, when the locale "hangs" at the end of every URL in your application. Moreover, from the architectural standpoint, locale is usually hierarchically above the other parts of the application domain: and URLs should reflect this.
-You probably want URLs to look like this: `http://www.example.com/en/books` (which loads the English locale) and `http://www.example.com/nl/books` (which loads the Dutch locale). This is achievable with the "over-riding `default_url_options`" strategy from above: you just have to set up your routes with [`scope`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Scoping.html):
+You probably want URLs to look like this: `http://www.example.com/en/books` (which loads the English locale) and `http://www.example.com/nl/books` (which loads the Dutch locale). This is achievable with the "over-riding `default_url_options`" strategy from above: you just have to set up your routes with [`scope`](https://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Scoping.html):
```ruby
# config/routes.rb
@@ -267,8 +279,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 +298,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
- logger.debug "* Locale set to '#{I18n.locale}'"
+ locale = extract_locale_from_accept_language_header
+ logger.debug "* Locale set to '#{locale}'"
+ I18n.with_locale(locale, &action)
end
private
@@ -306,10 +322,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 +343,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 +385,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 +423,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,7 +516,7 @@ 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>
+<h1><%= t :hello_world %></h1>
<p><%= flash[:notice] %></p>
<p><%= l Time.now, format: :short %></p>
```
@@ -549,7 +596,7 @@ You should have a good understanding of using the i18n library now and know how
to internationalize a basic Rails application. In the following chapters, we'll
cover its features in more depth.
-These chapters will show examples using both the `I18n.translate` method as well as the [`translate` view helper method](http://api.rubyonrails.org/classes/ActionView/Helpers/TranslationHelper.html#method-i-translate) (noting the additional feature provide by the view helper method).
+These chapters will show examples using both the `I18n.translate` method as well as the [`translate` view helper method](https://api.rubyonrails.org/classes/ActionView/Helpers/TranslationHelper.html#method-i-translate) (noting the additional feature provide by the view helper method).
Covered are features like these:
@@ -561,7 +608,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 +674,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 +735,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 +752,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 +861,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 +877,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 +889,12 @@ In the event you need to access nested attributes within a given model, you shou
en:
activerecord:
attributes:
- user/gender:
- female: "Female"
- male: "Male"
+ user/role:
+ admin: "Admin"
+ contributor: "Contributor"
```
-Then `User.human_attribute_name("gender.female")` will return "Female".
+Then `User.human_attribute_name("role.admin")` will return "Admin".
NOTE: If you are using a class which includes `ActiveModel` and does not inherit from `ActiveRecord::Base`, replace `activerecord` with `activemodel` in the above key paths.
@@ -867,7 +958,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}"`.
@@ -889,6 +980,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 |
@@ -919,7 +1011,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
@@ -992,7 +1084,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: {
@@ -1041,13 +1133,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)
@@ -1108,28 +1198,37 @@ To do so, the helper forces `I18n#translate` to raise exceptions no matter what
I18n.t :foo, raise: true # always re-raises exceptions from the backend
```
+Translating Model Content
+-------------------------
+
+The I18n API described in this guide is primarily intended for translating interface strings. If you are looking to translate model content (e.g. blog posts), you will need a different solution to help with this.
+
+Several gems can help with this:
+
+* [Globalize](https://github.com/globalize/globalize): Store translations on separate translation tables, one for each translated model
+* [Mobility](https://github.com/shioyama/mobility): Provides support for storing translations in many formats, including translation tables, json columns (Postgres), etc.
+* [Traco](https://github.com/barsoom/traco): Translatable columns for Rails 3 and 4, stored in the model table itself
+
Conclusion
----------
At this point you should have a good overview about how I18n support in Ruby on Rails works and are ready to start translating your project.
-If you want to discuss certain portions or have questions, please sign up to the [rails-i18n mailing list](http://groups.google.com/group/rails-i18n).
-
Contributing to Rails I18n
--------------------------
I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in gems and real applications first, and only then cherry-picking the best-of-breed of most widely useful features for inclusion in the core.
-Thus we encourage everybody to experiment with new ideas and features in gems or other libraries and make them available to the community. (Don't forget to announce your work on our [mailing list](http://groups.google.com/group/rails-i18n!))
+Thus we encourage everybody to experiment with new ideas and features in gems or other libraries and make them available to the community. (Don't forget to announce your work on our [mailing list](https://groups.google.com/forum/#!forum/rails-i18n)!)
-If you find your own locale (language) missing from our [example translations data](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) repository for Ruby on Rails, please [_fork_](https://github.com/guides/fork-a-project-and-submit-your-modifications) the repository, add your data and send a [pull request](https://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.
+* [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.
@@ -1143,7 +1242,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..10e388774c 100644
--- a/guides/source/index.html.erb
+++ b/guides/source/index.html.erb
@@ -1,6 +1,5 @@
-<% content_for :page_title do %>
-Ruby on Rails Guides
-<% end %>
+<% content_for :page_title, "Ruby on Rails Guides" %>
+<% content_for :description, "Ruby on Rails Guides" %>
<% content_for :header_section do %>
<%= render 'welcome' %>
@@ -10,7 +9,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 89e5346d86..c41eae18cf 100644
--- a/guides/source/initialization.md
+++ b/guides/source/initialization.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 Initialization Process
================================
@@ -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,36 +86,36 @@ The `APP_PATH` constant will be used later in `rails/commands`. The `config/boot
`config/boot.rb` contains:
```ruby
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
```
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
@@ -131,7 +131,7 @@ Once `config/boot.rb` has finished, the next file that is required is
`ARGV` array simply contains `server` which will be passed over:
```ruby
-ARGV << '--help' if ARGV.empty?
+require_relative "command"
aliases = {
"g" => "generate",
@@ -146,33 +146,37 @@ aliases = {
command = ARGV.shift
command = aliases[command] || command
-require 'rails/commands/commands_tasks'
-
-Rails::CommandsTasks.new(ARGV).run_command!(command)
+Rails::Command.invoke command, ARGV
```
-TIP: As you can see, an empty ARGV list will make Rails show the help
-snippet.
-
If we had used `s` rather than `server`, Rails would have used the `aliases`
defined here to find the matching command.
-### `rails/commands/commands_tasks.rb`
+### `rails/command.rb`
-When one types a valid Rails command, `run_command!` a method of the same name
-is called. If Rails doesn't recognize the command, it tries to run a Rake task
-of the same name.
+When one types a Rails command, `invoke` tries to lookup a command for the given
+namespace and executes the command if found.
-```ruby
-COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole application runner new version help)
+If Rails doesn't recognize the command, it hands the reins over to Rake
+to run a task of the same name.
+
+As shown, `Rails::Command` displays the help output automatically if the `args`
+are empty.
-def run_command!(command)
- command = parse_command(command)
+```ruby
+module Rails::Command
+ class << self
+ def invoke(namespace, args = [], **config)
+ namespace = namespace.to_s
+ namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace)
+ namespace = "version" if %w( -v --version ).include? namespace
- if COMMAND_WHITELIST.include?(command)
- send(command)
- else
- run_rake_task(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
```
@@ -180,53 +184,39 @@ end
With the `server` command, Rails will further run the following code:
```ruby
-def set_application_directory!
- Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
-end
-
-def server
- set_application_directory!
- require_command!("server")
-
- Rails::Server.new.tap do |server|
- # We need to require application after the server sets environment,
- # otherwise the --environment option given to the server won't propagate.
- require APP_PATH
- Dir.chdir(Rails.application.root)
- server.start
+module Rails
+ module Command
+ class ServerCommand < Base # :nodoc:
+ def perform
+ set_application_directory!
+
+ Rails::Server.new.tap do |server|
+ # Require application after server sets environment to propagate
+ # the --environment option.
+ require APP_PATH
+ Dir.chdir(Rails.application.root)
+ server.start
+ end
+ end
+ end
end
end
-
-def require_command!(command)
- require "rails/commands/#{command}"
-end
```
This file will change into the Rails root directory (a path two directories up
from `APP_PATH` which points at `config/application.rb`), but only if the
-`config.ru` file isn't found. This then requires `rails/commands/server` which
-sets up the `Rails::Server` class.
-
-```ruby
-require 'fileutils'
-require 'optparse'
-require 'action_dispatch'
-require 'rails'
-
-module Rails
- class Server < ::Rack::Server
-```
-
-`fileutils` and `optparse` are standard Ruby libraries which provide helper functions for working with files and parsing options.
+`config.ru` file isn't found. This then starts up the `Rails::Server` class.
### `actionpack/lib/action_dispatch.rb`
Action Dispatch is the routing component of the Rails framework.
It adds functionality like routing, session, and common middlewares.
-### `rails/commands/server.rb`
+### `rails/commands/server/server_command.rb`
-The `Rails::Server` class is defined in this file by inheriting from `Rack::Server`. When `Rails::Server.new` is called, this calls the `initialize` method in `rails/commands/server.rb`:
+The `Rails::Server` class is defined in this file by inheriting from
+`Rack::Server`. When `Rails::Server.new` is called, this calls the `initialize`
+method in `rails/commands/server/server_command.rb`:
```ruby
def initialize(*)
@@ -252,7 +242,10 @@ end
In this case, `options` will be `nil` so nothing happens in this method.
-After `super` has finished in `Rack::Server`, we jump back to `rails/commands/server.rb`. At this point, `set_environment` is called within the context of the `Rails::Server` object and this method doesn't appear to do much at first glance:
+After `super` has finished in `Rack::Server`, we jump back to
+`rails/commands/server/server_command.rb`. At this point, `set_environment`
+is called within the context of the `Rails::Server` object and this method
+doesn't appear to do much at first glance:
```ruby
def set_environment
@@ -289,17 +282,15 @@ With the `default_options` set to this:
```ruby
def default_options
- environment = ENV['RACK_ENV'] || 'development'
- default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
-
- {
- :environment => environment,
- :pid => nil,
- :Port => 9292,
- :Host => default_host,
- :AccessLog => [],
- :config => "config.ru"
- }
+ super.merge(
+ Port: ENV.fetch("PORT", 3000).to_i,
+ Host: ENV.fetch("HOST", "localhost").dup,
+ DoNotReverseLookup: true,
+ environment: (ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development").dup,
+ daemonize: false,
+ caching: nil,
+ pid: Options::DEFAULT_PID_PATH,
+ restart_cmd: restart_command)
end
```
@@ -311,22 +302,25 @@ def opt_parser
end
```
-The class **is** defined in `Rack::Server`, but is overwritten in `Rails::Server` to take different arguments. Its `parse!` method begins like this:
+The class **is** defined in `Rack::Server`, but is overwritten in
+`Rails::Server` to take different arguments. Its `parse!` method looks
+like this:
```ruby
def parse!(args)
args, options = args.dup, {}
- opt_parser = OptionParser.new do |opts|
- opts.banner = "Usage: rails server [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`
@@ -345,6 +339,7 @@ def start
print_boot_information
trap(:INT) { exit }
create_tmp_directories
+ setup_dev_caching
log_to_stdout if options[:log_stdout]
super
@@ -352,7 +347,6 @@ def start
end
private
-
def print_boot_information
...
puts "=> Run `rails server -h` for more startup options"
@@ -364,21 +358,30 @@ private
end
end
+ def setup_dev_caching
+ if options[:environment] == "development"
+ Rails::DevCaching.enable_by_argument(options[:caching])
+ end
+ end
+
def log_to_stdout
wrapped_app # touch the app so the logger is set up
- console = ActiveSupport::Logger.new($stdout)
+ console = ActiveSupport::Logger.new(STDOUT)
console.formatter = Rails.logger.formatter
console.level = Rails.logger.level
- Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
+ unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDOUT)
+ Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
+ end
end
```
This is where the first output of the Rails initialization happens. This method
creates a trap for `INT` signals, so if you `CTRL-C` the server, it will exit the
process. As we can see from the code here, it will create the `tmp/cache`,
-`tmp/pids`, and `tmp/sockets` directories. It then calls `wrapped_app` which is
+`tmp/pids`, and `tmp/sockets` directories. It then enables caching in development
+if `rails server` is called with `--dev-caching`. Finally, it calls `wrapped_app` which is
responsible for creating the Rack app, before creating and assigning an instance
of `ActiveSupport::Logger`.
@@ -464,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 %>
```
@@ -485,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`
@@ -495,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`
@@ -503,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,6 +532,7 @@ require "rails"
%w(
active_record/railtie
+ active_storage/engine
action_controller/railtie
action_view/railtie
action_mailer/railtie
@@ -538,7 +542,7 @@ require "rails"
sprockets/railtie
).each do |railtie|
begin
- require "#{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/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 5094fea4ca..9b73bc9bea 100644
--- a/guides/source/kindle/toc.ncx.erb
+++ b/guides/source/kindle/toc.ncx.erb
@@ -30,10 +30,6 @@
</navLabel>
<content src="welcome.html"/>
</navPoint>
- <navPoint class="article" id="credits" playOrder="3">
- <navLabel><text>Credits</text></navLabel>
- <content src="credits.html"/>
- </navPoint>
<navPoint class="article" id="copyright" playOrder="4">
<navLabel><text>Copyright &amp; License</text></navLabel>
<content src="copyright.html"/>
diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb
index 6db76b528e..3ffd7ff1ac 100644
--- a/guides/source/layout.html.erb
+++ b/guides/source/layout.html.erb
@@ -1,20 +1,26 @@
<!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) %></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>
+ <meta property="og:title" content="<%= yield(:page_title) %>" />
+ <meta name="description" content="<%= yield(:description) %>" />
+ <meta property="og:description" content="<%= yield(:description) %>" />
+ <meta property="og:locale" content="en_US" />
+ <meta property="og:site_name" content="Ruby on Rails Guides" />
+ <meta property="og:image" content="https://avatars.githubusercontent.com/u/4223" />
+ <meta property="og:type" content="website" />
</head>
<body class="guide">
<% if @edge %>
@@ -24,15 +30,15 @@
<% end %>
<div id="topNav">
<div class="wrapper">
- <strong class="more-info-label">More at <a href="http://rubyonrails.org/">rubyonrails.org:</a> </strong>
+ <strong class="more-info-label">More at <a href="https://rubyonrails.org/">rubyonrails.org:</a> </strong>
<span class="red-button more-info-button">
More Ruby on Rails
</span>
<ul class="more-info-links s-hidden">
- <li class="more-info"><a href="http://weblog.rubyonrails.org/">Blog</a></li>
- <li class="more-info"><a href="http://guides.rubyonrails.org/">Guides</a></li>
- <li class="more-info"><a href="http://api.rubyonrails.org/">API</a></li>
- <li class="more-info"><a href="http://stackoverflow.com/questions/tagged/ruby-on-rails">Ask for help</a></li>
+ <li class="more-info"><a href="https://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="https://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>
@@ -46,20 +52,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>
@@ -88,7 +93,7 @@
<div id="container">
<div class="wrapper">
<div id="mainCol">
- <%= yield.html_safe %>
+ <%= yield %>
<h3>Feedback</h3>
<p>
@@ -96,12 +101,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.
@@ -111,7 +116,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>
@@ -123,18 +128,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 2722789c49..a2ae4ea59e 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`](https://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`
@@ -168,7 +170,7 @@ render a file, because Windows filenames do not have the same format as Unix fil
#### 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:
@@ -221,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
@@ -230,14 +232,14 @@ You can send an HTML string back to the browser by using the `:html` option to
`render`:
```ruby
-render html: "<strong>Not Found</strong>".html_safe
+render html: helpers.tag.strong('Not Found')
```
TIP: This is useful when you're rendering a small snippet of HTML code.
However, you might want to consider moving it to a template file if the markup
is complex.
-NOTE: When using `html:` option, HTML entities will be escaped if the string is not marked as HTML safe by using `html_safe` method.
+NOTE: When using `html:` option, HTML entities will be escaped if the string is not composed with `html_safe`-aware APIs.
#### Rendering JSON
@@ -283,7 +285,7 @@ the response. Using `:plain` or `:html` might be more appropriate most of the
time.
NOTE: Unless overridden, your response returned from this render option will be
-`text/html`, as that is the default content type of Action Dispatch response.
+`text/plain`, as that is the default content type of Action Dispatch response.
#### Options for `render`
@@ -379,6 +381,7 @@ Rails understands both numeric status codes and the corresponding symbols shown
| | 415 | :unsupported_media_type |
| | 416 | :range_not_satisfiable |
| | 417 | :expectation_failed |
+| | 421 | :misdirected_request |
| | 422 | :unprocessable_entity |
| | 423 | :locked |
| | 424 | :failed_dependency |
@@ -386,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 |
@@ -399,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
@@ -411,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.
@@ -630,6 +636,8 @@ to use in this case.
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:
@@ -749,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`
@@ -764,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"},
@@ -1080,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 %>
@@ -1153,7 +1161,7 @@ To pass a local variable to a partial in only specific cases use the `local_assi
<%= render article, full: true %>
```
-* `_articles.html.erb`
+* `_article.html.erb`
```erb
<h2><%= article.title %></h2>
@@ -1167,7 +1175,7 @@ To pass a local variable to a partial in only specific cases use the `local_assi
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 %>
@@ -1202,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>
@@ -1258,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:
@@ -1278,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 f99b6ebd31..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:** `5.0.Z`.
+**Currently included series:** `5.2.Z`.
Security Issues
---------------
@@ -59,16 +59,16 @@ be built from 1.2.2, and then added to the end of 1-2-stable. This means that
security releases are easy to upgrade to if you're running the latest version
of Rails.
-**Currently included series:** `5.0.Z`, `4.2.Z`.
+**Currently included series:** `5.2.Z`, `5.1.Z`.
Severe Security Issues
----------------------
-For severe security issues we will provide new versions as above, and also the
+For severe security issues all releases in the current major series, and also the
last major release series will receive patches and new versions. The
classification of the security issue is judged by the core team.
-**Currently included series:** `5.0.Z`, `4.2.Z`.
+**Currently included series:** `5.2.Z`, `5.1.Z`, `5.0.Z`, `4.2.Z`.
Unsupported Release Series
--------------------------
diff --git a/guides/source/nested_model_forms.md b/guides/source/nested_model_forms.md
deleted file mode 100644
index 71efa4b0d0..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 < ApplicationRecord
- has_one :address
- accepts_nested_attributes_for :address
-end
-```
-
-#### belongs_to
-
-```ruby
-class Person < ApplicationRecord
- belongs_to :firm
- accepts_nested_attributes_for :firm
-end
-```
-
-#### has_many / has_and_belongs_to_many
-
-```ruby
-class Person < ApplicationRecord
- 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 8f055f8fe3..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
====================================
@@ -30,7 +30,7 @@ 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.
@@ -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
@@ -104,14 +104,16 @@ Finished in 0.003358s, 595.6483 runs/s, 297.8242 assertions/s.
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
```
@@ -120,7 +122,7 @@ 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
@@ -133,10 +135,10 @@ To test that your method does what it says it does, run the unit tests with `bin
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"
```
@@ -152,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
@@ -161,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
```
@@ -173,7 +177,6 @@ end
module Yaffle
module ActsAsYaffle
- # your code will go here
end
end
```
@@ -189,7 +192,7 @@ 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
@@ -234,12 +237,12 @@ Finished in 0.004812s, 831.2949 runs/s, 415.6475 assertions/s.
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
@@ -247,7 +250,7 @@ and migrating the database. First, run:
```bash
$ cd test/dummy
-$ bin/rails 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
@@ -276,12 +279,8 @@ 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
@@ -335,13 +334,9 @@ module Yaffle
module ActsAsYaffle
extend ActiveSupport::Concern
- included do
- end
-
- 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
@@ -364,14 +359,14 @@ When you run `bin/test`, you should see the tests all pass:
### Add an Instance Method
-This plugin will add a method named 'squawk' to any Active Record object that calls 'acts_as_yaffle'. The 'squawk'
+This plugin will add a method named 'squawk' to any Active Record object that calls `acts_as_yaffle`. The 'squawk'
method will simply set the value of one of the fields in the database.
To start out, write a failing test that shows the behavior you'd like:
```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
@@ -397,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
@@ -407,20 +402,14 @@ 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
@@ -450,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
------------------
@@ -480,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:
@@ -492,4 +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/)
+* [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 3b773d84f8..982df26987 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 task `app:template` to apply templates to an existing Rails application. The location of the template needs to be passed in to an environment variable named LOCATION. Again, this can either be path to a file or a URL.
+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/rails app:template LOCATION=~/template.rb
-$ bin/rails app:template LOCATION=http://example.com/template.rb
+$ rails app:template LOCATION=~/template.rb
+$ rails app:template LOCATION=http://example.com/template.rb
```
Template API
@@ -177,24 +177,30 @@ run "rm README.rdoc"
### rails_command(command, options = {})
-Runs the supplied task in the Rails application. Let's say you want to migrate the database:
+Runs the supplied command in the Rails application. Let's say you want to migrate the database:
```ruby
rails_command "db:migrate"
```
-You can also run tasks with a different Rails environment:
+You can also run commands with a different Rails environment:
```ruby
rails_command "db:migrate", env: 'production'
```
-You can also run tasks as a super-user:
+You can also run commands as a super-user:
```ruby
rails_command "log:clear", sudo: true
```
+You can also run commands that should abort application generation if they fail:
+
+```ruby
+rails_command "db:migrate", abort_on_failure: true
+```
+
### route(routing_code)
Adds a routing entry to the `config/routes.rb` file. In the steps above, we generated a person scaffold and also removed `README.rdoc`. Now, to make `PeopleController#index` the default page for the application:
@@ -277,6 +283,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 b712965b7f..d60e53b052 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
-------------
@@ -35,7 +35,7 @@ application. Any Rack compliant web server should be using
### `rails server`
-`rails server` does the basic job of creating a `Rack::Server` object and starting the webserver.
+`rails server` does the basic job of creating a `Rack::Server` object and starting the web server.
Here's how `rails server` creates an instance of `Rack::Server`
@@ -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 task for inspecting the middleware stack in use:
+Rails has a handy command for inspecting the middleware stack in use:
```bash
-$ bin/rails middleware
+$ rails middleware
```
For a freshly generated Rails application, this might produce something like:
@@ -104,34 +105,36 @@ For a freshly generated Rails application, this might produce something like:
```ruby
use Rack::Sendfile
use ActionDispatch::Static
-use ActionDispatch::LoadInterlock
-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::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
@@ -149,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
@@ -178,11 +181,10 @@ And now if you inspect the middleware stack, you'll find that `Rack::Runtime` is
not a part of it.
```bash
-$ bin/rails 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
```
@@ -219,7 +221,7 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
* Sets `env["rack.multithread"]` flag to `false` and wraps the application within a Mutex.
-**`ActionDispatch::LoadInterlock`**
+**`ActionDispatch::Executor`**
* Used for thread safe code reloading during development.
@@ -239,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`**
@@ -251,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.
@@ -267,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.
@@ -287,18 +285,26 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
* Sets up the flash keys. Only available if `config.action_controller.session_store` is set to a value.
+**`ActionDispatch::ContentSecurityPolicy::Middleware`**
+
+* Provides a DSL to configure a Content-Security-Policy header.
+
**`Rack::Head`**
* Converts HEAD requests to `GET` requests and serves them as so.
**`Rack::ConditionalGet`**
-* Adds support for "Conditional `GET`" so that server responds with nothing if page wasn't changed.
+* Adds support for "Conditional `GET`" so that server responds with nothing if the page wasn't changed.
**`Rack::ETag`**
* Adds ETag header on all String bodies. ETags are used to validate cache.
+**`Rack::TempfileReaper`**
+
+* Cleans up tempfiles used to buffer multipart requests.
+
TIP: It's possible to use any of the above middlewares in your custom Rack stack.
Resources
@@ -306,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 81321c7405..e3a6bbb138 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
=================================
@@ -9,16 +9,16 @@ After reading this guide, you will know:
* 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
-----------------------------------
@@ -116,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
@@ -142,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 but you must also specify the `controller:` to use:
+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, controller: 'users'
+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:
@@ -173,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
@@ -245,7 +260,7 @@ In each of these cases, the named routes remain the same as if you did not use `
| PATCH/PUT | /admin/articles/:id | articles#update | article_path(:id) |
| DELETE | /admin/articles/:id | articles#destroy | article_path(:id) |
-TIP: _If you need to use a different controller namespace inside a `namespace` block you can specify an absolute controller path, e.g: `get '/foo' => '/foo#index'`._
+TIP: _If you need to use a different controller namespace inside a `namespace` block you can specify an absolute controller path, e.g: `get '/foo', to: '/foo#index'`._
### Nested Resources
@@ -425,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
@@ -504,7 +519,7 @@ resources :photos do
end
```
-You can leave out the `:on` option, this will create the same member route except that the resource id value will be available in `params[:photo_id]` instead of `params[:id]`.
+You can leave out the `:on` option, this will create the same member route except that the resource id value will be available in `params[:photo_id]` instead of `params[:id]`. Route helpers will also be renamed from `preview_photo_url` and `preview_photo_path` to `photo_preview_url` and `photo_preview_path`.
#### Adding Collection Routes
@@ -528,6 +543,8 @@ resources :photos do
end
```
+NOTE: If you're defining additional resource routes with a symbol as the first positional argument, be mindful that it is not equivalent to using a string. Symbols infer controller actions while strings infer paths.
+
#### Adding Routes for Additional New Actions
To add an alternate new action using the `:on` shortcut:
@@ -545,7 +562,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.
@@ -553,29 +570,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.
@@ -584,39 +595,39 @@ 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
@@ -635,7 +646,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
@@ -653,7 +664,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
@@ -710,12 +721,12 @@ NOTE: There is an exception for the `format` constraint: while it's a method on
### 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)
@@ -724,8 +735,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
```
@@ -733,8 +744,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
```
@@ -821,14 +832,14 @@ NOTE: For the curious, `'articles#index'` actually expands out to `ArticlesContr
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'
@@ -865,6 +876,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
------------------------------
@@ -1009,7 +1063,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
@@ -1087,10 +1141,10 @@ 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
@@ -1108,7 +1162,7 @@ class Video < ApplicationRecord
end
video = Video.find_by(identifier: "Roman-Holiday")
-edit_videos_path(video) # => "/videos/Roman-Holiday"
+edit_video_path(video) # => "/videos/Roman-Holiday/edit"
```
Inspecting and Testing Routes
@@ -1136,28 +1190,55 @@ For example, here's a small section of the `rails routes` output for a RESTful r
edit_user GET /users/:id/edit(.:format) users#edit
```
+You can also use the `--expanded` option to turn on the expanded table formatting mode.
+
+```
+$ rails routes --expanded
+
+--[ Route 1 ]----------------------------------------------------
+Prefix | users
+Verb | GET
+URI | /users(.:format)
+Controller#Action | users#index
+--[ Route 2 ]----------------------------------------------------
+Prefix |
+Verb | POST
+URI | /users(.:format)
+Controller#Action | users#create
+--[ Route 3 ]----------------------------------------------------
+Prefix | new_user
+Verb | GET
+URI | /users/new(.:format)
+Controller#Action | users#new
+--[ Route 4 ]----------------------------------------------------
+Prefix | edit_user
+Verb | GET
+URI | /users/:id/edit(.:format)
+Controller#Action | users#edit
+```
+
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.
```
-$ bin/rails routes -g new_comment
-$ bin/rails routes -g POST
-$ bin/rails routes -g admin
+$ 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.
```
-$ bin/rails routes -c users
-$ bin/rails routes -c admin/users
-$ bin/rails routes -c Comments
-$ bin/rails routes -c Articles::CommentsController
+$ 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 `rails routes` is much more readable if you widen your terminal window until the output lines don't wrap.
### Testing Routes
-Routes should be included in your testing strategy (just like the rest of your application). Rails offers three [built-in assertions](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html) designed to make testing routes simpler:
+Routes should be included in your testing strategy (just like the rest of your application). Rails offers three [built-in assertions](https://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html) designed to make testing routes simpler:
* `assert_generates`
* `assert_recognizes`
diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md
index 50866350f8..67b0e523a7 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
+
+```
+https://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,
+
+```
+https://api.rubyonrails.org/classes/ActionDispatch/Response.html
+```
+
+becomes
+
+```
+https://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
+
+```
+https://api.rubyonrails.org/classes/ActionDispatch/Response.html
+```
+
+becomes
+
+```
+https://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 16c5291037..22c122d4b9 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,44 +21,34 @@ 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 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
--------
-A good place to start looking at security is with sessions, which can be vulnerable to particular attacks.
+This chapter describes some particular attacks related to sessions, and security measures to protect your session data.
### What are Sessions?
-NOTE: _HTTP is a stateless protocol. Sessions make it stateful._
+INFO: Sessions enable the application to maintain user-specific state, while users interact with the application. For example, sessions allow users to authenticate once and remain signed in for future requests.
-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.
+Most applications need to keep track of state for users that interact with the application. This could be the contents of a shopping basket, or the user id of the currently logged in user. This kind of user-specific state can be stored in the session.
-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:
+Rails provides a session object for each user that accesses the application. If the user already has an active session, Rails uses the existing session. Otherwise a new session is created.
-```ruby
-session[:user_id] = @current_user.id
-User.find(session[:user_id])
-```
-
-### Session id
-
-NOTE: _The session id is a 32 byte long MD5 hash value._
-
-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.
+NOTE: Read more about sessions and how to use them in [Action Controller Overview Guide](action_controller_overview.html#session).
### 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:
@@ -74,50 +64,101 @@ 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
+### Session Storage
-Here are some general guidelines on sessions.
+NOTE: Rails uses `ActionDispatch::Session::CookieStore` as the default session storage.
-* _Do not store large objects in a session_. Instead you should store them in the database and save their id in the session. This will eliminate synchronization headaches and it won't fill up your session storage space (depending on what session storage you chose, see below).
-This will also be a good idea, if you modify the structure of an object and old versions of it are still in some user's cookies. With server-side session storages you can clear out the sessions, but with client-side storages, this is hard to mitigate.
+TIP: Learn more about other session storages in [Action Controller Overview Guide](action_controller_overview.html#session).
-* _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.
+Rails `CookieStore` saves the session hash 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:
-### Session Storage
+* Cookies have a size limit of 4kB. Use cookies only for data which is relevant for the session.
+
+* Cookies are stored on the client-side. The client may preserve cookie contents even for expired cookies. The client may copy cookies to other machines. Avoid storing sensitive data in cookies.
+
+* Cookies are temporary by nature. The server can set expiration time for the cookie, but the client may delete the cookie and its contents before that. Persist all data that is of more permanent nature on the server side.
+
+* 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.
+
+* Rails encrypts cookies by default. The client cannot read or edit the contents of the cookie, without breaking encryption. If you take appropriate care of your secrets, you can consider your cookies to be generally secured.
-NOTE: _Rails provides several storage mechanisms for the session hashes. The most important is `ActionDispatch::Session::CookieStore`._
+The `CookieStore` uses the
+[encrypted](https://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](https://api.rubyonrails.org/classes/ActionDispatch/Cookies/ChainedCookieJars.html#method-i-signed)
+cookies, is derived from the `secret_key_base` configuration value.
-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:
+TIP: Secrets must be long and random. Use `rails secret` to get new unique secrets.
-* 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_.
+INFO: Learn more about [managing credentials later in this guide](security.html#custom-credentials)
-* The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret (`secrets.secret_token`) and inserted into the end of the cookie.
+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.
-However, since Rails 4, the default store is EncryptedCookieStore. With
-EncryptedCookieStore the session is encrypted before being stored in a cookie.
-This prevents the user from accessing and tampering the content of the cookie.
-Thus the session becomes a more secure place to store data. The encryption is
-done using a server-side secret key `secrets.secret_key_base` stored in
-`config/secrets.yml`.
+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:
-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, use `rails secret` instead_.
+ secret_key_base: 492f...
-`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.:
+WARNING: If your application's secrets may have been exposed, strongly consider changing them. Changing `secret_key_base` will expire currently active sessions.
- development:
- secret_key_base: a75d...
+### Rotating Encrypted and Signed Cookies Configurations
- test:
- secret_key_base: 492f...
+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.
+
+It's possible to rotate the ciphers and digests used for encrypted and signed cookies.
+
+For instance to change the digest used for signed cookies from SHA1 to SHA256,
+you would first assign the new configuration value:
+
+```ruby
+Rails.application.config.action_dispatch.signed_cookie_digest = "SHA256"
+```
+
+Now add a rotation for the old SHA1 digest so existing cookies are
+seamlessly upgraded to the new SHA256 digest.
+
+```ruby
+Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
+ cookies.rotate :signed, digest: "SHA1"
+end
+```
- production:
- secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
+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.
-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.
+Once users with SHA1 digested signed cookies should no longer have a chance to
+have their cookies rewritten, remove the rotation.
-If you have received an application where the secret was exposed (e.g. an application whose source was shared), strongly consider changing the secret.
+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](https://api.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html)
+and
+[MessageVerifier API](https://api.rubyonrails.org/classes/ActiveSupport/MessageVerifier.html)
+documentation.
### Replay Attacks for CookieStore Sessions
@@ -131,22 +172,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.
@@ -166,9 +207,9 @@ Another countermeasure is to _save user-specific properties in the session_, ver
### 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 < ApplicationRecord
@@ -182,7 +223,7 @@ class Session < ApplicationRecord
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
@@ -194,17 +235,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 if the request comes from a site of a different domain, it will also send the cookie. 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.
+* 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_.
@@ -212,7 +253,7 @@ CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures) - less th
NOTE: _First, as is required by the W3C, use GET and POST appropriately. Secondly, a security token in non-GET requests will protect your application from CSRF._
-The HTTP protocol basically provides two main types of requests - GET and POST (and more, but they are not supported by most browsers). The World Wide Web Consortium (W3C) provides a checklist for choosing HTTP GET or POST:
+The HTTP protocol basically provides two main types of requests - GET and POST (DELETE, PUT, and PATCH should be used like POST). The World Wide Web Consortium (W3C) provides a checklist for choosing HTTP GET or POST:
**Use GET if:**
@@ -224,7 +265,7 @@ The HTTP protocol basically provides two main types of requests - GET and POST (
* The interaction _changes the state_ of the resource in a way that the user would perceive (e.g., a subscription to a service), or
* The user is _held accountable for the results_ of the interaction.
-If your web application is RESTful, you might be used to additional HTTP verbs, such as PATCH, PUT or DELETE. Most of today's web browsers, however, do not support them - only GET and POST. Rails uses a hidden `_method` field to handle this barrier.
+If your web application is RESTful, you might be used to additional HTTP verbs, such as PATCH, PUT, or DELETE. Some legacy web browsers, however, do not support them - only GET and POST. Rails uses a hidden `_method` field to handle these cases.
_POST requests can be sent automatically, too_. In this example, the link www.harmless.com is shown as the destination in the browser's status bar. But it has actually dynamically created a new form that sends a POST request.
@@ -247,9 +288,9 @@ Or the attacker places the code into the onmouseover event handler of an image:
There are many other possibilities, like using a `<script>` tag to make a cross-site request to a URL with a JSONP or JavaScript response. The response is executable code that the attacker can find a way to run, possibly extracting sensitive data. To protect against this data leakage, we must disallow cross-site `<script>` tags. Ajax requests, however, obey the browser's same-origin policy (only your own site is allowed to initiate `XmlHttpRequest`) so we can safely allow them to return JavaScript responses.
-Note: We can't distinguish a `<script>` tag's origin—whether it's a tag on your own site or on some other malicious site—so we must block all `<script>` across the board, even if it's actually a safe same-origin script served from your own site. In these cases, explicitly skip CSRF protection on actions that serve JavaScript meant for a `<script>` tag.
+NOTE: We can't distinguish a `<script>` tag's origin—whether it's a tag on your own site or on some other malicious site—so we must block all `<script>` across the board, even if it's actually a safe same-origin script served from your own site. In these cases, explicitly skip CSRF protection on actions that serve JavaScript meant for a `<script>` tag.
-To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created rails applications:
+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
@@ -257,13 +298,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:
@@ -287,7 +327,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
@@ -301,7 +341,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 a 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
@@ -315,9 +355,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)
@@ -342,7 +382,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
@@ -357,7 +397,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), '../../../'))
@@ -377,7 +417,7 @@ In 2007 there was the first tailor-made trojan which stole information from an I
Having one single place in the admin interface or Intranet, where the input has not been sanitized, makes the entire application vulnerable. Possible exploits include stealing the privileged administrator's cookie, injecting an iframe to steal the administrator's password or installing malicious software through browser security holes to take over the administrator's computer.
-Refer to the Injection section for countermeasures against XSS. It is _recommended to use the SafeErb plugin_ also in an Intranet or administration interface.
+Refer to the Injection section for countermeasures against XSS.
**CSRF** Cross-Site Request Forgery (CSRF), also known as Cross-Site Reference Forgery (XSRF), is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface.
@@ -385,7 +425,7 @@ A real-world example is a [router reconfiguration by CSRF](http://www.h-online.c
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_.
@@ -397,7 +437,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
---------------
@@ -425,7 +465,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
@@ -459,7 +499,7 @@ Depending on your web application, there may be more ways to hijack the user's a
INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect registration forms from attackers and comment forms from automatic spam bots by asking the user to type the letters of a distorted image. This is the positive CAPTCHA, but there is also the negative CAPTCHA. The idea of a negative CAPTCHA is not for a user to prove that they are human, but reveal that a robot is a robot._
-A popular positive CAPTCHA API is [reCAPTCHA](http://recaptcha.net/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API.
+A popular positive CAPTCHA API is [reCAPTCHA](https://developers.google.com/recaptcha/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API.
You will get two keys from the API, a public and a private key, which you have to put into your Rails environment. After that you can use the recaptcha_tags method in the view, and the verify_recaptcha method in the controller. Verify_recaptcha will return false if the validation fails.
The problem with CAPTCHAs is that they have a negative impact on the user experience. Additionally, some visually impaired users have found certain kinds of distorted CAPTCHAs difficult to read. Still, positive CAPTCHAs are one of the best methods to prevent all kinds of bots from submitting forms.
@@ -474,7 +514,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):
@@ -496,18 +536,6 @@ config.filter_parameters << :password
NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`.
-### Good Passwords
-
-INFO: _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._
-
-Bruce Schneier, a security technologist, [has analyzed](http://www.schneier.com/blog/archives/2006/12/realworld_passw.html) 34,000 real-world user names and passwords from the MySpace phishing attack mentioned [below](#examples-from-the-underground). It turns out that most of the passwords are quite easy to crack. The 20 most common passwords are:
-
-password1, abc123, myspace1, password, blink182, qwerty1, ****you, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, and monkey.
-
-It is interesting that only 4% of these passwords were dictionary words and the great majority is actually alphanumeric. However, password cracker dictionaries contain a large number of today's passwords, and they try out all kinds of (alphanumerical) combinations. If an attacker knows your user name and you use a weak password, your account will be easily cracked.
-
-A good password is a long alphanumeric combination of mixed cases. As this is quite hard to remember, it is advisable to enter only the _first letters of a sentence that you can easily remember_. For example "The quick brown fox jumps over the lazy dog" will be "Tqbfjotld". Note that this is just an example, you should not use well known phrases like these, as they might appear in cracker dictionaries, too.
-
### Regular Expressions
INFO: _A common pitfall in Ruby's regular expressions is to match the string's beginning and end by ^ and $, instead of \A and \z._
@@ -567,28 +595,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
@@ -615,7 +643,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:
@@ -653,7 +681,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:
@@ -677,13 +705,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
@@ -722,11 +748,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>
@@ -747,15 +773,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)
@@ -764,7 +790,7 @@ s = sanitize(user_input, tags: tags, attributes: %w(href title))
This allows only the given tags and does a good job, even against all kinds of tricks and malformed tags.
-As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &amp;, &quot;, &lt;, and &gt; by their uninterpreted representations in HTML (`&amp;`, `&quot;`, `&lt;`, and `&gt;`). 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
@@ -789,7 +815,7 @@ 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 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 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.
@@ -797,9 +823,9 @@ In December 2006, 34,000 actual user names and passwords were stolen in a [MySpa
### 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 the well-known [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, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm.
+CSS Injection is explained best by the well-known [MySpace Samy worm](https://samy.pl/myspace/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm.
MySpace blocked many tags, but allowed CSS. So the worm's author put JavaScript into CSS like this:
@@ -813,7 +839,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'));
@@ -833,7 +859,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
@@ -862,7 +888,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
@@ -886,9 +912,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:
@@ -967,7 +993,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:
@@ -995,7 +1021,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'
}
```
@@ -1021,41 +1050,165 @@ Here is a list of common headers:
* **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)
+* **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`](https://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.
+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`, master key for `credentials.yml`, and other unencrypted secrets. You may want to further restrict access, using environment-specific versions of these files and any others that may contain sensitive information.
+
+### Custom credentials
+
+Rails stores secrets in `config/credentials.yml.enc`, which is encrypted and hence cannot be edited directly. Rails uses `config/master.key` or alternatively looks for environment variable `ENV["RAILS_MASTER_KEY"]` to encrypt the credentials file. The credentials file can be stored in version control, as long as master key is kept safe.
-### Custom secrets
+To add new secret to credentials, first run `rails secret` to get a new secret. Then run `rails credentials:edit` to edit credentials, and add the secret. Running `credentials:edit` creates new credentials file and master key, if they did not already exist.
-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.
+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.
-The secrets added to this file are accessible via `Rails.application.secrets`.
-For example, with the following `config/secrets.yml`:
+The secrets kept in credentials file are accessible via `Rails.application.credentials`.
+For example, with the following decrypted `config/credentials.yml.enc`:
- development:
- secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
- some_api_key: SOMEKEY
+ secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
+ some_api_key: SOMEKEY
-`Rails.application.secrets.some_api_key` returns `SOMEKEY` in the development
-environment.
+`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: key not found: :some_api_key
+Rails.application.credentials.some_api_key! # => raises KeyError: :some_api_key is blank
```
+
+TIP: Learn more about credentials with `rails credentials:help`.
+
+WARNING: Keep your master key safe. Do not commit your master key.
+
+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 e302611b2a..1fad02812b 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,7 +18,7 @@ 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.
@@ -33,16 +33,27 @@ Rails creates a `test` directory for you as soon as you create a Rails project u
```bash
$ ls -F test
-controllers/ helpers/ mailers/ test_helper.rb
-fixtures/ integration/ models/
+application_system_test_case.rb controllers/ helpers/ mailers/ system/
+channels/ fixtures/ integration/ models/ test_helper.rb
```
-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.
+The `helpers`, `mailers`, and `models` directories are meant to hold tests for view helpers, mailers, and models, respectively. The `channels` directory is meant to hold tests for Action Cable connection and channels. The `controllers` directory is meant to hold tests for controllers, routes, and views. The `integration` directory is meant to hold tests for interactions between controllers.
+
+The system test directory holds system tests, which are used for full browser
+testing of your application. System tests allow you to test your application
+the way your users experience it and help you test your JavaScript as well.
+System tests inherit from Capybara and perform in browser tests for your
+application.
Fixtures are a way of organizing test data; they reside in the `fixtures` directory.
+A `jobs` directory will also be created when an associated test is first generated.
+
The `test_helper.rb` file holds the default configuration for your tests.
+The `application_system_test_case.rb` holds the default configuration for your system
+tests.
+
### The Test Environment
@@ -59,7 +70,7 @@ If you remember, we used the `rails generate model` command in the
model, and among other things it created test stubs in the `test` directory:
```bash
-$ bin/rails generate model article title:string body:text
+$ rails generate model article title:string body:text
...
create app/models/article.rb
create test/models/article_test.rb
@@ -94,7 +105,7 @@ 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, 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:
@@ -112,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.
@@ -145,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
+
+
+rails test test/models/article_test.rb:6
+
- 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
+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 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:
+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
@@ -169,8 +190,8 @@ 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
```
@@ -185,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
@@ -214,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
-Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s.
+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>'
- 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
+rails test test/models/article_test.rb:9
+
+
+
+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.
@@ -239,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:
@@ -285,9 +319,10 @@ specify to make your test failure messages clearer.
| `assert_not_includes( collection, obj, [msg] )` | Ensures that `obj` is not in `collection`.|
| `assert_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.|
| `assert_not_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.|
+| `assert_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` have a relative error less than `epsilon`.|
+| `assert_not_in_epsilon ( expected, actual, [epsilon], [msg] )` | Ensures that the numbers `expected` and `actual` don't have a relative error less than `epsilon`.|
| `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.|
| `assert_raises( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.|
-| `assert_nothing_raised { block }` | Ensures that the given block doesn't raise any exceptions.|
| `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.|
| `assert_not_instance_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class`.|
| `assert_kind_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class` or is descending from it.|
@@ -298,7 +333,6 @@ specify to make your test failure messages clearer.
| `assert_not_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is false.|
| `assert_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is true, e.g. `assert_predicate str, :empty?`|
| `assert_not_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is false, e.g. `assert_not_predicate str, :empty?`|
-| `assert_send( array, [msg] )` | Ensures that executing the method listed in `array[1]` on the object in `array[0]` with the parameters of `array[2 and up]` is true, e.g. assert_send [@user, :full_name, 'Sam Smith']. This one is weird eh?|
| `flunk( [msg] )` | Ensures failure. This is useful to explicitly mark a test that isn't finished yet.|
The above are a subset of assertions that minitest supports. For an exhaustive &
@@ -316,12 +350,15 @@ Rails adds some custom assertions of its own to the `minitest` framework:
| Assertion | Purpose |
| --------------------------------------------------------------------------------- | ------- |
-| [`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_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`.|
+| [`assert_difference(expressions, difference = 1, message = nil) {...}`](https://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)`](https://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)`](https://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)`](https://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 }`](https://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)`](https://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)`](https://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)`](https://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](https://rubydoc.info/github/rack/rack/master/Rack/Utils#SYMBOL_TO_STATUS_CODE-constant) works.|
+| [`assert_redirected_to(options = {}, message=nil)`](https://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.
@@ -329,11 +366,13 @@ You'll see the usage of some of these assertions in the next chapter.
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`](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)
-* [`ActionDispatch::IntegrationTest`](http://api.rubyonrails.org/classes/ActionDispatch/IntegrationTest.html)
-* [`ActiveJob::TestCase`](http://api.rubyonrails.org/classes/ActiveJob/TestCase.html)
+* [`ActiveSupport::TestCase`](https://api.rubyonrails.org/classes/ActiveSupport/TestCase.html)
+* [`ActionMailer::TestCase`](https://api.rubyonrails.org/classes/ActionMailer/TestCase.html)
+* [`ActionView::TestCase`](https://api.rubyonrails.org/classes/ActionView/TestCase.html)
+* [`ActiveJob::TestCase`](https://api.rubyonrails.org/classes/ActiveJob/TestCase.html)
+* [`ActionDispatch::IntegrationTest`](https://api.rubyonrails.org/classes/ActionDispatch/IntegrationTest.html)
+* [`ActionDispatch::SystemTestCase`](https://api.rubyonrails.org/classes/ActionDispatch/SystemTestCase.html)
+* [`Rails::Generators::TestCase`](https://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.
@@ -344,15 +383,19 @@ documentation](http://docs.seattlerb.org/minitest).
We can run all of our tests at once by using the `rails test` command.
-Or we can run a single test by passing the `rails test` command the filename containing the test cases.
+Or we can run a single test file by passing the `rails test` command the filename containing the test cases.
```bash
-$ bin/rails test test/models/article_test.rb
-.
+$ rails test test/models/article_test.rb
+Run options: --seed 1559
-Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s.
+# Running:
-1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
+..
+
+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.
@@ -361,7 +404,11 @@ 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
-$ bin/rails test test/models/article_test.rb -n test_the_truth
+$ 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.
@@ -372,15 +419,131 @@ Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.
You can also run a test at a specific line by providing the line number.
```bash
-$ bin/rails test test/models/post_test.rb:44 # run specific test and line
+$ 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
-$ bin/rails test test/controllers # run all tests from specific directory
+$ 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 number is the actual core count
+on the machine you are on, but can be changed by the number passed to the parallelize method.
+
+To enable parallelization add the following to your `test_helper.rb`:
+
+```ruby
+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:
+
+```bash
+PARALLEL_WORKERS=15 rails test
+```
+
+When parallelizing tests, Active Record automatically handles creating a database and loading the schema into the 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 forked process is 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.
+
+```ruby
+class ActiveSupport::TestCase
+ parallelize_setup do |worker|
+ # setup databases
+ end
+
+ parallelize_teardown do |worker|
+ # cleanup databases
+ end
+
+ parallelize(workers: :number_of_processors)
+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`
+
+```ruby
+class ActiveSupport::TestCase
+ parallelize(workers: :number_of_processors, 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:
+
+```bash
+PARALLEL_WORKERS=15 rails test
+```
The Test Database
-----------------
@@ -399,17 +562,17 @@ 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 (`bin/rails db:migrate`) will
+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 `bin/rails db:test:prepare`.
+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).
+You can find comprehensive documentation in the [Fixtures API documentation](https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html).
#### What Are Fixtures?
@@ -440,7 +603,7 @@ steve:
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
+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:
@@ -458,7 +621,7 @@ first:
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).
+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](https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html).
#### ERB'in It Up
@@ -495,7 +658,8 @@ users(:david)
users(:david).id
# one can also access methods available on the User class
-email(david.partner.email, david.location_tonight)
+david = users(:david)
+david.call(david.partner)
```
To get multiple fixtures at once, you can pass in a list of fixture names. For example:
@@ -515,28 +679,242 @@ Rails model tests are stored under the `test/models` directory. Rails provides
a generator to create a model test skeleton for you.
```bash
-$ bin/rails generate test_unit:model article title:string body:text
+$ 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).
+Model tests don't have their own superclass like `ActionMailer::TestCase` instead they inherit from [`ActiveSupport::TestCase`](https://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.
+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
-$ bin/rails generate integration_test user_flows
+$ 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:
+Here's what a freshly generated integration test looks like:
```ruby
require 'test_helper'
@@ -554,11 +932,11 @@ Here the test is inheriting from `ActionDispatch::IntegrationTest`. This makes s
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).
+For dealing with the integration test runner, see [`ActionDispatch::Integration::Runner`](https://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.
+When performing requests, we will have [`ActionDispatch::Integration::RequestHelpers`](https://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.
+If we need to modify the session, or state of our integration test, take a look at [`ActionDispatch::Integration::Session`](https://api.rubyonrails.org/classes/ActionDispatch/Integration/Session.html) to help.
### Implementing an integration test
@@ -567,7 +945,7 @@ Let's add an integration test to our blog application. We'll start with a basic
We'll start by generating our integration test skeleton:
```bash
-$ bin/rails generate integration_test blog_flow
+$ rails generate integration_test blog_flow
```
It should have created a test file placeholder for us. With the output of the
@@ -634,7 +1012,7 @@ Finally we can assert that our response was successful and our new article is re
#### 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.
+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
@@ -649,13 +1027,13 @@ 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?
The easiest way to see functional tests in action is to generate a controller using the scaffold generator:
```bash
-$ bin/rails generate scaffold_controller article title:string body:text
+$ rails generate scaffold_controller article title:string body:text
...
create app/controllers/articles_controller.rb
...
@@ -671,10 +1049,10 @@ If you already have a controller and just want to generate the test scaffold cod
each of the seven default actions, you can use the following command:
```bash
-$ bin/rails generate test_unit:scaffold article
+$ rails generate test_unit:scaffold article
...
invoke test_unit
-create test/controllers/articles_controller_test.rb
+create test/controllers/articles_controller_test.rb
...
```
@@ -684,9 +1062,8 @@ Let's take a look at one such test, `test_should_get_index` from the file `artic
# articles_controller_test.rb
class ArticlesControllerTest < ActionDispatch::IntegrationTest
test "should get index" do
- get '/articles'
+ get articles_url
assert_response :success
- assert_includes @response.body, 'Articles'
end
end
```
@@ -694,30 +1071,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 route (i.e. `articles_url`).
+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).
-
-* `session`: option with a hash of session variables to pass along with the request.
-
-* `flash`: option with a hash of flash values.
+* `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.
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_url, 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.
@@ -727,7 +1103,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 '/article', 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)
@@ -736,6 +1112,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:
@@ -747,7 +1129,7 @@ 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.
@@ -758,7 +1140,7 @@ To test AJAX requests, you can specify the `xhr: true` option to `get`, `post`,
```ruby
test "ajax request" do
- article = articles(:first)
+ article = articles(:one)
get article_url(article), xhr: true
assert_equal 'hello world', @response.body
@@ -784,27 +1166,38 @@ 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 an HTTP Header
-@request.headers["Accept"] = "text/plain, text/html"
-get articles_url # simulate the request with custom 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 article_url # 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
@@ -830,7 +1223,7 @@ end
If we run our test now, we should see a failure:
```bash
-$ bin/rails test test/controllers/articles_controller_test.rb -n 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:
@@ -840,7 +1233,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 @@
@@ -868,7 +1261,7 @@ end
Now if we run our tests, we should see it pass:
```bash
-$ bin/rails test test/controllers/articles_controller_test.rb -n 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:
@@ -889,7 +1282,7 @@ Let's write a test for the `:show` action:
```ruby
test "should show article" do
article = articles(:one)
- get '/article', params: { id: article.id }
+ get article_url(article)
assert_response :success
end
```
@@ -915,7 +1308,7 @@ We can also add a test for updating an existing Article.
test "should update article" do
article = articles(:one)
- patch '/article', 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.
@@ -958,7 +1351,7 @@ class ArticlesControllerTest < ActionDispatch::IntegrationTest
end
test "should update article" do
- patch '/article', 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.
@@ -976,11 +1369,11 @@ 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
@@ -996,7 +1389,7 @@ 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 profile_url
assert_response :success
@@ -1004,14 +1397,64 @@ class ProfileControllerTest < ActionDispatch::IntegrationTest
end
```
+#### Using Separate Files
+
+If you find your helpers are cluttering `test_helper.rb`, you can extract them into separate files. One good place to store them is `lib/test`.
+
+```ruby
+# lib/test/multiple_assertions.rb
+module MultipleAssertions
+ def assert_multiple_of_forty_two(number)
+ assert (number % 42 == 0), 'expected #{number} to be a multiple of 42'
+ end
+end
+```
+
+These helpers can then be explicitly required as needed and included as needed
+
+```ruby
+require 'test_helper'
+require 'test/multiple_assertions'
+
+class NumberTest < ActiveSupport::TestCase
+ include MultipleAssertions
+
+ test '420 is a multiple of forty two' do
+ assert_multiple_of_forty_two 420
+ end
+end
+```
+
+or they can continue to be included directly into the relevant parent classes
+
+```ruby
+# test/test_helper.rb
+require 'test/sign_in_helper'
+
+class ActionDispatch::IntegrationTest
+ include SignInHelper
+end
+```
+
+#### Eagerly Requiring Helpers
+
+You may find it convenient to eagerly require helpers in `test_helper.rb` so your test files have implicit access to them. This can be accomplished using globbing, as follows
+
+```ruby
+# test/test_helper.rb
+Dir[Rails.root.join('lib', 'test', '**', '*.rb')].each { |file| require file }
+```
+
+This has the downside of increasing the boot-up time, as opposed to manually requiring only the necessary files in your individual tests.
+
Testing Routes
--------------
-Like everything else in your Rails application, you can test your routes.
+Like everything else in your Rails application, you can test your routes. Route tests reside in `test/controllers/` or are part of controller tests.
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).
+For more information on routing assertions available in Rails, see the API documentation for [`ActionDispatch::Assertions::RoutingAssertions`](https://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html).
Testing Views
-------------
@@ -1081,7 +1524,7 @@ Testing Helpers
---------------
A helper is just a simple module where you can define methods which are
-available into your views.
+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
@@ -1090,7 +1533,7 @@ located under the `test/helpers` directory.
Given we have the following helper:
```ruby
-module UserHelper
+module UsersHelper
def link_to_user(user)
link_to "#{user.first_name} #{user.last_name}", user
end
@@ -1100,7 +1543,7 @@ end
We can test the output of this method like this:
```ruby
-class UserHelperTest < ActionView::TestCase
+class UsersHelperTest < ActionView::TestCase
test "should return the user's full name" do
user = users(:david)
@@ -1139,7 +1582,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 create 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
@@ -1168,11 +1611,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:
```
@@ -1195,32 +1642,48 @@ NOTE: The `ActionMailer::Base.deliveries` array is only reset automatically in
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 and System 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:
+Unit testing allows us to test the attributes of the email while functional and system testing allows us to test whether user interactions appropriately trigger the email to be delivered. For example, you can check that the invite friend operation is sending an email appropriately:
```ruby
+# Integration Test
require 'test_helper'
-class UserControllerTest < ActionDispatch::IntegrationTest
+class UsersControllerTest < ActionDispatch::IntegrationTest
test "invite friend" do
- assert_difference 'ActionMailer::Base.deliveries.size', +1 do
+ # Asserts the difference in the ActionMailer::Base.deliveries
+ assert_emails 1 do
post invite_friend_url, params: { email: 'friend@example.com' }
end
- invite_email = ActionMailer::Base.deliveries.last
+ end
+end
+```
- 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)
+```ruby
+# System Test
+require 'test_helper'
+
+class UsersTest < ActionDispatch::SystemTestCase
+ driven_by :selenium, using: :headless_chrome
+
+ test "inviting a friend" do
+ visit invite_users_url
+ fill_in 'Email', with: 'friend@example.com'
+ assert_emails 1 do
+ click_on 'Invite'
+ end
end
end
```
+NOTE: The `assert_emails` method is not tied to a particular deliver method and will work with emails delivered with either the `deliver_now` or `deliver_later` method. If we explicitly want to assert that the email has been enqueued we can use the `assert_enqueued_emails` method. More information can be found in the [documentation here](https://api.rubyonrails.org/classes/ActionMailer/TestHelper.html).
+
Testing Jobs
------------
Since your custom jobs can be queued at different levels inside your application,
-you'll need to test both, the jobs themselves (their behavior when they get enqueued)
+you'll need to test both the jobs themselves (their behavior when they get enqueued)
and that other entities correctly enqueue them.
### A Basic Test Case
@@ -1239,7 +1702,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
@@ -1249,7 +1712,7 @@ no jobs have already been executed in the scope of each test.
### Custom Assertions And Testing Jobs Inside Other Components
-Active Job ships with a bunch of custom assertions that can be used to lessen the verbosity of tests. For a full list of available assertions, see the API documentation for [`ActiveJob::TestHelper`](http://api.rubyonrails.org/classes/ActiveJob/TestHelper.html).
+Active Job ships with a bunch of custom assertions that can be used to lessen the verbosity of tests. For a full list of available assertions, see the API documentation for [`ActiveJob::TestHelper`](https://api.rubyonrails.org/classes/ActiveJob/TestHelper.html).
It's a good practice to ensure that your jobs correctly get enqueued or performed
wherever you invoke them (e.g. inside your controllers). This is precisely where
@@ -1259,7 +1722,9 @@ within a model:
```ruby
require 'test_helper'
-class ProductTest < ActiveJob::TestCase
+class ProductTest < ActiveSupport::TestCase
+ include ActiveJob::TestHelper
+
test 'billing job scheduling' do
assert_enqueued_with(job: BillingJob) do
product.charge(account)
@@ -1268,6 +1733,139 @@ class ProductTest < ActiveJob::TestCase
end
```
+Testing Action Cable
+--------------------
+
+Since Action Cable is used at different levels inside your application,
+you'll need to test both the channels, connection classes themselves, and that other
+entities broadcast correct messages.
+
+### Connection Test Case
+
+By default, when you generate new Rails application with Action Cable, a test for the base connection class (`ApplicationCable::Connection`) is generated as well under `test/channels/application_cable` directory.
+
+Connection tests aim to check whether a connection's identifiers get assigned properly
+or that any improper connection requests are rejected. Here is an example:
+
+```ruby
+class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
+ test "connects with params" do
+ # Simulate a connection opening by calling the `connect` method
+ connect params: { user_id: 42 }
+
+ # You can access the Connection object via `connection` in tests
+ assert_equal connection.user_id, "42"
+ end
+
+ test "rejects connection without params" do
+ # Use `assert_reject_connection` matcher to verify that
+ # connection is rejected
+ assert_reject_connection { connect }
+ end
+end
+```
+
+You can also specify request cookies the same way you do in integration tests:
+
+```ruby
+test "connects with cookies" do
+ cookies.signed[:user_id] = "42"
+
+ connect
+
+ assert_equal connection.user_id, "42"
+end
+```
+
+See the API documentation for [`ActionCable::Connection::TestCase`](https://api.rubyonrails.org/classes/ActionCable/Connection/TestCase.html) for more information.
+
+### Channel Test Case
+
+By default, when you generate a channel, an associated test will be generated as well
+under the `test/channels` directory. Here's an example test with a chat channel:
+
+```ruby
+require "test_helper"
+
+class ChatChannelTest < ActionCable::Channel::TestCase
+ test "subscribes and stream for room" do
+ # Simulate a subscription creation by calling `subscribe`
+ subscribe room: "15"
+
+ # You can access the Channel object via `subscription` in tests
+ assert subscription.confirmed?
+ assert_has_stream "chat_15"
+ end
+end
+```
+
+This test is pretty simple and only asserts that the channel subscribes the connection to a particular stream.
+
+You can also specify the underlying connection identifiers. Here's an example test with a web notifications channel:
+
+```ruby
+require "test_helper"
+
+class WebNotificationsChannelTest < ActionCable::Channel::TestCase
+ test "subscribes and stream for user" do
+ stub_connection current_user: users(:john)
+
+ subscribe
+
+ assert_has_stream_for users(:john)
+ end
+end
+```
+
+See the API documentation for [`ActionCable::Channel::TestCase`](https://api.rubyonrails.org/classes/ActionCable/Channel/TestCase.html) for more information.
+
+### Custom Assertions And Testing Broadcasts Inside Other Components
+
+Action Cable ships with a bunch of custom assertions that can be used to lessen the verbosity of tests. For a full list of available assertions, see the API documentation for [`ActionCable::TestHelper`](https://api.rubyonrails.org/classes/ActionCable/TestHelper.html).
+
+It's a good practice to ensure that the correct message has been broadcasted inside other components (e.g. inside your controllers). This is precisely where
+the custom assertions provided by Action Cable are pretty useful. For instance,
+within a model:
+
+```ruby
+require 'test_helper'
+
+class ProductTest < ActionCable::TestCase
+ test "broadcast status after charge" do
+ assert_broadcast_on("products:#{product.id}", type: "charged") do
+ product.charge(account)
+ end
+ end
+end
+```
+
+If you want to test the broadcasting made with `Channel.broadcast_to`, you shoud use
+`Channel.broadcasting_for` to generate an underlying stream name:
+
+```ruby
+# app/jobs/chat_relay_job.rb
+class ChatRelayJob < ApplicationJob
+ def perform_later(room, message)
+ ChatChannel.broadcast_to room, text: message
+ end
+end
+
+# test/jobs/chat_relay_job_test.rb
+require 'test_helper'
+
+class ChatRelayJobTest < ActiveJob::TestCase
+ include ActionCable::TestHelper
+
+ test "broadcast message to room" do
+ room = rooms(:all)
+
+ assert_broadcast_on(ChatChannel.broadcasting_for(room), text: "Hi!") do
+ ChatRelayJob.perform_now(room, "Hi!")
+ end
+ end
+end
+```
+
Additional Testing Resources
----------------------------
@@ -1275,18 +1873,18 @@ Additional Testing Resources
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:
+Here is an example using the [`travel_to`](https://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_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.
```
-Please see [`ActiveSupport::TimeHelpers` API Documentation](http://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html)
+Please see [`ActiveSupport::Testing::TimeHelpers` API Documentation](https://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 0c1e00100b..79bad8f4ed 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.
@@ -22,12 +22,12 @@ When changing Rails versions, it's best to move slowly, one minor version at a t
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
+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.
+Repeat this process until you reach your target Rails version. Each time you move versions, you will need to change the Rails version number in the `Gemfile` (and possibly other gem versions) and run `bundle update`. Then run the Update task mentioned below to update configuration files, then run your tests.
You can find a list of all released Rails versions [here](https://rubygems.org/gems/rails/versions).
@@ -35,17 +35,18 @@ You can find a list of all released Rails versions [here](https://rubygems.org/g
Rails generally stays close to the latest released Ruby version when it's released:
+* Rails 6 requires Ruby 2.5.0 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 Task
+### The Update Task
-Rails provides the `app:update` task. After updating the Rails version
-in the Gemfile, run this 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.
@@ -65,16 +66,142 @@ 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`.
+
+### ActionCable javascript API Changes
+
+The ActionCable javascript package has been converted from CoffeeScript
+to ES2015, and we now publish the source code in the npm distribution.
+
+This change includes some breaking changes to optional parts of the
+ActionCable javascript API:
+
+- Configuration of the WebSocket adapter and logger adapter have been moved
+ from properties of `ActionCable` to properties of `ActionCable.adapters`.
+ If you are currently configuring these adapters you will need to make
+ these changes when upgrading:
+
+ ```diff
+ - ActionCable.WebSocket = MyWebSocket
+ + ActionCable.adapters.WebSocket = MyWebSocket
+ ```
+ ```diff
+ - ActionCable.logger = myLogger
+ + ActionCable.adapters.logger = myLogger
+ ```
+
+- The `ActionCable.startDebugging()` and `ActionCable.stopDebugging()`
+ methods have been removed and replaced with the property
+ `ActionCable.logger.enabled`. If you are currently using these methods you
+ will need to make these changes when upgrading:
+
+ ```diff
+ - ActionCable.startDebugging()
+ + ActionCable.logger.enabled = true
+ ```
+ ```diff
+ - ActionCable.stopDebugging()
+ + ActionCable.logger.enabled = 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
-------------------------------------
-### Ruby 2.2.2+
+For more information on changes made to Rails 5.0 please see the [release notes](5_0_release_notes.html).
-ToDo...
+### Ruby 2.2.2+ required
-### Active Record models now inherit from ApplicationRecord by default
+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.
-In Rails 4.2 an Active Record model inherits from `ActiveRecord::Base`. In Rails 5.0,
+### 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
@@ -82,7 +209,7 @@ 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
+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:
```
@@ -91,7 +218,9 @@ class ApplicationRecord < ActiveRecord::Base
end
```
-### Halting callback chains via `throw(:abort)`
+Then make sure that all your models inherit from it.
+
+### Halting Callback Chains via `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,
@@ -116,12 +245,12 @@ halted the chain when any value was returned.
See [#17227](https://github.com/rails/rails/pull/17227) for more details.
-### ActiveJob jobs now inherit 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:
```
@@ -133,16 +262,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 specific 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
@@ -286,7 +650,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'
@@ -294,7 +658,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](https://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
@@ -344,7 +708,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
@@ -375,7 +739,7 @@ xhr :get, :index, format: :js
to explicitly test an `XmlHttpRequest`.
-Note: Your own `<script>` tags are treated as cross-origin and blocked by
+NOTE: Your own `<script>` tags are treated as cross-origin and blocked by
default, too. If you really mean to load JavaScript from `<script>` tags,
you must now explicitly skip CSRF protection on those actions.
@@ -494,9 +858,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.
@@ -537,7 +901,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
@@ -826,14 +1190,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:
```
@@ -862,8 +1226,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
@@ -874,13 +1238,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.
@@ -941,7 +1305,7 @@ end
### Active Resource
-Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your Gemfile.
+Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your `Gemfile`.
### Active Model
@@ -978,7 +1342,7 @@ 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.
@@ -1037,7 +1401,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:
@@ -1048,6 +1412,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.
@@ -1069,6 +1444,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.
@@ -1137,7 +1516,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
@@ -1217,7 +1596,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
diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md
index c58aee96db..8cf8efefd0 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
@@ -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,
@@ -149,18 +151,22 @@ Because of Unobtrusive JavaScript, the Rails "Ajax helpers" are actually in two
parts: the JavaScript half and the Ruby half.
Unless you have disabled the Asset Pipeline,
-[rails.js](https://github.com/rails/jquery-ujs/blob/master/src/rails.js)
+[rails-ujs](https://github.com/rails/rails/tree/master/actionview/app/assets/javascripts)
provides the JavaScript half, and the regular Ruby view helpers add appropriate
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`](https://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,41 +188,23 @@ 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)
+[`link_to`](https://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
can use like this:
@@ -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,13 +230,13 @@ 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:
+[`button_to`](https://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:
```erb
<%= button_to "An article", @article, remote: true %>
@@ -262,7 +250,150 @@ this generates
</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.
@@ -355,7 +486,7 @@ 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%28%29_method),
Turbolinks will make an Ajax request for the page, parse the response, and
@@ -363,10 +494,6 @@ 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 JavaScript manifest, which is usually
-`app/assets/javascripts/application.js`.
-
If you want to disable Turbolinks for certain links, add a `data-turbolinks="false"`
attribute to the tag:
@@ -385,7 +512,7 @@ $(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
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/package.json b/package.json
new file mode 100644
index 0000000000..e5c71777ba
--- /dev/null
+++ b/package.json
@@ -0,0 +1,12 @@
+{
+ "private": true,
+ "workspaces": [
+ "actioncable",
+ "actiontext",
+ "activestorage",
+ "actionview"
+ ],
+ "dependencies": {
+ "webpack": "^4.17.1"
+ }
+}
diff --git a/rails.gemspec b/rails.gemspec
index 8f9cc181a0..5f2f1dfdcc 100644
--- a/rails.gemspec
+++ b/rails.gemspec
@@ -1,33 +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 = '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.5.0"
+ 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://rubyonrails.org'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "https://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 'actioncable', 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 "actionmailbox", version
+ s.add_dependency "actiontext", version
+ s.add_dependency "railties", version
- s.add_dependency 'bundler', '>= 1.3.0', '< 2.0'
- s.add_dependency 'sprockets-rails', '>= 2.0.0'
+ 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..17a49da08c 100644
--- a/railties/.gitignore
+++ b/railties/.gitignore
@@ -1 +1,6 @@
-log/
+/log/
+/test/500.html
+/test/fixtures/tmp/
+/test/initializer/root/log/
+/test/isolation/assets/yarn.lock
+/tmp/
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 52c2e0743c..c5136b7ab0 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,514 +1,366 @@
-* Make `rails restart` command work with Puma by passing the restart command
- which Puma can use to restart rails server.
-
- *Prathamesh Sonpatki*
-
-* The application generator writes a new file `config/spring.rb`, which tells
- Spring to watch additional common files.
-
- *Xavier Noria*
-
-* The tasks in the rails task namespace is deprecated in favor of app namespace.
- (e.g. `rails:update` and `rails:template` tasks is renamed to `app:update` and `app:template`.)
-
- *Ryo Hashimoto*
-
-* Enable HSTS with IncludeSudomains header for new applications.
-
- *Egor Homakov*, *Prathamesh Sonpatki*
-
-## Rails 5.0.0.beta3 (February 24, 2016) ##
-
-* Alias `rake` with `rails_command` in the Rails Application Templates API
- following Rails 5 convention of preferring "rails" to "rake" to run tasks.
-
- *claudiob*
-
-* Generate applications with an option to log to STDOUT in production
- using the environment variable `RAILS_LOG_TO_STDOUT`.
-
- *Richard Schneeman*
-
-* Change fail fast of `bin/rails test` interrupts run on error.
+* The `connection` option of `rails dbconsole` command is deprecated in
+ favor of `database` option.
*Yuji Yaginuma*
-* The application generator supports `--skip-listen` to opt-out of features
- that depend on the listen gem. As of this writing they are the evented file
- system monitor and the async plugin for spring.
-
-* The Gemfiles of new applications include spring-watcher-listen on Linux and
- Mac OS X (unless --skip-spring).
+* Replace `chromedriver-helper` gem with `webdrivers` in default Gemfile.
+ `chromedriver-helper` is deprecated as of March 31, 2019 and won't
+ receive any further updates.
- *Xavier Noria*
+ *Guillermo Iguaran‮*
-* New applications are generated with the evented file system monitor enabled
- on Linux and Mac OS X.
+* Applications running in `:zeitwerk` mode that use `bootsnap` need
+ to upgrade `bootsnap` to at least 1.4.2.
*Xavier Noria*
-* Add dummy files for apple-touch-icon.png and apple-touch-icon.png.
-
- See #23427.
-
- *Alexey Zabelin*
-
-## Rails 5.0.0.beta2 (February 01, 2016) ##
-
-* Add `after_bundle` callbacks in Rails plugin templates. Useful for allowing
- templates to perform actions that are dependent upon `bundle install`.
+* Add `config.disable_sandbox` option to Rails console.
- *Ryan Manuel*
+ This setting will disable `rails console --sandbox` mode, preventing
+ developer from accidentally starting a sandbox console,
+ which when left inactive, can cause the database server to run out of memory.
-* Bring back `TEST=` env for `rake test` task.
+ *Prem Sichanugrist*
- *Yves Senn*
+* Add `-e/--environment` option to `rails initializers`.
-* Specify log file names or all logs to clear `rake log:clear`
-
- Specify which logs to clear when using the `rake log:clear` task, e.g. `rake log:clear LOGS=test,staging`
-
- Clear all logs from log/*.log e.g. `rake log:clear LOGS=all`
-
- By default `rake log:clear` clears standard environment log files i.e. 'development,test,production'
-
- *Pramod Shinde*
+ *Yuji Yaginuma*
-* Fix using `add_source` with a block after using `gem` in a custom generator.
- *Will Fisher*
+## Rails 6.0.0.beta3 (March 11, 2019) ##
+* Generate random development secrets
-## Rails 5.0.0.beta1 (December 18, 2015) ##
+ A random development secret is now generated to tmp/development_secret.txt
-* Newly generated plugins get a `README.md` in Markdown.
+ This avoids an issue where development mode servers were vulnerable to
+ remote code execution.
- *Yuji Yaginuma*
+ Fixes CVE-2019-5420
-* The generated config file for the development environment includes a new
- config line, commented out, showing how to enable the evented file watcher.
+ *Eileen M. Uchitelle*, *Aaron Patterson*, *John Hawthorn*
- *Xavier Noria*
-* `config.debug_exception_response_format` configures the format used
- in responses when errors occur in development mode.
+## Rails 6.0.0.beta2 (February 25, 2019) ##
- Set `config.debug_exception_response_format` to render an HTML page with
- debug info (using the value `:default`) or render debug info preserving
- the response format (using the value `:api`).
+* Fix non-symbol access to nested hashes returned from `Rails::Application.config_for`
+ being broken by allowing non-symbol access with a deprecation notice.
- *Jorge Bejar*
+ *Ufuk Kayserilioglu*
-* Fix setting exit status code for rake test tasks. The exit status code
- was not set when tests were fired with `rake`. Now, it is being set and it matches
- behavior of running tests via `rails` command (`rails test`), so no matter if
- `rake test` or `rails test` command is used the exit code will be set.
+* Fix deeply nested namespace command printing.
- *Arkadiusz Fal*
+ *Gannon McGibbon*
-* Add Command infrastructure to replace rake.
- Also move `rake dev:cache` to new infrastructure. You'll need to use
- `rails dev:cache` to toggle development caching from now on.
+## Rails 6.0.0.beta1 (January 18, 2019) ##
- *Chuck Callebs*
+* Remove deprecated `after_bundle` helper inside plugins templates.
-* Allow use of minitest-rails gem with Rails test runner.
-
- Fixes #22455.
+ *Rafael Mendonça França*
- *Chris Kottom*
+* Remove deprecated support to old `config.ru` that use the application class as argument of `run`.
-* Add `bin/test` script to rails plugin.
+ *Rafael Mendonça França*
- `bin/test` can use the same API as `bin/rails test`.
+* Remove deprecated `environment` argument from the rails commands.
- *Yuji Yaginuma*
+ *Rafael Mendonça França*
-* Make `static_index` part of the `config.public_file_server` config and
- call it `public_file_server.index_name`.
+* Remove deprecated `capify!`.
- *Yuki Nishijima*
+ *Rafael Mendonça França*
-* Deprecate `serve_static_files` in favor of `public_file_server.enabled`.
+* Remove deprecated `config.secret_token`.
- Unifies the static asset options under `public_file_server`.
+ *Rafael Mendonça França*
- To upgrade, replace occurrences of:
+* Seed database with inline ActiveJob job adapter.
- ```
- config.serve_static_files = # false or true
- ```
+ *Gannon McGibbon*
- in your environment files, with:
+* Add `rails db:system:change` command for changing databases.
```
- config.public_file_server.enabled = # false or true
+ bin/rails db:system:change --to=postgresql
+ force config/database.yml
+ gsub Gemfile
```
- *Kasper Timm Hansen*
+ The change command copies a template `config/database.yml` with
+ the target database adapter into your app, and replaces your database gem
+ with the target database gem.
-* Deprecate `config.static_cache_control` in favor of
- `config.public_file_server.headers`.
+ *Gannon McGibbon*
- To upgrade, replace occurrences of:
+* Add `rails test:channels`.
- ```
- config.static_cache_control = 'public, max-age=60'
- ```
+ *bogdanvlviv*
- in your environment files, with:
+* Use original `bundler` environment variables during the process of generating a new rails project.
- ```
- config.public_file_server.headers = {
- 'Cache-Control' => 'public, max-age=60'
- }
- ```
+ *Marco Costa*
- `config.public_file_server.headers` can set arbitrary headers, sent along when
- a response is delivered.
+* Send Active Storage analysis and purge jobs to dedicated queues by default.
- *Yuki Nishijima*
+ Analysis jobs now use the `:active_storage_analysis` queue, and purge jobs
+ now use the `:active_storage_purge` queue. This matches Action Mailbox,
+ which sends its jobs to dedicated queues by default.
-* Route generator should be idempotent
- running generators several times no longer require you to cleanup routes.rb
+ *George Claghorn*
- *Thiago Pinto*
+* Add `rails test:mailboxes`.
-* Allow passing an environment to `config_for`.
+ *George Claghorn*
- *Simon Eskildsen*
+* Introduce guard against DNS rebinding attacks.
-* Allow rake:stats to account for rake tasks in lib/tasks
+ The `ActionDispatch::HostAuthorization` is a new middleware that prevents
+ against DNS rebinding and other `Host` header attacks. It is included in
+ the development environment by default with the following configuration:
- *Kevin Deisz*
+ Rails.application.config.hosts = [
+ IPAddr.new("0.0.0.0/0"), # All IPv4 addresses.
+ IPAddr.new("::/0"), # All IPv6 addresses.
+ "localhost" # The localhost reserved domain.
+ ]
-* Added javascript to update the URL on mailer previews with the currently
- selected email format. Reloading the page now keeps you on your selected
- format rather than going back to the default html version.
+ In other environments `Rails.application.config.hosts` is empty and no
+ `Host` header checks will be done. If you want to guard against header
+ attacks on production, you have to manually permit the allowed hosts
+ with:
- *James Kerr*
+ Rails.application.config.hosts << "product.com"
-* Add fail fast to `bin/rails test`
+ The host of a request is checked against the `hosts` entries with the case
+ operator (`#===`), which lets `hosts` support entries of type `RegExp`,
+ `Proc` and `IPAddr` to name a few. Here is an example with a regexp.
- Adding `--fail-fast` or `-f` when running tests will interrupt the run on
- the first failure:
+ # Allow requests from subdomains like `www.product.com` and
+ # `beta1.product.com`.
+ Rails.application.config.hosts << /.*\.product\.com/
- ```
- # Running:
+ A special case is supported that allows you to permit all sub-domains:
- ................................................S......E
+ # Allow requests from subdomains like `www.product.com` and
+ # `beta1.product.com`.
+ Rails.application.config.hosts << ".product.com"
- ArgumentError: Wups! Bet you didn't expect this!
- test/models/bunny_test.rb:19:in `block in <class:BunnyTest>'
+ *Genadi Samokovarov*
- bin/rails test test/models/bunny_test.rb:18
+* Remove redundant suffixes on generated helpers.
- ....................................F
+ *Gannon McGibbon*
- This failed
+* Remove redundant suffixes on generated integration tests.
- bin/rails test test/models/bunny_test.rb:14
+ *Gannon McGibbon*
- Interrupted. Exiting...
+* Fix boolean interaction in scaffold system tests.
+ *Gannon McGibbon*
- Finished in 0.051427s, 1808.3872 runs/s, 1769.4972 assertions/s.
+* Remove redundant suffixes on generated system tests.
- ```
+ *Gannon McGibbon*
- Note that any unexpected errors don't abort the run.
+* Add an `abort_on_failure` boolean option to the generator method that shell
+ out (`generate`, `rake`, `rails_command`) to abort the generator if the
+ command fails.
- *Kasper Timm Hansen*
+ *David Rodríguez*
-* Add inline output to `bin/rails test`
+* Remove `app/assets` and `app/javascript` from `eager_load_paths` and `autoload_paths`.
- Any failures or errors (and skips if running in verbose mode) are output
- during a test run:
+ *Gannon McGibbon*
- ```
- # Running:
+* Use Ids instead of memory addresses when displaying references in scaffold views.
- .....S..........................................F
+ Fixes #29200.
- This failed
+ *Rasesh Patel*
- bin/rails test test/models/bunny_test.rb:14
+* 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`).
- .................................E
+ *Gannon McGibbon*
- ArgumentError: Wups! Bet you didn't expect this!
- test/models/bunny_test.rb:19:in `block in <class:BunnyTest>'
+* 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.
- bin/rails test test/models/bunny_test.rb:18
+ *DHH*, *Lachlan Sylvester*
- ....................
+* 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.
- Finished in 0.069708s, 1477.6019 runs/s, 1448.9106 assertions/s.
+ ```
+ bin/rails g model Room capacity:integer --database=kingston
+ invoke active_record
+ create db/kingston_migrate/20180830151055_create_rooms.rb
```
- Output can be deferred to after a run with the `--defer-output` option.
-
- *Kasper Timm Hansen*
-
-* Fix displaying mailer previews on non local requests when config
- `action_mailer.show_previews` is set
+ Because rails scaffolding uses the model generator, you can
+ also specify a database with the scaffold generator.
- *Wojciech Wnętrzak*
+ *Gannon McGibbon*
-* `rails server` will now honour the `PORT` environment variable
+* 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.
- *David Cornu*
+ *Richard Schneeman*
-* Plugins generated using `rails plugin new` are now generated with the
- version number set to 0.1.0.
+* Support environment specific credentials overrides.
- *Daniel Morris*
+ So any environment will look for `config/credentials/#{Rails.env}.yml.enc` and fall back
+ to `config/credentials.yml.enc`.
-* `I18n.load_path` is now reloaded under development so there's no need to
- restart the server to make new locale files available. Also, I18n will no
- longer raise for deleted locale files.
+ The encryption key can be in `ENV["RAILS_MASTER_KEY"]` or `config/credentials/production.key`.
- *Kir Shatrov*
+ Environment credentials overrides can be edited with `rails credentials:edit --environment production`.
+ If no override is set up for the passed environment, it will be created.
-* Add `bin/update` script to update development environment automatically.
+ Additionally, the default lookup paths can be overwritten with these configs:
- *Mehmet Emin İNAÇ*
+ - `config.credentials.content_path`
+ - `config.credentials.key_path`
-* Fix STATS_DIRECTORIES already defined warning when running rake from within
- the top level directory of an engine that has a test app.
+ *Wojciech Wnętrzak*
- Fixes #20510.
+* Make `ActiveSupport::Cache::NullStore` the default cache store in the test environment.
- *Ersin Akinci*
+ *Michael C. Nelson*
-* Make enabling or disabling caching in development mode possible with
- rake dev:cache.
+* Emit warning for unknown inflection rule when generating model.
- Running rake dev:cache will create or remove tmp/caching-dev.txt. When this
- file exists config.action_controller.perform_caching will be set to true in
- config/environments/development.rb.
+ *Yoshiyuki Kinjo*
- Additionally, a server can be started with either --dev-caching or
- --no-dev-caching included to toggle caching on startup.
+* Add `database` (aliased as `db`) option to migration generator.
- *Jussi Mertanen*, *Chuck Callebs*
+ 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.
-* Add a `--api` option in order to generate plugins that can be added
- inside an API application.
+ ```
+ rails g migration CreateHouses --database=kingston
+ invoke active_record
+ create db/kingston_migrate/20180830151055_create_houses.rb
+ ```
- *Robin Dupret*
+ *Eileen M. Uchitelle*
-* Fix `NoMethodError` when generating a scaffold inside a full engine.
+* Deprecate `rake routes` in favor of `rails routes`.
*Yuji Yaginuma*
-* Adding support for passing a block to the `add_source` action of a custom generator
+* Deprecate `rake initializers` in favor of `rails initializers`.
- *Mike Dalton*, *Hirofumi Wakasugi*
+ *Annie-Claude Côté*
-* `assert_file` understands paths with special characters
- (eg. `v0.1.4~alpha+nightly`).
+* Deprecate `rake dev:cache` in favor of `rails dev:cache`.
- *Diego Carrion*
+ *Annie-Claude Côté*
-* Remove ContentLength middleware from the defaults. If you want it, just
- add it as a middleware in your config.
+* Deprecate `rails notes` subcommands in favor of passing an `annotations` argument to `rails notes`.
- *Egg McMuffin*
+ 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`.
-* Make it possible to customize the executable inside rerun snippets.
+ *Annie-Claude Côté*
- *Yves Senn*
+* Deprecate `SOURCE_ANNOTATION_DIRECTORIES` environment variable used by `rails notes`
+ through `Rails::SourceAnnotationExtractor::Annotation` in favor of using `config.annotations.register_directories`.
-* 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.
+ *Annie-Claude Côté*
- *Santiago Pastorino*, *Jorge Bejar*
-
-* Make generated scaffold functional tests work inside engines.
-
- *Yuji Yaginuma*
+* Deprecate `rake notes` in favor of `rails notes`.
-* Generate 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.
+ *Annie-Claude Côté*
- See #20299.
+* Don't generate unused files in `app:update` task.
- *Yoong Kang Lim*, *Sunny Juneja*
+ Skip the assets' initializer when sprockets isn't loaded.
-* `config.static_index` configures directory `index.html` filename
+ Skip `config/spring.rb` when spring isn't loaded.
- 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"`.
+ Skip yarn's contents when yarn integration isn't used.
- *Eliot Sykes*
+ *Tsukuru Tanimichi*
-* `bin/setup` uses built-in rake tasks (`log:clear`, `tmp:clear`).
+* Make the master.key file read-only for the owner upon generation on
+ POSIX-compliant systems.
- *Mohnish Thallavajhula*
+ Previously:
-* Fix mailer previews with attachments by using the mail gem's own API to
- locate the first part of the correct mime type.
+ $ ls -l config/master.key
+ -rw-r--r-- 1 owner group 32 Jan 1 00:00 master.key
- Fixes #14435.
+ Now:
- *Andrew White*
+ $ ls -l config/master.key
+ -rw------- 1 owner group 32 Jan 1 00:00 master.key
-* Remove sqlite support from `rails dbconsole`.
+ Fixes #32604.
- *Andrew White*
+ *Jose Luis Duran*
-* Rename `railties/bin` to `railties/exe` to match the new Bundler executables
- convention.
+* Deprecate support for using the `HOST` environment variable to specify the server IP.
- *Islam Wazery*
+ The `BINDING` environment variable should be used instead.
-* Print `bundle install` output in `rails new` as soon as it's available.
-
- Running `rails new` will now print the output of `bundle install` as
- it is available, instead of waiting until all gems finish installing.
-
- *Max Holder*
-
-* Respect `pluralize_table_names` when generating fixture file.
-
- Fixes #19519.
+ Fixes #29516.
*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*
-
-* Created rake restart task. Restarts your Rails app by touching the
- `tmp/restart.txt`.
-
- See #18876.
-
- *Hyonjee Joo*
-
-* Add `config/initializers/active_record_belongs_to_required_by_default.rb`.
-
- 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.
-
- As a result, new Rails apps require `belongs_to` association on model
- to be valid.
-
- This initializer is *not* added when running `rake rails:update`, so
- old apps ported to Rails 5 will work without any change.
-
- *Josef Šimánek*
-
-* `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.
-
- Fixes #16433.
-
- *Guo Xiang Tan*
-
-* Newly generated applications get a `README.md` in Markdown.
-
- *Xavier Noria*
-
-* Remove the documentation tasks `doc:app`, `doc:rails`, and `doc:guides`.
-
- *Xavier Noria*
-
-* Force generated routes to be inserted into `config/routes.rb`.
+* Deprecate passing Rack server name as a regular argument to `rails server`.
- *Andrew White*
+ Previously:
-* Don't remove all line endings from `config/routes.rb` when revoking scaffold.
+ $ bin/rails server thin
- Fixes #15913.
+ There wasn't an explicit option for the Rack server to use, now we have the
+ `--using` option with the `-u` short switch.
- *Andrew White*
+ Now:
-* Rename `--skip-test-unit` option to `--skip-test` in app generator
+ $ bin/rails server -u thin
- *Melanie Gilman*
+ This change also improves the error message if a missing or mistyped rack
+ server is given.
-* Add the `method_source` gem to the default Gemfile for apps.
+ *Genadi Samokovarov*
- *Sean Griffin*
+* Add "rails routes --expanded" option to output routes in expanded mode like
+ "psql --expanded". Result looks like:
-* Drop old test locations from `rake stats`:
-
- - 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.
-
- *Rafael Mendonça França*
-
-* Use local variables in `_form.html.erb` partial generated by scaffold.
-
- *Andrew Kozlov*
-
-* Add `config/initializers/callback_terminator.rb`.
-
- Newly generated Rails apps have a new initializer called
- `callback_terminator.rb` which sets the value of the configuration option
- `ActiveSupport.halt_callback_chains_on_return_false` to `false`.
-
- As a result, new Rails apps do not halt Active Record and Active Model
- callback chains when a callback returns `false`; only when they are
- explicitly halted with `throw(:abort)`.
-
- 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.
-
- *claudiob*
-
-* Generated fixtures won't use the id when generated with references attributes.
-
- *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.5.0 or newer.
-* Autoload any second level directories called `app/*/concerns`.
+ *Jeremy Daer*, *Kasper Timm Hansen*
- *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 1f496cf280..ea8823d7ef 100644
--- a/railties/MIT-LICENSE
+++ b/railties/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2016 David Heinemeier Hansson
+Copyright (c) 2004-2019 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc
index 26a25ee9dc..f1783a4737 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)}[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: 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; {Action Mailbox}[link:files/actionmailbox/README_md.html], a library to receive emails within a Rails application;
+{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; {Action Text}[link:files/actiontext/README_md.html], a library to handle rich text content;
+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:
-
- "Yay! You’re 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://www.railstutorial.org/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}[https://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}[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}[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..51437e3147 100644
--- a/railties/README.rdoc
+++ b/railties/README.rdoc
@@ -23,13 +23,13 @@ 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
API documentation is at
-* http://api.rubyonrails.org
+* https://api.rubyonrails.org
Bug reports can be filed for the Ruby on Rails project here:
@@ -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 3421d9b464..4ae546b202 100644
--- a/railties/Rakefile
+++ b/railties/Rakefile
@@ -1,35 +1,97 @@
-require 'rake/testtask'
+# frozen_string_literal: true
-task :default => :test
+require "rake/testtask"
+
+task default: :test
task :package
-task "package:clean"
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_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
+ test_options = ENV["TESTOPTS"].to_s.split(/[\s]+/)
+
+ test_patterns = dirs.map { |dir| "test/#{dir}/*_test.rb" }
+ test_files = Dir[*test_patterns].select do |file|
+ !file.start_with?("test/fixtures/") && !file.start_with?("test/isolation/assets/")
+ end.sort
+
+ if ENV["BUILDKITE_PARALLEL_JOB_COUNT"]
+ n = ENV["BUILDKITE_PARALLEL_JOB"].to_i
+ m = ENV["BUILDKITE_PARALLEL_JOB_COUNT"].to_i
+
+ test_files = test_files.each_slice(m).map { |slice| slice[n] }.compact
+ end
+
+ test_files.each do |file|
+ puts "--- #{file}"
+ fake_command = Shellwords.join([
+ FileUtils::RUBY,
+ "-w",
+ *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
+ puts "^^^ +++"
+ end
+ end
+
+ puts "--- All tests completed"
+ unless failing_files.empty?
+ puts "^^^ +++"
+ puts
+ puts "Failed in:"
+ failing_files.each do |file|
+ puts " #{file}"
+ end
+ puts
+
+ exit 1
end
end
end
-Rake::TestTask.new('test:regular') do |t|
- t.libs << 'test' << "#{File.dirname(__FILE__)}/../activesupport/lib"
- t.pattern = 'test/**/*_test.rb'
- t.warning = false
+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)
end
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..4b7df6360a
--- /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
+
+ # Backwards compatibility with Rails 5.0 generated plugin test scripts
+ mattr_reader :run_via, default: {}
+end
diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb
index fe789f3c2a..1f533a8c04 100644
--- a/railties/lib/rails.rb
+++ b/railties/lib/rails.rb
@@ -1,18 +1,22 @@
-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 "rails/autoloaders"
+
+require "active_support/railtie"
+require "action_dispatch/railtie"
# UTF-8 is the default internal and external encoding.
silence_warnings do
@@ -46,14 +50,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 +71,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 +81,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 +90,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,12 +104,16 @@ 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>
def public_path
application && Pathname.new(application.paths["public"].first)
end
+
+ def autoloaders
+ Autoloaders
+ end
end
end
diff --git a/railties/lib/rails/all.rb b/railties/lib/rails/all.rb
index 11f4d5c4bc..da810f1eed 100644
--- a/railties/lib/rails/all.rb
+++ b/railties/lib/rails/all.rb
@@ -1,17 +1,24 @@
+# frozen_string_literal: true
+
+# rubocop:disable Style/RedundantBegin
+
require "rails"
%w(
active_record/railtie
+ active_storage/engine
action_controller/railtie
action_view/railtie
action_mailer/railtie
active_job/railtie
action_cable/engine
+ action_mailbox/engine
+ action_text/engine
rails/test_unit/railtie
sprockets/railtie
).each do |railtie|
begin
- require "#{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 d478bbf9e8..d5312843e4 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,41 +37,69 @@ 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
)
},
- 'actioncable' => {
- :include => %w(
+ "actioncable" => {
+ include: %w(
README.md
lib/action_cable/**/*.rb
)
},
- 'railties' => {
- :include => %w(
+ "activestorage" => {
+ include: %w(
+ README.md
+ app/**/active_storage/**/*.rb
+ lib/active_storage/**/*.rb
+ )
+ },
+
+ "actionmailbox" => {
+ include: %w(
+ README.md
+ app/**/action_mailbox/**/*.rb
+ lib/action_mailbox/**/*.rb
+ )
+ },
+
+ "actiontext" => {
+ include: %w(
+ README.md
+ app/**/action_text/**/*.rb
+ lib/action_text/**/*.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
+ )
}
}
@@ -80,7 +110,7 @@ module Rails
# Be lazy computing stuff to have as light impact as possible to
# the rest of tasks.
before_running_rdoc do
- load_and_configure_sdoc
+ configure_sdoc
configure_rdoc_files
setup_horo_variables
end
@@ -91,20 +121,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
@@ -121,22 +146,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)
@@ -144,7 +182,7 @@ module Rails
end
def api_dir
- 'doc/rdoc'
+ "doc/rdoc"
end
end
@@ -156,7 +194,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 af004d85bf..cc057a407d 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
+inadvertently 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
- rails app:update:bin # Use the new Rails 5 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 4729ddcf62..038284ebdd 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -1,10 +1,15 @@
-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 "active_support/hash_with_indifferent_access"
+require "rails/engine"
+require "rails/secrets"
module Rails
# An Engine with the responsibility of coordinating the whole boot process.
@@ -73,21 +78,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
+ # 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
@@ -147,7 +153,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)
@@ -168,16 +173,9 @@ module Rails
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
- unless secrets.secret_key_base.kind_of?(String)
- raise ArgumentError, "`secret_key_base` for #{Rails.env} environment must be a type of String, change this value in `config/secrets.yml`"
- end
- key_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000)
- ActiveSupport::CachingKeyGenerator.new(key_generator)
- else
- ActiveSupport::LegacyKeyGenerator.new(secrets.secret_token)
- end
+ @caching_key_generator ||= ActiveSupport::CachingKeyGenerator.new(
+ ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
+ )
end
# Returns a message verifier object.
@@ -230,7 +228,12 @@ module Rails
if yaml.exist?
require "erb"
- (YAML.load(ERB.new(yaml.read).result) || {})[env] || {}
+ config = YAML.load(ERB.new(yaml.read).result) || {}
+ config = (config["shared"] || {}).merge(config[env] || {})
+
+ ActiveSupport::OrderedOptions.new.tap do |options|
+ options.update(NonSymbolAccessDeprecatedHash.new(config))
+ end
else
raise "Could not load configuration. No such file - #{yaml}"
end
@@ -244,13 +247,10 @@ 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,
@@ -260,14 +260,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
@@ -275,7 +284,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
@@ -318,7 +327,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
@@ -348,7 +357,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
@@ -365,9 +374,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.
#
@@ -386,25 +393,83 @@ 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
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 development and test, this is randomly generated and stored in a
+ # temporary file in <tt>tmp/development_secret.txt</tt>.
+ #
+ # 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.development? || Rails.env.test?
+ secrets.secret_key_base ||= generate_development_secret
+ 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:
@@ -505,28 +570,90 @@ 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`"
+ else
+ 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 generate_development_secret
+ if secrets.secret_key_base.nil?
+ key_file = Rails.root.join("tmp/development_secret.txt")
- def build_middleware
- config.app_middleware + super
- end
+ if !File.exist?(key_file)
+ random_key = SecureRandom.hex(64)
+ FileUtils.mkdir_p(key_file.dirname)
+ File.binwrite(key_file, random_key)
+ end
+
+ secrets.secret_key_base = File.binread(key_file)
+ end
+
+ secrets.secret_key_base
+ end
+
+ 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
+
+ class NonSymbolAccessDeprecatedHash < HashWithIndifferentAccess # :nodoc:
+ def initialize(value = nil)
+ if value.is_a?(Hash)
+ value.each_pair { |k, v| self[k] = v }
+ else
+ super
+ end
+ end
+
+ def []=(key, value)
+ regular_writer(key.to_sym, convert_value(value, for: :assignment))
+ end
+
+ private
+
+ def convert_key(key)
+ unless key.kind_of?(Symbol)
+ ActiveSupport::Deprecation.warn(<<~MESSAGE.squish)
+ Accessing hashes returned from config_for by non-symbol keys
+ is deprecated and will be removed in Rails 6.1.
+ Use symbols for access instead.
+ MESSAGE
+
+ key = key.to_sym
+ end
+
+ key
+ end
+
+ def convert_value(value, options = {}) # :doc:
+ if value.is_a? Hash
+ if options[:for] == :to_hash
+ value.to_hash
+ else
+ self.class.new(value)
+ end
+ elsif value.is_a?(Array)
+ if options[:for] != :assignment || value.frozen?
+ value = value.dup
+ end
+ value.map! { |e| convert_value(e, options) }
+ else
+ value
+ end
+ end
+ end
end
end
diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb
index 9baf8aa742..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
@@ -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 65cff1561a..b79dbdbc6f 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -1,10 +1,10 @@
-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/deprecation'
-require 'active_support/core_ext/string/strip' # for strip_heredoc
+require "ipaddr"
+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
@@ -12,77 +12,141 @@ module Rails
attr_accessor :allow_concurrency, :asset_host, :autoflush_log,
:cache_classes, :cache_store, :consider_all_requests_local, :console,
: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,
+ :force_ssl, :helpers_paths, :hosts, :logger, :log_formatter, :log_tags,
+ :railties_order, :relative_url_root, :secret_key_base,
: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,
+ :disable_sandbox
- attr_writer :log_level
- attr_reader :encoding, :api_only, :static_cache_control
+ attr_reader :encoding, :api_only, :loaded_config_version, :autoloader
def initialize(*)
super
- self.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 = :cookie_store
- @session_options = {}
- @time_zone = "UTC"
- @beginning_of_week = :monday
- @log_level = nil
- @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
- end
-
- def static_cache_control=(value)
- ActiveSupport::Deprecation.warn <<-eow.strip_heredoc
- `static_cache_control` is deprecated and will be removed in Rails 5.1.
- Please use
- `config.public_file_server.headers = { 'Cache-Control' => '#{value}' }`
- instead.
- eow
-
- @static_cache_control = value
- end
-
- def serve_static_files
- ActiveSupport::Deprecation.warn <<-eow.strip_heredoc
- `serve_static_files` is deprecated and will be removed in Rails 5.1.
- Please use `public_file_server.enabled` instead.
- eow
-
- @public_file_server.enabled
- end
-
- def serve_static_files=(value)
- ActiveSupport::Deprecation.warn <<-eow.strip_heredoc
- `serve_static_files` is deprecated and will be removed in Rails 5.1.
- Please use `public_file_server.enabled = #{value}` instead.
- eow
-
- @public_file_server.enabled = value
+ self.encoding = Encoding::UTF_8
+ @allow_concurrency = nil
+ @consider_all_requests_local = false
+ @filter_parameters = []
+ @filter_redirect = []
+ @helpers_paths = []
+ @hosts = Array(([IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0"), ".localhost"] if Rails.env.development?))
+ @public_file_server = ActiveSupport::OrderedOptions.new
+ @public_file_server.enabled = true
+ @public_file_server.index_name = "index"
+ @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_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
+ @autoloader = :classic
+ @disable_sandbox = false
+ 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
+ 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"
+
+ self.autoloader = :zeitwerk if RUBY_ENGINE == "ruby"
+
+ 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
+
+ if respond_to?(:action_mailer)
+ action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"
+ end
+
+ if respond_to?(:active_job)
+ active_job.return_false_on_aborted_enqueue = true
+ end
+
+ if respond_to?(:active_storage)
+ active_storage.queues.analysis = :active_storage_analysis
+ active_storage.queues.purge = :active_storage_purge
+ end
+ else
+ raise "Unknown version #{target_version.to_s.inspect}"
+ end
+
+ @loaded_config_version = target_version
end
def encoding=(value)
@@ -104,15 +168,13 @@ module Rails
@debug_exception_response_format || :default
end
- def debug_exception_response_format=(value)
- @debug_exception_response_format = value
- 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"
@@ -124,8 +186,28 @@ module Rails
end
end
+ # Load the database YAML without evaluating ERB. This allows us to
+ # create the rake tasks for multiple databases without filling in the
+ # configuration values or loading the environment. Do not use this
+ # method.
+ #
+ # This uses a DummyERB custom compiler so YAML can ignore the ERB
+ # tags and load the database.yml for the rake tasks.
+ def load_database_yaml # :nodoc:
+ if path = paths["config/database"].existent.first
+ require "rails/application/dummy_erb_compiler"
+
+ yaml = Pathname.new(path)
+ erb = DummyERB.new(yaml.read)
+
+ YAML.load(erb.result)
+ else
+ {}
+ end
+ 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
@@ -133,8 +215,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.
{}
@@ -148,11 +237,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
@@ -161,34 +246,62 @@ 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
+
+ def autoloader=(autoloader)
+ case autoloader
+ when :classic
+ @autoloader = autoloader
+ when :zeitwerk
+ require "zeitwerk"
+ @autoloader = autoloader
+ else
+ raise ArgumentError, "config.autoloader may be :classic or :zeitwerk, got #{autoloader.inspect} instead"
+ end
end
class Custom #:nodoc:
@@ -205,7 +318,32 @@ module Rails
}
end
end
+
+ def respond_to_missing?(symbol, *)
+ true
+ end
end
+
+ private
+ def default_credentials_content_path
+ if credentials_available_for_current_env?
+ root.join("config", "credentials", "#{Rails.env}.yml.enc")
+ else
+ root.join("config", "credentials.yml.enc")
+ end
+ end
+
+ def default_credentials_key_path
+ if credentials_available_for_current_env?
+ root.join("config", "credentials", "#{Rails.env}.key")
+ else
+ root.join("config", "master.key")
+ end
+ end
+
+ def credentials_available_for_current_env?
+ File.exist?(root.join("config", "credentials", "#{Rails.env}.yml.enc"))
+ end
end
end
end
diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb
index 381e548730..193cc59f3a 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,7 +12,9 @@ module Rails
end
def build_stack
- ActionDispatch::MiddlewareStack.new.tap do |middleware|
+ ActionDispatch::MiddlewareStack.new do |middleware|
+ middleware.use ::ActionDispatch::HostAuthorization, config.hosts, config.action_dispatch.hosts_response_app
+
if config.force_ssl
middleware.use ::ActionDispatch::SSL, config.ssl_options
end
@@ -19,7 +23,6 @@ module Rails
if config.public_file_server.enabled
headers = config.public_file_server.headers || {}
- headers['Cache-Control'.freeze] = config.static_cache_control if config.static_cache_control
middleware.use ::ActionDispatch::Static, paths["public"].first, index: config.public_file_server.index_name, headers: headers
end
@@ -41,12 +44,11 @@ module Rails
middleware.use ::Rack::Runtime
middleware.use ::Rack::MethodOverride unless config.api_only
middleware.use ::ActionDispatch::RequestId
+ middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
- # Must come after Rack::MethodOverride to properly log overridden methods
middleware.use ::Rails::Rack::Logger, config.log_tags
middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app
middleware.use ::ActionDispatch::DebugExceptions, app, config.debug_exception_response_format
- middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
unless config.cache_classes
middleware.use ::ActionDispatch::Reloader, app.reloader
@@ -63,9 +65,15 @@ module Rails
middleware.use ::ActionDispatch::Flash
end
+ unless config.api_only
+ middleware.use ::ActionDispatch::ContentSecurityPolicy::Middleware
+ end
+
middleware.use ::Rack::Head
middleware.use ::Rack::ConditionalGet
middleware.use ::Rack::ETag, "no-cache"
+
+ middleware.use ::Rack::TempfileReaper unless config.api_only
end
end
@@ -76,9 +84,9 @@ module Rails
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/dummy_erb_compiler.rb b/railties/lib/rails/application/dummy_erb_compiler.rb
new file mode 100644
index 0000000000..c4659123bb
--- /dev/null
+++ b/railties/lib/rails/application/dummy_erb_compiler.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# These classes are used to strip out the ERB configuration
+# values so we can evaluate the database.yml without evaluating
+# the ERB values.
+class DummyERB < ERB # :nodoc:
+ def make_compiler(trim_mode)
+ DummyCompiler.new trim_mode
+ end
+end
+
+class DummyCompiler < ERB::Compiler # :nodoc:
+ def compile_content(stag, out)
+ case stag
+ when "<%="
+ out.push "_erbout << 'dummy_compiler'"
+ end
+ end
+end
diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb
index 34f2265108..8d2c13d2a8 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
@@ -19,17 +21,35 @@ module Rails
end
end
+ initializer :let_zeitwerk_take_over do
+ if config.autoloader == :zeitwerk
+ require "active_support/dependencies/zeitwerk_integration"
+ ActiveSupport::Dependencies::ZeitwerkIntegration.take_over
+ end
+ end
+
initializer :add_builtin_route do |app|
if Rails.env.development?
+ app.routes.prepend do
+ 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", internal: true
- get '/rails/info/routes' => "rails/info#routes", internal: true
- get '/rails/info' => "rails/info#index", internal: true
- get '/' => "rails/welcome#index", internal: true
+ 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
@@ -45,7 +65,7 @@ module Rails
end
# This needs to happen before eager load so it happens
- # in exactly the same point regardless of config.cache_classes
+ # in exactly the same point regardless of config.eager_load
initializer :run_prepare_callbacks do |app|
app.reloader.prepare!
end
@@ -53,6 +73,10 @@ module Rails
initializer :eager_load! do
if config.eager_load
ActiveSupport.run_load_hooks(:before_eager_load, self)
+ # Checks defined?(Zeitwerk) instead of zeitwerk_enabled? because we
+ # want to eager load any dependency managed by Zeitwerk regardless of
+ # the autoloading mode of the application.
+ Zeitwerk::Loader.eager_load_all if defined?(Zeitwerk)
config.eager_load_namespaces.each(&:eager_load!)
end
end
@@ -62,22 +86,40 @@ 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
- mutex = Mutex.new
- app.executor.to_run(prepend: true) do
- mutex.lock
- end
- app.executor.to_complete(:after) do
- mutex.unlock
- end
+ app.executor.register_hook(MutexHook.new, outer: true)
elsif config.allow_concurrency == :unsafe
# Do nothing, even if we know this is dangerous. This is the
- # historical behaviour for true.
+ # historical behavior for true.
else
# Default concurrency setting: enabled, but safe
@@ -86,12 +128,7 @@ module Rails
# Without cache_classes + eager_load, the load interlock
# is required for proper operation
- app.executor.to_run(prepend: true) do
- ActiveSupport::Dependencies.interlock.start_running
- end
- app.executor.to_complete(:after) do
- ActiveSupport::Dependencies.interlock.done_running
- end
+ app.executor.register_hook(InterlockHook, outer: true)
end
end
end
@@ -100,8 +137,9 @@ module Rails
# the hook are taken into account.
initializer :set_routes_reloader_hook do |app|
reloader = routes_reloader
- reloader.execute_if_updated
- self.reloaders << reloader
+ 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
@@ -137,7 +175,7 @@ module Rails
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
@@ -160,7 +198,7 @@ module Rails
# 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 618a09a5b3..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 html: '<p>For security purposes, this information is only available to local requests.</p>'.html_safe, 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/autoloaders.rb b/railties/lib/rails/autoloaders.rb
new file mode 100644
index 0000000000..1bd3f18a74
--- /dev/null
+++ b/railties/lib/rails/autoloaders.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "active_support/dependencies/zeitwerk_integration"
+
+module Rails
+ module Autoloaders # :nodoc:
+ class << self
+ include Enumerable
+
+ def main
+ if zeitwerk_enabled?
+ @main ||= Zeitwerk::Loader.new.tap do |loader|
+ loader.tag = "rails.main"
+ loader.inflector = ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector
+ end
+ end
+ end
+
+ def once
+ if zeitwerk_enabled?
+ @once ||= Zeitwerk::Loader.new.tap do |loader|
+ loader.tag = "rails.once"
+ loader.inflector = ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector
+ end
+ end
+ end
+
+ def each
+ if zeitwerk_enabled?
+ yield main
+ yield once
+ end
+ end
+
+ def logger=(logger)
+ each { |loader| loader.logger = logger }
+ end
+
+ def zeitwerk_enabled?
+ Rails.configuration.autoloader == :zeitwerk
+ end
+ end
+ 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 7a8f42fe94..09082282f3 100644
--- a/railties/lib/rails/code_statistics.rb
+++ b/railties/lib/rails/code_statistics.rb
@@ -1,16 +1,18 @@
-require 'rails/code_statistics_calculator'
-require 'active_support/core_ext/enumerable'
+# frozen_string_literal: true
-class CodeStatistics #:nodoc:
+require "rails/code_statistics_calculator"
+require "active_support/core_ext/enumerable"
- TEST_TYPES = ['Controller tests',
- 'Helper tests',
- 'Model tests',
- 'Mailer tests',
- 'Job tests',
- 'Integration tests']
+class CodeStatistics #:nodoc:
+ TEST_TYPES = ["Controller tests",
+ "Helper tests",
+ "Model tests",
+ "Mailer tests",
+ "Job tests",
+ "Integration tests",
+ "System tests"]
- HEADERS = {lines: ' Lines', code_lines: ' LOC', classes: 'Classes', methods: 'Methods'}
+ HEADERS = { lines: " Lines", code_lines: " LOC", classes: "Classes", methods: "Methods" }
def initialize(*pairs)
@pairs = pairs
@@ -33,7 +35,7 @@ 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|rake)$/)
@@ -44,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
@@ -71,30 +73,30 @@ class CodeStatistics #:nodoc:
end
def width_for(label)
- [@statistics.values.sum {|s| s.send(label) }.to_s.size, HEADERS[label].length].max
+ [@statistics.values.sum { |s| s.send(label) }.to_s.size, HEADERS[label].length].max
end
def print_header
print_splitter
- print '| Name '
+ print "| Name "
HEADERS.each do |k, v|
print " | #{v.rjust(width_for(k))}"
end
- puts ' | M/C | LOC/M |'
+ puts " | M/C | LOC/M |"
print_splitter
end
def print_splitter
- print '+----------------------'
+ print "+----------------------"
HEADERS.each_key do |k|
print "+#{'-' * (width_for(k) + 2)}"
end
- puts '+-----+-------+'
+ 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
+ m_over_c = (statistics.methods / statistics.classes) rescue 0
+ loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue 0
print "| #{name.ljust(20)} "
HEADERS.each_key do |k|
@@ -107,7 +109,7 @@ class CodeStatistics #:nodoc:
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 fad13e8517..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
@@ -43,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
@@ -77,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..50651ad61a
--- /dev/null
+++ b/railties/lib/rails/command/actions.rb
@@ -0,0 +1,54 @@
+# 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_application!
+ require_environment!
+ end
+
+ def require_application!
+ require ENGINE_PATH if defined?(ENGINE_PATH)
+
+ if defined?(APP_PATH)
+ require APP_PATH
+ end
+ end
+
+ def require_environment!
+ if defined?(APP_PATH)
+ Rails.application.require_environment!
+ end
+ end
+
+ 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..a22b198c66
--- /dev/null
+++ b/railties/lib/rails/command/base.rb
@@ -0,0 +1,165 @@
+# 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(relative_command_path, __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)).join(":")
+ end
+
+ def relative_command_path
+ File.join("../commands", *command_root_namespace.split(":"))
+ end
+
+ def namespaced_commands
+ commands.keys.map do |key|
+ if command_root_namespace.match?(/(\A|\:)#{key}\z/)
+ command_root_namespace
+ else
+ "#{command_root_namespace}:#{key}"
+ end
+ 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..7fb2a99e67
--- /dev/null
+++ b/railties/lib/rails/command/behavior.rb
@@ -0,0 +1,84 @@
+# 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|
+ path = path.sub("#{base}/", "")
+ require path
+ rescue Exception
+ # No problem
+ 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(":")
+ path = pieces.join("/")
+ paths << "#{path}/#{pieces.last}"
+ paths << path
+ 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..0cb3f1ce1e
--- /dev/null
+++ b/railties/lib/rails/command/environment_argument.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "active_support"
+require "active_support/core_ext/class/attribute"
+
+module Rails
+ module Command
+ module EnvironmentArgument #:nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :environment_desc, default: "Specifies the environment to run this #{self.command_name} under (test/development/production)."
+ class_option :environment, aliases: "-e", type: :string, desc: environment_desc
+ end
+
+ private
+ def extract_environment_option_from_argument(default_environment: Rails::Command.environment)
+ if options[:environment]
+ self.options = options.merge(environment: acceptable_environment(options[:environment]))
+ else
+ self.options = options.merge(environment: default_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 5a66b78a92..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",
@@ -13,6 +15,4 @@ aliases = {
command = ARGV.shift
command = aliases[command] || command
-require 'rails/commands/commands_tasks'
-
-Rails::CommandsTasks.new(ARGV).run_command!(command)
+Rails::Command.invoke command, ARGV
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 da3b9452d5..0000000000
--- a/railties/lib/rails/commands/commands_tasks.rb
+++ /dev/null
@@ -1,180 +0,0 @@
-require 'rails/commands/rake_proxy'
-
-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:
- include Rails::RakeProxy
-
- 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"
-
-All commands can be run with -h (or --help) for more information.
-
-In addition to those commands, there are:
-EOT
-
- ADDITIONAL_COMMANDS = [
- [ '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")' ]
- ]
-
- 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
- run_rake_task(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
- write_commands ADDITIONAL_COMMANDS + formatted_rake_tasks
- 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
-
- def write_commands(commands)
- width = commands.map { |name, _| name.size }.max || 10
- commands.each { |command| printf(" %-#{width}s %s\n", *command) }
- 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..7a9eaefea1
--- /dev/null
+++ b/railties/lib/rails/commands/console/console_command.rb
@@ -0,0 +1,106 @@
+# 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?
+
+ if sandbox? && app.config.disable_sandbox
+ puts "Error: Unable to start console in sandbox mode as sandbox mode is disabled (config.disable_sandbox is true)."
+ exit 1
+ end
+
+ 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..c8d3fb9eda
--- /dev/null
+++ b/railties/lib/rails/commands/credentials/USAGE
@@ -0,0 +1,58 @@
+=== 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
+
+The `credentials` command supports passing an `--environment` option to create an
+environment specific override. That override will take precedence over the
+global `config/credentials.yml.enc` file when running in that environment. So:
+
+ rails credentials:edit --environment development
+
+will create `config/credentials/development.yml.enc` with the corresponding
+encryption key in `config/credentials/development.key` if the credentials file
+doesn't exist.
+
+The encryption key can also be put in `ENV["RAILS_MASTER_KEY"]`, which takes
+precedence over the file encryption key.
+
+In addition to that, the default credentials lookup paths can be overridden through
+`config.credentials.content_path` and `config.credentials.key_path`.
diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb
new file mode 100644
index 0000000000..e23a1b3008
--- /dev/null
+++ b/railties/lib/rails/commands/credentials/credentials_command.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require "active_support"
+require "rails/command/helpers/editor"
+require "rails/command/environment_argument"
+
+module Rails
+ module Command
+ class CredentialsCommand < Rails::Command::Base # :nodoc:
+ include Helpers::Editor
+ include EnvironmentArgument
+
+ self.environment_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
+ extract_environment_option_from_argument(default_environment: nil)
+ require_application!
+
+ ensure_editor_available(command: "bin/rails credentials:edit") || (return)
+
+ ensure_encryption_key_has_been_added if credentials.key.nil?
+ ensure_credentials_have_been_added
+
+ catch_editing_exceptions do
+ change_credentials_in_system_editor
+ 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
+ extract_environment_option_from_argument(default_environment: nil)
+ require_application!
+
+ say credentials.read.presence || missing_credentials_message
+ end
+
+ private
+ def credentials
+ Rails.application.encrypted(content_path, key_path: key_path)
+ end
+
+ def ensure_encryption_key_has_been_added
+ encryption_key_file_generator.add_key_file(key_path)
+ encryption_key_file_generator.ignore_key_file(key_path)
+ end
+
+ def ensure_credentials_have_been_added
+ if options[:environment]
+ encrypted_file_generator.add_encrypted_file_silently(content_path, key_path)
+ else
+ credentials_generator.add_credentials_file_silently
+ end
+ end
+
+ def change_credentials_in_system_editor
+ credentials.change do |tmp_path|
+ system("#{ENV["EDITOR"]} #{tmp_path}")
+ end
+ end
+
+ def missing_credentials_message
+ if credentials.key.nil?
+ "Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`"
+ else
+ "File '#{content_path}' does not exist. Use `rails credentials:edit` to change that."
+ end
+ end
+
+
+ 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 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 credentials_generator
+ require "rails/generators"
+ require "rails/generators/rails/credentials/credentials_generator"
+
+ Rails::Generators::CredentialsGenerator.new
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/db/system/change/change_command.rb b/railties/lib/rails/commands/db/system/change/change_command.rb
new file mode 100644
index 0000000000..760c229c07
--- /dev/null
+++ b/railties/lib/rails/commands/db/system/change/change_command.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require "rails/generators"
+require "rails/generators/rails/db/system/change/change_generator"
+
+module Rails
+ module Command
+ module Db
+ module System
+ class ChangeCommand < Base # :nodoc:
+ class_option :to, desc: "The database system to switch to."
+
+ def perform
+ Rails::Generators::Db::System::ChangeGenerator.start
+ end
+ 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 2c36edfa3f..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' => '--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']
- 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..72f3235ce3
--- /dev/null
+++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+require "active_support/deprecation"
+require "active_support/core_ext/string/filters"
+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 database the
+ # first time around to show a consistent error message to people
+ # relying on 2-level database configuration.
+ if @options["database"] && configurations[database].blank?
+ raise ActiveRecord::AdapterNotSpecified, "'#{database}' database is not configured. Available configuration: #{configurations.inspect}"
+ elsif configurations[environment].blank? && configurations[database].blank?
+ raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}"
+ else
+ configurations[database] || configurations[environment].presence
+ end
+ end
+ end
+
+ def environment
+ Rails.respond_to?(:env) ? Rails.env : Rails::Command.environment
+ end
+
+ def database
+ @options.fetch(:database, "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."
+
+ class_option :database, aliases: "--db", type: :string,
+ desc: "Specifies the database 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]
+
+ if options["connection"]
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `connection` option is deprecated and will be removed in Rails 6.1. Please use `database` option instead.
+ MSG
+ options["database"] = options["connection"]
+ end
+
+ 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/USAGE b/railties/lib/rails/commands/encrypted/USAGE
new file mode 100644
index 0000000000..253eec2378
--- /dev/null
+++ b/railties/lib/rails/commands/encrypted/USAGE
@@ -0,0 +1,28 @@
+=== Storing Encrypted Files in Source Control
+
+The Rails `encrypted` commands provide access to encrypted files or configurations.
+See the `Rails.application.encrypted` documentation for using them in your app.
+
+=== Encryption Keys
+
+By default, Rails looks for the encryption key in `config/master.key` or
+`ENV["RAILS_MASTER_KEY"]`, but that lookup can be overridden with `--key`:
+
+ rails encrypted:edit config/encrypted_file.yml.enc --key config/encrypted_file.key
+
+Don't commit the key! Add it to your source control's ignore file. If you use
+Git, Rails handles this for you.
+
+=== Editing Files
+
+To edit or create an encrypted file use:
+
+ rails encrypted:edit config/encrypted_file.yml.enc
+
+This opens a temporary file in `$EDITOR` with the decrypted contents for editing.
+
+=== Viewing Files
+
+To print the decrypted contents of an encrypted file use:
+
+ rails encrypted:show config/encrypted_file.yml.enc
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..f10a07cdf8
--- /dev/null
+++ b/railties/lib/rails/commands/encrypted/encrypted_command.rb
@@ -0,0 +1,87 @@
+# 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 ""
+ say self.class.desc
+ 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..bd2f3bed67
--- /dev/null
+++ b/railties/lib/rails/commands/initializers/initializers_command.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require "rails/command/environment_argument"
+
+module Rails
+ module Command
+ class InitializersCommand < Base # :nodoc:
+ include EnvironmentArgument
+
+ desc "initializers", "Print out all defined initializers in the order they are invoked by Rails."
+ def perform
+ extract_environment_option_from_argument
+ ENV["RAILS_ENV"] = options[:environment]
+
+ 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/rake_proxy.rb b/railties/lib/rails/commands/rake_proxy.rb
deleted file mode 100644
index f7d5df6b2f..0000000000
--- a/railties/lib/rails/commands/rake_proxy.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'rake'
-require 'active_support'
-
-module Rails
- module RakeProxy #:nodoc:
- private
- def run_rake_task(command)
- ARGV.unshift(command) # Prepend the command, 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
-
- def rake_tasks
- return @rake_tasks if defined?(@rake_tasks)
-
- ActiveSupport::Deprecation.silence do
- require_application_and_environment!
- end
-
- Rake::TaskManager.record_task_metadata = true
- Rake.application.instance_variable_set(:@name, 'rails')
- Rails.application.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
- 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 f9c183ac86..0000000000
--- a/railties/lib/rails/commands/runner.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'optparse'
-
-options = { environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup }
-code_or_file = nil
-command = 'bin/rails runner'
-
-if ARGV.first.nil?
- ARGV.push "-h"
-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(command)}"
- 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 '#{command} -h' for help."
- exit 1
-elsif File.exist?(code_or_file)
- $0 = code_or_file
- Kernel.load code_or_file
-else
- begin
- eval(code_or_file, binding, __FILE__, __LINE__)
- rescue SyntaxError, NameError
- $stderr.puts "Please specify a valid ruby command or the path of a script to run."
- $stderr.puts "Run '#{command} -h' for help."
- exit 1
- end
-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..40fb5e4d89
--- /dev/null
+++ b/railties/lib/rails/commands/runner/runner_command.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require "rails/command/environment_argument"
+
+module Rails
+ module Command
+ class RunnerCommand < Base # :nodoc:
+ include EnvironmentArgument
+
+ self.environment_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)
+ extract_environment_option_from_argument
+
+ 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 7418dff18b..0000000000
--- a/railties/lib/rails/commands/server.rb
+++ /dev/null
@@ -1,139 +0,0 @@
-require 'fileutils'
-require 'optparse'
-require 'action_dispatch'
-require 'rails'
-require 'rails/dev_caching'
-
-module Rails
- class Server < ::Rack::Server
- class Options
- DEFAULT_PID_PATH = File.expand_path("tmp/pids/server.pid").freeze
-
- def parse!(args)
- args, options = args.dup, {}
-
- option_parser(options).parse! args
-
- options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development"
- options[:server] = args.shift
- options
- end
-
- 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.on("-C", "--[no-]dev-caching",
- "Specifies whether to perform caching in development.",
- "true or false") { |v| options[:caching] = v }
-
- opts.separator ""
-
- opts.on("-h", "--help", "Shows this help message.") { puts opts; exit }
- end
- 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
- setup_dev_caching
- 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: ENV.fetch('PORT', 3000).to_i,
- 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
-
- private
-
- def setup_dev_caching
- if options[:environment] == "development"
- Rails::DevCaching.enable_by_argument(options[:caching])
- end
- end
-
- 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"
- 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 restart_command
- "bin/rails server #{ARGV.join(' ')}"
- 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..982b83ead5
--- /dev/null
+++ b/railties/lib/rails/commands/server/server_command.rb
@@ -0,0 +1,312 @@
+# 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"
+require "rails/command/environment_argument"
+
+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 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:
+ include EnvironmentArgument
+
+ # 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 :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
+ extract_environment_option_from_argument
+ 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 variable to specify the IP is deprecated and will be removed in Rails 6.1.
+ Please use `BINDING` environment variable 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 -u [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.squish)
+ Passing the Rack server name as a regular argument is deprecated
+ and will be removed in the next Rails version. Please, use the -u
+ option instead.
+ MSG
+
+ 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)
+ suggestion_msg = "Maybe you meant #{suggestion.inspect}?" if suggestion
+
+ <<~MSG
+ Could not find server "#{server}". #{suggestion_msg}
+ 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 dd069f081f..0000000000
--- a/railties/lib/rails/commands/test.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-require "rails/test_unit/minitest_plugin"
-
-if defined?(ENGINE_ROOT)
- $: << File.expand_path('test', ENGINE_ROOT)
-else
- $: << File.expand_path('../../test', APP_PATH)
-end
-
-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 30eafd59f2..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
@@ -77,13 +79,7 @@ module Rails
end
protected
- def operations
- @operations
- end
-
- def delete_operations
- @delete_operations
- end
+ attr_reader :operations, :delete_operations
end
class Generators #:nodoc:
@@ -91,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
@@ -112,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 9ad77e0a80..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,7 +29,7 @@ module Rails
end
# reloads the environment
- def reload!(print=true)
+ def reload!(print = true)
puts "Reloading..." if print
Rails.application.reloader.reload!
true
diff --git a/railties/lib/rails/console/helpers.rb b/railties/lib/rails/console/helpers.rb
index a33f71dc5b..39fbc55606 100644
--- a/railties/lib/rails/console/helpers.rb
+++ b/railties/lib/rails/console/helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
module ConsoleMethods
# Gets the helper methods available to the controller.
diff --git a/railties/lib/rails/dev_caching.rb b/railties/lib/rails/dev_caching.rb
index 3c20164f0f..ff629b2527 100644
--- a/railties/lib/rails/dev_caching.rb
+++ b/railties/lib/rails/dev_caching.rb
@@ -1,25 +1,28 @@
+# frozen_string_literal: true
+
+require "fileutils"
+
module Rails
module DevCaching # :nodoc:
class << self
- FILE = 'tmp/caching-dev.txt'
+ FILE = "tmp/caching-dev.txt"
def enable_by_file
- FileUtils.mkdir_p('tmp')
+ FileUtils.mkdir_p("tmp")
if File.exist?(FILE)
delete_cache_file
- puts 'Development mode is no longer being cached.'
+ puts "Development mode is no longer being cached."
else
create_cache_file
- puts 'Development mode is now being cached.'
+ puts "Development mode is now being cached."
end
- FileUtils.touch 'tmp/restart.txt'
- FileUtils.rm_f('tmp/pids/server.pid')
+ FileUtils.touch "tmp/restart.txt"
end
def enable_by_argument(caching)
- FileUtils.mkdir_p('tmp')
+ FileUtils.mkdir_p("tmp")
if caching
create_cache_file
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index 5757d235d2..778bbebe75 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -1,8 +1,10 @@
-require 'rails/railtie'
-require 'rails/engine/railties'
-require 'active_support/core_ext/module/delegation'
-require 'pathname'
-require 'thread'
+# 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
@@ -40,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
@@ -109,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:
@@ -128,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
@@ -219,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
@@ -228,7 +230,7 @@ module Rails
#
# If +MyEngine+ is isolated, The routes above will point to
# <tt>MyEngine::ArticlesController</tt>. You also don't need to use longer
- # url helpers like +my_engine_articles_path+. Instead, you should simply use
+ # URL helpers like +my_engine_articles_path+. Instead, you should simply use
# +articles_path+, like you would do with your main application.
#
# To make this behavior consistent with other parts of the framework,
@@ -236,7 +238,7 @@ module Rails
# normal Rails app, when you use a namespaced model such as
# <tt>Namespace::Article</tt>, <tt>ActiveModel::Naming</tt> will generate
# names with the prefix "namespace". In an isolated engine, the prefix will
- # be omitted in url helpers and form fields, for convenience.
+ # be omitted in URL helpers and form fields, for convenience.
#
# polymorphic_url(MyEngine::Article.new)
# # => "articles_path" # not "my_engine_articles_path"
@@ -284,11 +286,11 @@ module Rails
# Note that the <tt>:as</tt> option given to mount takes the <tt>engine_name</tt> as default, so most of the time
# you can simply omit it.
#
- # Finally, if you want to generate a url to an engine's route using
+ # Finally, if you want to generate a URL to an engine's route using
# <tt>polymorphic_url</tt>, you also need to pass the engine helper. Let's
# say that you want to create a form pointing to one of the engine's routes.
# All you need to do is pass the helper as the first element in array with
- # attributes for url:
+ # attributes for URL:
#
# form_for([my_engine, @user])
#
@@ -337,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
@@ -380,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)
@@ -402,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
@@ -436,7 +438,7 @@ module Rails
# 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)
@@ -445,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
@@ -460,20 +462,22 @@ 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)
self
end
- # Eager load the application by loading all ruby
- # files inside eager_load paths.
def eager_load!
+ # Already done by Zeitwerk::Loader.eager_load_all in the finisher.
+ return if Rails.autoloaders.zeitwerk_enabled?
+
config.eager_load_paths.each do |load_path|
- matcher = /\A#{Regexp.escape(load_path.to_s)}\/(.*)\.rb\Z/
+ # Starts after load_path plus a slash, ends before ".rb".
+ relname_range = (load_path.to_s.length + 1)...-3
Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
- require_dependency file.sub(matcher, '\1')
+ require_dependency file[relname_range]
end
end
end
@@ -499,7 +503,7 @@ module Rails
paths["app/helpers"].existent
end
- # Returns the underlying rack application for this engine.
+ # Returns the underlying Rack application for this engine.
def app
@app || @app_build_lock.synchronize {
@app ||= begin
@@ -529,9 +533,9 @@ module Rails
# Defines the routes for this engine. If a block is given to
# routes, it is appended to the engine.
- def routes
+ def routes(&block)
@routes ||= ActionDispatch::Routing::RouteSet.new_with_config(config)
- @routes.append(&Proc.new) if block_given?
+ @routes.append(&block) if block_given?
@routes
end
@@ -546,10 +550,10 @@ module Rails
# Blog::Engine.load_seed
def load_seed
seed_file = paths["db/seeds.rb"].existent.first
- load(seed_file) if seed_file
+ with_inline_jobs { 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)
@@ -566,14 +570,17 @@ module Rails
ActiveSupport::Dependencies.autoload_paths.unshift(*_all_autoload_paths)
ActiveSupport::Dependencies.autoload_once_paths.unshift(*_all_autoload_once_paths)
- # Freeze so future modifications will fail rather than do nothing mysteriously
config.autoload_paths.freeze
- config.eager_load_paths.freeze
config.autoload_once_paths.freeze
end
+ initializer :set_eager_load_paths, before: :bootstrap_hook do
+ ActiveSupport::Dependencies._eager_load_paths.merge(config.eager_load_paths)
+ config.eager_load_paths.freeze
+ 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)
@@ -590,8 +597,8 @@ module Rails
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
@@ -619,7 +626,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
@@ -643,62 +650,73 @@ 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 with_inline_jobs
+ queue_adapter = config.active_job.queue_adapter
+ ActiveSupport.on_load(:active_job) do
+ self.queue_adapter = :inline
+ end
+ yield
+ ensure
+ ActiveSupport.on_load(:active_job) do
+ self.queue_adapter = queue_adapter
+ end
+ end
- while root_path && File.directory?(root_path) && !File.exist?("#{root_path}/#{flag}")
- parent = File.dirname(root_path)
- root_path = parent != root_path && parent
+ def has_migrations?
+ paths["db/migrate"].existent.any?
end
- root = File.exist?("#{root_path}/#{flag}") ? root_path : default
- raise "Could not find root path for #{self}" unless root
+ 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
- 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 #:nodoc:
- ActionDispatch::MiddlewareStack.new
- end
+ Pathname.new File.realpath root
+ end
- def _all_autoload_once_paths #:nodoc:
- config.autoload_once_paths
- end
+ def default_middleware_stack
+ ActionDispatch::MiddlewareStack.new
+ end
- def _all_autoload_paths #:nodoc:
- @_all_autoload_paths ||= (config.autoload_paths + config.eager_load_paths + config.autoload_once_paths).uniq
- end
+ def _all_autoload_once_paths
+ config.autoload_once_paths
+ end
- def _all_load_paths #:nodoc:
- @_all_load_paths ||= (config.paths.load_paths + _all_autoload_paths).uniq
- end
+ def _all_autoload_paths
+ @_all_autoload_paths ||= (config.autoload_paths + config.eager_load_paths + config.autoload_once_paths).uniq
+ end
- private
+ def _all_load_paths
+ @_all_load_paths ||= (config.paths.load_paths + _all_autoload_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 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 build_middleware
- config.middleware
- 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 7bbd9ef744..05218640c6 100644
--- a/railties/lib/rails/engine/commands.rb
+++ b/railties/lib/rails/engine/commands.rb
@@ -1,14 +1,9 @@
-require 'rails/engine/commands_tasks'
+# frozen_string_literal: true
-ARGV << '--help' if ARGV.empty?
+unless defined?(APP_PATH)
+ if File.exist?(File.expand_path("test/dummy/config/application.rb", ENGINE_ROOT))
+ APP_PATH = File.expand_path("test/dummy/config/application", ENGINE_ROOT)
+ end
+end
-aliases = {
- "g" => "generate",
- "d" => "destroy",
- "t" => "test"
-}
-
-command = ARGV.shift
-command = aliases[command] || command
-
-Rails::Engine::CommandsTasks.new(ARGV).run_command!(command)
+require "rails/commands"
diff --git a/railties/lib/rails/engine/commands_tasks.rb b/railties/lib/rails/engine/commands_tasks.rb
deleted file mode 100644
index fa3ee59b7d..0000000000
--- a/railties/lib/rails/engine/commands_tasks.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-require 'rails/commands/rake_proxy'
-
-module Rails
- class Engine
- class CommandsTasks # :nodoc:
- include Rails::RakeProxy
-
- attr_reader :argv
-
- HELP_MESSAGE = <<-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")
- test Run tests (short-cut alias: "t")
-
-All commands can be run with -h for more information.
-
-If you want to run any commands that need to be run in context
-of the application, like `rails server` or `rails console`,
-you should do it from application's directory (typically test/dummy).
-
-In addition to those commands, there are:
- EOT
-
- COMMAND_WHITELIST = %w(generate destroy 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
- run_rake_task(command)
- end
- end
-
- def generate
- generate_or_destroy(:generate)
- end
-
- def destroy
- generate_or_destroy(:destroy)
- end
-
- def test
- require_command!("test")
- end
-
- def version
- argv.unshift '--version'
- require_command!("application")
- end
-
- def help
- write_help_message
- write_commands(formatted_rake_tasks)
- end
-
- private
-
- def require_command!(command)
- require "rails/commands/#{command}"
- end
-
- def generate_or_destroy(command)
- load_generators
- require_command!(command)
- end
-
- def load_generators
- require 'rails/generators'
- require ENGINE_PATH
-
- engine = ::Rails::Engine.find(ENGINE_ROOT)
- Rails::Generators.namespace = engine.railtie_namespace
- engine.load_generators
- end
-
- def write_help_message
- puts HELP_MESSAGE
- end
-
- def write_commands(commands)
- width = commands.map { |name, _| name.size }.max || 10
- commands.each { |command| printf(" %-#{width}s %s\n", *command) }
- end
-
- def parse_command(command)
- case command
- when '--version', '-v'
- 'version'
- when '--help', '-h'
- 'help'
- else
- command
- end
- end
-
- def rake_tasks
- return @rake_tasks if defined?(@rake_tasks)
-
- load_generators
- Rake::TaskManager.record_task_metadata = true
- Rake.application.init('rails')
- Rake.application.load_rakefile
- @rake_tasks = Rake.application.tasks.select(&:comment)
- end
- end
- end
-end
diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb
index 294d07446f..4143b3c881 100644
--- a/railties/lib/rails/engine/configuration.rb
+++ b/railties/lib/rails/engine/configuration.rb
@@ -1,4 +1,6 @@
-require 'rails/railtie/configuration'
+# frozen_string_literal: true
+
+require "rails/railtie/configuration"
module Rails
class Engine
@@ -7,7 +9,7 @@ module Rails
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
@@ -36,7 +38,9 @@ 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"
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 081222425c..fea24810f5 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 = "beta3"
diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb
index 330bd7ec5d..b835b3f3fd 100644
--- a/railties/lib/rails/generators.rb
+++ b/railties/lib/rails/generators.rb
@@ -1,45 +1,51 @@
-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 :Database, "rails/generators/database"
+ autoload :AppName, "rails/generators/app_name"
+ 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 +56,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,340 +63,261 @@ 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 helpers and assets
- # so generators such as scaffold won't create them.
- def self.api_only!
- hide_namespaces "assets", "helper", "css", "js"
-
- options[:rails].merge!(
- api: true,
- assets: false,
- helper: false,
- template_engine: nil
- )
-
- if ARGV.first == 'mailer'
- options[:rails].merge!(template_engine: :erb)
+ 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
- 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
-
- # 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 ", locale: :en) }\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",
- "#{test}:job",
- "#{template}:controller",
- "#{template}:scaffold",
- "#{template}:mailer",
- "#{css}:scaffold",
- "#{css}:assets",
- "css:assets",
- "css:scaffold"
- ]
+ # Returns an array of generator namespaces that are hidden.
+ # Generator namespaces may be hidden for a variety of reasons.
+ # Some are aliased such as "rails:migration" and can be
+ # invoked with the shorter "migration", others are private to other generators
+ # such as "css:scaffold".
+ def hidden_namespaces
+ @hidden_namespaces ||= begin
+ orm = options[:rails][:orm]
+ test = options[:rails][:test_framework]
+ template = options[:rails][:template_engine]
+ css = options[:rails][:stylesheet_engine]
+
+ [
+ "rails",
+ "resource_route",
+ "#{orm}:migration",
+ "#{orm}:model",
+ "#{test}:controller",
+ "#{test}:helper",
+ "#{test}:integration",
+ "#{test}:system",
+ "#{test}:mailer",
+ "#{test}:model",
+ "#{test}:scaffold",
+ "#{test}:view",
+ "#{test}:job",
+ "#{template}:controller",
+ "#{template}:scaffold",
+ "#{template}:mailer",
+ "#{css}:scaffold",
+ "#{css}:assets",
+ "css:assets",
+ "css:scaffold"
+ ]
+ end
end
- end
- class << self
def hide_namespaces(*namespaces)
hidden_namespaces.concat(namespaces)
end
alias hide_namespace hide_namespaces
- end
- # Show help message with available generators.
- def self.help(command = 'generate')
- puts "Usage: rails #{command} GENERATOR [args] [options]"
- puts
- puts "General options:"
- puts " -h, [--help] # Print generator's options and usage"
- puts " -p, [--pretend] # Run but do not make any changes"
- puts " -f, [--force] # Overwrite files that already exist"
- puts " -s, [--skip] # Skip files that already exist"
- puts " -q, [--quiet] # Suppress status output"
- puts
- puts "Please choose a generator below."
- puts
-
- print_generators
- end
+ # 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")
+ rails.delete("db:system:change")
- 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)
+ suggestion_msg = "Maybe you meant #{suggestion.inspect}?" if suggestion
+
+ puts <<~MSG
+ Could not find generator '#{namespace}'. #{suggestion_msg}
+ 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 = []
- # 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
+ Array(fallbacks[base.to_sym]).each do |fallback|
+ next if invoked_fallbacks.include?(fallback)
+ invoked_fallbacks << fallback
+
+ 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 57309112b5..1a5f2ff203 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -1,41 +1,45 @@
-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
- @after_bundle_callbacks = []
+ @indentation = 0
end
# Adds an entry into +Gemfile+ for the supplied gem.
#
# 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.dup
- if version ||= options.delete(:version)
- parts << quote(version)
- message << " (#{version})"
+
+ 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 +51,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 +86,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", "\nsource #{quote(source)} do", force: true
- @in_group = true
- instance_eval(&block)
- @in_group = false
+ with_indentation(&block)
append_file "Gemfile", "\nend\n", force: true
else
prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false
@@ -98,17 +112,17 @@ module Rails
# environment(nil, env: "development") do
# "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 +134,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 +153,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 +167,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 +188,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 +207,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.
@@ -202,9 +220,12 @@ module Rails
# generate(:authenticated, "user session")
def generate(what, *args)
log :generate, what
+
+ options = args.extract_options!
+ options[:without_rails_env] = true
argument = args.flat_map(&:to_s).join(" ")
- in_root { run_ruby_script("bin/rails generate #{what} #{argument}", verbose: false) }
+ execute_command :rails, "generate #{what} #{argument}", options
end
# Runs the supplied rake task (invoked with 'rake ...')
@@ -212,27 +233,21 @@ module Rails
# rake("db:migrate")
# rake("db:migrate", env: "production")
# rake("gems:install", sudo: true)
- def rake(command, options={})
+ # rake("gems:install", capture: true)
+ def rake(command, options = {})
execute_command :rake, command, options
end
# Runs the supplied rake task (invoked with 'rails ...')
#
- # rails("db:migrate")
- # rails("db:migrate", env: "production")
- # rails("gems:install", sudo: true)
- def rails_command(command, options={})
+ # 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!
- log :capify, ""
- in_root { run("#{extify(:capify)} .", verbose: false) }
- end
-
# Make an entry in Rails routing file <tt>config/routes.rb</tt>
#
# route "root 'welcome#index'"
@@ -241,7 +256,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: false }
+ inject_into_file "config/routes.rb", optimize_indentation(routing_code, 2), after: sentinel, verbose: false, force: false
end
end
@@ -252,43 +267,38 @@ module Rails
log File.read(find_in_source_paths(path))
end
- # Registers a callback to be executed after bundle and spring binstubs
- # have run.
- #
- # after_bundle do
- # git add: '.'
- # end
- def after_bundle(&block)
- @after_bundle_callbacks << block
- end
-
- protected
+ private
# Define log for backwards compatibility. If just one argument is sent,
# invoke say, otherwise invoke say_status. Differently from say and
# similarly to say_status, this method respects the quiet? option given.
- def log(*args)
+ def log(*args) # :doc:
if args.size == 1
say args.first.to_s unless options.quiet?
else
- args << (self.behavior == :invoke ? :green : :red)
+ args << (behavior == :invoke ? :green : :red)
say_status(*args)
end
end
-
# Runs the supplied command using either "rake ..." or "rails ..."
# based on the executor parameter provided.
- def execute_command(executor, command, options={})
+ def execute_command(executor, command, options = {}) # :doc:
log executor, command
- env = options[:env] || ENV["RAILS_ENV"] || 'development'
- sudo = options[:sudo] && RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ ? 'sudo ' : ''
- in_root { run("#{sudo}#{extify(executor)} #{command} RAILS_ENV=#{env}", verbose: false) }
+ env = options[:env] || ENV["RAILS_ENV"] || "development"
+ rails_env = " RAILS_ENV=#{env}" unless options[:without_rails_env]
+ sudo = options[:sudo] && !Gem.win_platform? ? "sudo " : ""
+ config = { verbose: false }
+
+ config[:capture] = options[:capture] if options[:capture]
+ config[:abort_on_failure] = options[:abort_on_failure] if options[:abort_on_failure]
+
+ in_root { run("#{sudo}#{extify(executor)} #{command}#{rails_env}", 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
@@ -297,7 +307,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?("'")
@@ -306,6 +321,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 d664b07652..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 #: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 89341e6fa2..66f6b57379 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -1,17 +1,18 @@
-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 )
- JDBC_DATABASES = %w( jdbcmysql jdbcsqlite3 jdbcpostgresql jdbc )
- DATABASES.concat(JDBC_DATABASES)
+ include Database
+ include AppName
attr_accessor :rails_template
add_shebang_option!
@@ -23,84 +24,92 @@ 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 :database, type: :string, aliases: '-d', default: 'sqlite3',
- desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})"
+ class_option :skip_keeps, type: :boolean, default: false,
+ desc: "Skip source control .keep files"
- class_option :javascript, type: :string, aliases: '-j', default: 'jquery',
- desc: 'Preconfigure for selected JavaScript library'
+ class_option :skip_action_mailer, type: :boolean, aliases: "-M",
+ default: false,
+ desc: "Skip Action Mailer files"
- class_option :skip_gemfile, type: :boolean, default: false,
- desc: "Don't create a Gemfile"
+ class_option :skip_action_mailbox, type: :boolean, default: false,
+ desc: "Skip Action Mailbox gem"
- class_option :skip_bundle, type: :boolean, aliases: '-B', default: false,
- desc: "Don't run bundle install"
+ class_option :skip_action_text, type: :boolean, default: false,
+ desc: "Skip Action Text gem"
- 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_puma, type: :boolean, aliases: '-P', default: false,
- desc: 'Skip Puma related files'
+ class_option :skip_sprockets, type: :boolean, aliases: "-S", default: false,
+ desc: "Skip Sprockets files"
- class_option :skip_action_cable, type: :boolean, aliases: '-C', default: false,
- desc: 'Skip Action Cable files'
+ class_option :skip_spring, type: :boolean, default: false,
+ desc: "Don't install Spring application preloader"
- class_option :skip_sprockets, type: :boolean, aliases: '-S', default: false,
- desc: 'Skip Sprockets files'
+ class_option :skip_listen, type: :boolean, default: false,
+ desc: "Don't generate configuration that depends on the listen gem"
- class_option :skip_spring, type: :boolean, default: false,
- desc: "Don't install Spring application preloader"
+ class_option :skip_javascript, type: :boolean, aliases: "-J", default: name == "plugin",
+ desc: "Skip JavaScript files"
- class_option :skip_listen, type: :boolean, default: false,
- desc: "Don't generate configuration that depends on the listen gem"
+ class_option :skip_turbolinks, type: :boolean, default: false,
+ desc: "Skip turbolinks gem"
- class_option :skip_javascript, type: :boolean, aliases: '-J', default: false,
- desc: 'Skip JavaScript files'
+ class_option :skip_test, type: :boolean, aliases: "-T", default: false,
+ desc: "Skip test files"
- class_option :skip_turbolinks, type: :boolean, default: false,
- desc: 'Skip turbolinks gem'
+ class_option :skip_system_test, type: :boolean, default: false,
+ desc: "Skip system test files"
- class_option :skip_test, type: :boolean, aliases: '-T', default: false,
- desc: 'Skip test files'
+ class_option :skip_bootsnap, type: :boolean, default: false,
+ desc: "Skip bootsnap gem"
- class_option :dev, type: :boolean, default: false,
- desc: "Setup the #{name} with Gemfile pointing to your Rails checkout"
+ class_option :dev, type: :boolean, default: false,
+ desc: "Setup the #{name} with Gemfile pointing to your Rails checkout"
- class_option :edge, type: :boolean, default: false,
- desc: "Setup the #{name} with Gemfile pointing to Rails repository"
+ class_option :edge, type: :boolean, default: false,
+ desc: "Setup the #{name} with Gemfile pointing to Rails repository"
- class_option :rc, type: :string, default: nil,
- desc: "Path to file containing extra configuration options for rails command"
+ class_option :rc, type: :string, default: nil,
+ desc: "Path to file containing extra configuration options for rails command"
- class_option :no_rc, type: :boolean, default: false,
- desc: 'Skip loading of extra configuration options from .railsrc file'
+ class_option :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'
+ class_option :help, type: :boolean, aliases: "-h", group: :rails,
+ desc: "Show this help message and quit"
end
def initialize(*args)
@gem_filter = lambda { |gem| true }
@extra_entries = []
super
- 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]
@@ -116,11 +125,12 @@ module Rails
self
end
- def gemfile_entries
+ def gemfile_entries # :doc:
[rails_gemfile_entry,
database_gemfile_entry,
- webserver_gemfile_entry,
+ web_server_gemfile_entry,
assets_gemfile_entry,
+ webpacker_gemfile_entry,
javascript_gemfile_entry,
jbuilder_gemfile_entry,
psych_gemfile_entry,
@@ -128,13 +138,13 @@ module Rails
@extra_entries].flatten.find_all(&@gem_filter)
end
- def add_gem_entry_filter
+ def add_gem_entry_filter # :doc:
@gem_filter = lambda { |next_filter, entry|
yield(entry) && next_filter.call(entry)
}.curry[@gem_filter]
end
- def builder
+ def builder # :doc:
@builder ||= begin
builder_class = get_builder_class
builder_class.include(ActionMethods)
@@ -142,62 +152,95 @@ 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]
gem_name, gem_version = gem_for_database
GemfileEntry.version gem_name, gem_version,
"Use #{options[:database]} as the database for Active Record"
end
- def webserver_gemfile_entry
+ def web_server_gemfile_entry # :doc:
return [] if options[:skip_puma]
- comment = 'Use Puma as the app server'
- GemfileEntry.new('puma', '~> 3.0', comment)
+ 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?,
+ skip_action_mailbox?,
+ skip_action_text?
+ ].flatten.none?
+ end
+
+ def comment_if(value) # :doc:
+ question = "#{value}?"
+
+ comment =
+ if respond_to?(question, true)
+ send(question)
+ else
+ options[value]
+ end
+
+ comment ? "# " : ""
end
- def include_all_railties?
- options.values_at(:skip_active_record, :skip_action_mailer, :skip_test, :skip_sprockets, :skip_action_cable).none?
+ def keeps? # :doc:
+ !options[:skip_keeps]
end
- def comment_if(value)
- options[value] ? '# ' : ''
+ def sqlite3? # :doc:
+ !options[:skip_active_record] && options[:database] == "sqlite3"
end
- def keeps?
- !options[:skip_keeps]
+ def skip_active_storage? # :doc:
+ options[:skip_active_storage] || options[:skip_active_record]
end
- def sqlite3?
- !options[:skip_active_record] && options[:database] == 'sqlite3'
+ def skip_action_mailbox? # :doc:
+ options[:skip_action_mailbox] || skip_active_storage?
+ end
+
+ def skip_action_text? # :doc:
+ options[:skip_action_text] || skip_active_storage?
end
class GemfileEntry < Struct.new(:name, :version, :comment, :options, :commented_out)
@@ -233,130 +276,81 @@ module Rails
end
def rails_gemfile_entry
- dev_edge_common = [
- ]
if options.dev?
[
- GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH)
- ] + dev_edge_common
+ GemfileEntry.path("rails", Rails::Generators::RAILS_DEV_PATH)
+ ]
elsif options.edge?
[
- GemfileEntry.github('rails', 'rails/rails')
- ] + dev_edge_common
+ GemfileEntry.github("rails", "rails/rails")
+ ]
else
- [GemfileEntry.version('rails',
+ [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.prerelease?
- next_series = gem_version
- next_series = next_series.bump while next_series.segments.size > 2
-
- [">= #{gem_version}", "< #{next_series}"]
- elsif gem_version.segments.size == 3
+ if gem_version.segments.size == 3 || gem_version.release.segments.size == 3
+ # ~> 1.2.3
+ # ~> 1.2.3.pre4
"~> #{gem_version}"
else
+ # ~> 1.2.3, >= 1.2.3.4
+ # ~> 1.2.3, >= 1.2.3.4.pre5
patch = gem_version.segments[0, 3].join(".")
["~> #{patch}", ">= #{gem_version}"]
end
end
- def gem_for_database
- # %w( mysql oracle postgresql sqlite3 frontbase ibm_db sqlserver jdbcmysql jdbcsqlite3 jdbcpostgresql )
- case options[:database]
- when "oracle" then ["ruby-oci8", nil]
- when "postgresql" then ["pg", ["~> 0.18"]]
- when "frontbase" then ["ruby-frontbase", nil]
- when "mysql" then ["mysql2", [">= 0.3.18", "< 0.5"]]
- 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"
- end
- end
- end
-
def assets_gemfile_entry
return [] if options[:skip_sprockets]
- gems = []
- gems << GemfileEntry.version('sass-rails', '~> 5.0',
- 'Use SCSS for stylesheets')
+ GemfileEntry.version("sass-rails", "~> 5.0", "Use SCSS for stylesheets")
+ end
- gems << GemfileEntry.version('uglifier',
- '>= 1.3.0',
- 'Use Uglifier as compressor for JavaScript assets')
+ def webpacker_gemfile_entry
+ return [] if options[:skip_javascript]
- gems
+ if options.dev? || options.edge?
+ GemfileEntry.github "webpacker", "rails/webpacker", nil, "Use development version of Webpacker"
+ else
+ GemfileEntry.version "webpacker", "~> 4.0", "Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker"
+ end
end
def jbuilder_gemfile_entry
- comment = 'Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder'
- GemfileEntry.new 'jbuilder', '~> 2.0', comment, {}, options[:api]
- end
-
- def coffee_gemfile_entry
- GemfileEntry.version 'coffee-rails', '~> 4.1.0', 'Use CoffeeScript for .coffee assets and views'
+ comment = "Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder"
+ GemfileEntry.new "jbuilder", "~> 2.5", comment, {}, options[:api]
end
def javascript_gemfile_entry
- if options[:skip_javascript] || options[:skip_sprockets]
+ if options[:skip_javascript] || options[:skip_turbolinks]
[]
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", "~> 5.x",
- "Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks")
- end
-
- gems
- end
- 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
- 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'
+ comment = "Use Redis adapter to run Action Cable in production"
gems = []
- gems << GemfileEntry.new("redis", '~> 3.0', comment, {}, true)
+ gems << GemfileEntry.new("redis", "~> 4.0", comment, {}, true)
gems
end
- def bundle_command(command)
+ def bundle_command(command, env = {})
say_status :run, "bundle #{command}"
# We are going to shell out rather than invoking Bundler::CLI.new(command)
@@ -364,19 +358,21 @@ module Rails
# its own vendored Thor, which could be a different version. Running both
# things in the same process is a recipe for a night with paracetamol.
#
- # 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'
- Bundler.with_clean_env do
- full_command = %Q["#{Gem.ruby}" "#{_bundle_command}" #{command}]
- if options[:quiet]
- system(full_command, out: File::NULL)
- else
- system(full_command)
- end
+ require "bundler"
+ Bundler.with_original_env do
+ exec_bundle_command(_bundle_command, command, env)
+ end
+ end
+
+ def exec_bundle_command(bundle_command, command, env)
+ full_command = %Q["#{Gem.ruby}" "#{bundle_command}" #{command}]
+ if options[:quiet]
+ system(env, full_command, out: File::NULL)
+ else
+ system(env, full_command)
end
end
@@ -388,16 +384,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/
+ RbConfig::CONFIG["host_os"] =~ /darwin|linux/
end
def run_bundle
- bundle_command('install') if bundle_install?
+ bundle_command("install", "BUNDLE_IGNORE_MESSAGES" => "1") 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/app_name.rb b/railties/lib/rails/generators/app_name.rb
new file mode 100644
index 0000000000..c4f71694d8
--- /dev/null
+++ b/railties/lib/rails/generators/app_name.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Rails
+ module Generators
+ module AppName # :nodoc:
+ RESERVED_NAMES = %w(application destroy plugin runner test)
+
+ private
+ def app_name
+ @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
+ defined_app_const_base.underscore
+ end
+
+ def defined_app_const_base
+ Rails.respond_to?(:application) && defined?(Rails::Application) &&
+ Rails.application.is_a?(Rails::Application) && Rails.application.class.name.chomp("::Application")
+ end
+
+ 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
+ end
+ alias :camelized :app_const_base
+
+ def app_const
+ @app_const ||= "#{app_const_base}::Application"
+ end
+
+ def valid_const?
+ 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 #{original_app_name}, constant #{app_const_base} is already in use. Please choose another application name."
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb
index c72ec400a0..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
@@ -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
# Returns default aliases for the option name given doing a lookup in
# Rails::Generators.aliases.
- def self.default_aliases_for_option(name, options)
+ def self.default_aliases_for_option(name, options) # :doc:
default_for_option(Rails::Generators.aliases, name, options, options[:aliases])
end
# Returns default for the option name given doing a lookup in config.
- def self.default_for_option(config, name, options, default)
- if generator_name and c = config[generator_name.to_sym] and c.key?(name)
+ 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/database.rb b/railties/lib/rails/generators/database.rb
new file mode 100644
index 0000000000..18fc7be3ff
--- /dev/null
+++ b/railties/lib/rails/generators/database.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Rails
+ module Generators
+ module Database # :nodoc:
+ JDBC_DATABASES = %w( jdbcmysql jdbcsqlite3 jdbcpostgresql jdbc )
+ DATABASES = %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver ) + JDBC_DATABASES
+
+ def initialize(*)
+ super
+ convert_database_option_for_jruby
+ end
+
+ def gem_for_database(database = options[:database])
+ case database
+ when "mysql" then ["mysql2", [">= 0.4.4"]]
+ when "postgresql" then ["pg", [">= 0.18", "< 2.0"]]
+ when "sqlite3" then ["sqlite3", ["~> 1.3", ">= 1.3.6"]]
+ when "oracle" then ["activerecord-oracle_enhanced-adapter", nil]
+ when "frontbase" then ["ruby-frontbase", nil]
+ when "sqlserver" then ["activerecord-sqlserver-adapter", nil]
+ 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 [database, nil]
+ end
+ end
+
+ def convert_database_option_for_jruby
+ if defined?(JRUBY_VERSION)
+ 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
+
+ private
+ def mysql_socket
+ @mysql_socket ||= [
+ "/tmp/mysql.sock", # default
+ "/var/run/mysqld/mysqld.sock", # debian/gentoo
+ "/var/tmp/mysql.sock", # freebsd
+ "/var/lib/mysql/mysql.sock", # fedora
+ "/opt/local/lib/mysql/mysql.sock", # fedora
+ "/opt/local/var/run/mysqld/mysqld.sock", # mac + darwinports + mysql
+ "/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 Gem.win_platform?
+ end
+ 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 7f00943d80..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,9 +8,16 @@ 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 behavior == :invoke
+ formats.each do |format|
+ layout_path = File.join("app/views/layouts", class_path, filename_with_extensions("mailer", format))
+ template filename_with_extensions(:layout, format), layout_path unless File.exist?(layout_path)
+ end
+ end
+
actions.each do |action|
@action = action
@@ -19,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.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.tt b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb.tt
new file mode 100644
index 0000000000..6363733e6e
--- /dev/null
+++ b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb.tt
@@ -0,0 +1 @@
+<%%= yield %>
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 519b6c8603..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 %>
- <%%= f.password_field :password %>
- </div>
-
- <div class="field">
- <%%= f.label :password_confirmation %>
- <%%= f.password_field :password_confirmation %>
-<% else -%>
- <%%= f.label :<%= attribute.column_name %> %>
- <%%= 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..1dddc3d698
--- /dev/null
+++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb.tt
@@ -0,0 +1,37 @@
+<%%= 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 %>
+<% elsif attribute.attachments? -%>
+ <%%= form.label :<%= attribute.column_name %> %>
+ <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %>, multiple: true %>
+<% 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 5f4904fee1..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 <%= singular_table_name.titleize %>', 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..6b216001d2
--- /dev/null
+++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb.tt
@@ -0,0 +1,19 @@
+<p id="notice"><%%= notice %></p>
+
+<% attributes.reject(&:password_digest?).each do |attribute| -%>
+<p>
+ <strong><%= attribute.human_name %>:</strong>
+<% if attribute.attachment? -%>
+ <%%= link_to @<%= singular_table_name %>.<%= attribute.column_name %>.filename, @<%= singular_table_name %>.<%= attribute.column_name %> %>
+<% elsif attribute.attachments? -%>
+ <%% @<%= singular_table_name %>.<%= attribute.column_name %>.each do |<%= attribute.singular_name %>| %>
+ <div><%%= link_to <%= attribute.singular_name %>.filename, <%= attribute.singular_name %> %></div>
+ <%% end %>
+<% else -%>
+ <%%= @<%= singular_table_name %>.<%= attribute.column_name %> %>
+<% end -%>
+</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 7e437e7344..99c1bc4269 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
@@ -37,26 +39,26 @@ module Rails
private
- # parse possible attribute options like :limit for string/text/binary/integer, :precision/:scale for decimals or :polymorphic for references/belongs_to
- # when declaring options curly brackets should be used
- def parse_type_and_options(type)
- case type
- when /(string|text|binary|integer)\{(\d+)\}/
- return $1, limit: $2.to_i
- when /decimal\{(\d+)[,.-](\d+)\}/
- return :decimal, precision: $1.to_i, scale: $2.to_i
- when /(references|belongs_to)\{(.+)\}/
- type = $1
- provided_options = $2.split(/[,.-]/)
- options = Hash[provided_options.map { |opt| [opt.to_sym, true] }]
- return type, options
- else
- return type, {}
+ # parse possible attribute options like :limit for string/text/binary/integer, :precision/:scale for decimals or :polymorphic for references/belongs_to
+ # when declaring options curly brackets should be used
+ def parse_type_and_options(type)
+ case type
+ when /(string|text|binary|integer)\{(\d+)\}/
+ return $1, limit: $2.to_i
+ when /decimal\{(\d+)[,.-](\d+)\}/
+ return :decimal, precision: $1.to_i, scale: $2.to_i
+ when /(references|belongs_to)\{(.+)\}/
+ type = $1
+ provided_options = $2.split(/[,.-]/)
+ options = Hash[provided_options.map { |opt| [opt.to_sym, true] }]
+ return type, options
+ else
+ return type, {}
+ end
end
- 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)
@@ -66,40 +68,44 @@ 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 :rich_text then :rich_text_area
+ when :boolean then :check_box
+ when :attachment, :attachments then :file_field
+ 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,
+ :attachment, :attachments,
+ :rich_text 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
@@ -127,11 +133,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?
@@ -143,15 +149,31 @@ module Rails
end
def password_digest?
- name == 'password' && type == :digest
+ name == "password" && type == :digest
end
def token?
type == :token
end
+ def rich_text?
+ type == :rich_text
+ end
+
+ def attachment?
+ type == :attachment
+ end
+
+ def attachments?
+ type == :attachments
+ end
+
+ def virtual?
+ rich_text? || attachment? || attachments?
+ 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 87f2e1d42b..b6ec0160cf 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
@@ -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,20 @@ 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)
+ if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
+ ERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer").result(context)
+ else
+ ERB.new(::File.binread(source), nil, "-", "@output_buffer").result(context)
+ 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 efbf51ddfb..42e64cd11f 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,180 +25,180 @@ 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 show_helper
- "#{singular_table_name}_url(@#{singular_table_name})"
+ def show_helper # :doc:
+ "#{singular_route_name}_url(@#{singular_table_name})"
end
- def edit_helper
+ def edit_helper # :doc:
"edit_#{show_helper}"
end
- def new_helper
- "new_#{singular_table_name}_url"
+ def new_helper # :doc:
+ "new_#{singular_route_name}_url"
end
- def singular_table_name
+ 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
- @url_helper_prefix ||= (class_path + [file_name]).join('_')
+ 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|
+ next if a.attachments?
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
@@ -213,9 +212,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/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb
index f58e6ba653..f2f46d6e25 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,14 +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'
- empty_directory_with_keep_file 'app/assets/javascripts/channels' unless options[:skip_action_cable]
+ 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
@@ -70,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"
@@ -77,10 +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"
@@ -89,40 +119,62 @@ 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')
- action_cable_config_exist = File.exist?('config/cable.yml')
- ssl_options_exist = File.exist?('config/initializers/ssl_options.rb')
- rack_cors_config_exist = File.exist?('config/initializers/cors.rb')
+ 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
-
- gsub_file 'config/environments/development.rb', /^(\s+)config\.file_watcher/, '\1# config.file_watcher'
+ @config_target_version = Rails.application.config.loaded_config_version || "5.0"
- unless callback_terminator_config_exist
- remove_file 'config/initializers/callback_terminator.rb'
- end
+ config
unless cookie_serializer_config_exist
- gsub_file 'config/initializers/cookies_serializer.rb', /json(?!,)/, 'marshal'
+ gsub_file "config/initializers/cookies_serializer.rb", /json(?!,)/, "marshal"
end
- unless active_record_belongs_to_required_by_default_config_exist
- remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb'
+ if !options[:skip_action_cable] && !action_cable_config_exist
+ template "config/cable.yml"
end
- unless action_cable_config_exist
- template 'config/cable.yml'
+ if !skip_active_storage? && !active_storage_config_exist
+ template "config/storage.yml"
end
- unless ssl_options_exist
- remove_file 'config/initializers/ssl_options.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'
+ 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
@@ -134,29 +186,41 @@ 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/channels/application_cable/connection_test.rb"
+ 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
@@ -166,51 +230,54 @@ 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__))
- RESERVED_NAMES = %w[application destroy plugin runner test]
+ RAILS_DEV_PATH = File.expand_path("../../../../../..", __dir__)
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(", ")}."
+ raise Error, "Invalid value for --database option. Supported preconfigurations 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
+
+ @after_bundle_callbacks = []
end
public_task :set_default_accessors!
@@ -219,9 +286,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
@@ -232,6 +302,18 @@ module Rails
build(:bin)
end
+ def update_bin_files
+ build(:bin_when_updating)
+ end
+ remove_task :update_bin_files
+
+ def update_active_storage
+ unless skip_active_storage?
+ rails_command "active_storage:update"
+ end
+ end
+ remove_task :update_active_storage
+
def create_config_files
build(:config)
end
@@ -241,6 +323,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
@@ -251,6 +346,7 @@ module Rails
end
def create_db_files
+ return if options[:skip_active_record]
build(:db)
end
@@ -266,10 +362,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
@@ -278,157 +370,143 @@ 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_application_layout_file_if_api_option
+ def delete_app_views_if_api_option
if options[:api]
- remove_file 'app/views/layouts/application.html.erb'
+ if options[:skip_action_mailer]
+ remove_dir "app/views"
+ else
+ remove_file "app/views/layouts/application.html.erb"
+ end
+ end
+ end
+
+ 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_application_record_skipping_active_record
if options[:skip_active_record]
- remove_file 'app/models/application_record.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/mailers/application_mailer.rb'
- remove_file 'app/views/layouts/mailer.html.erb'
- remove_file 'app/views/layouts/mailer.text.erb'
- end
- end
-
- def delete_active_record_initializers_skipping_active_record
- if options[:skip_active_record]
- remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb'
+ 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_file 'config/cable.yml'
- remove_file 'app/assets/javascripts/cable.js'
- remove_dir 'app/channels'
+ remove_dir "app/javascript/channels"
+ remove_dir "app/channels"
+ remove_dir "test/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/request_forgery_protection.rb'
- remove_file 'config/initializers/per_form_csrf_tokens.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'
+ 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(". ", "_")
- end
-
- def defined_app_name
- defined_app_const_base.underscore
- end
-
- def defined_app_const_base
- Rails.respond_to?(:application) && defined?(Rails::Application) &&
- Rails.application.is_a?(Rails::Application) && Rails.application.class.name.sub(/::Application$/, "")
- end
-
- 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
- end
- alias :camelized :app_const_base
-
- def app_const
- @app_const ||= "#{app_const_base}::Application"
- 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 " \
- "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."
- end
- end
-
- def app_secret
- SecureRandom.hex(64)
- end
-
- def mysql_socket
- @mysql_socket ||= [
- "/tmp/mysql.sock", # default
- "/var/run/mysqld/mysqld.sock", # debian/gentoo
- "/var/tmp/mysql.sock", # freebsd
- "/var/lib/mysql/mysql.sock", # fedora
- "/opt/local/lib/mysql/mysql.sock", # fedora
- "/opt/local/var/run/mysqld/mysqld.sock", # mac + darwinports + mysql
- "/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/
+ # Registers a callback to be executed after bundle and spring binstubs
+ # have run.
+ #
+ # after_bundle do
+ # git add: '.'
+ # end
+ def after_bundle(&block) # :doc:
+ @after_bundle_callbacks << block
end
def get_builder_class
@@ -456,14 +534,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
@@ -473,20 +551,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 86143ca1f1..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 Capistrano for deployment
-# gem 'capistrano-rails', group: :development
-
-<%- 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', platform: :mri
-end
-
-group :development do
-<%- unless options.api? -%>
- # Access an IRB console on exception pages or by using <%%= console %> anywhere in the code.
- <%- if options.dev? || options.edge? -%>
- gem 'web-console', github: 'rails/web-console'
- <%- else -%>
- gem 'web-console'
- <%- end -%>
-<%- end -%>
-<% if depend_on_listen? -%>
- gem 'listen', '~> 3.0.5'
-<% end -%>
-<% if spring_install? -%>
- # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
- gem 'spring'
-<% if depend_on_listen? -%>
- gem 'spring-watcher-listen', '~> 2.0.0'
-<% end -%>
-<% end -%>
-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/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
new file mode 100644
index 0000000000..d7221453e7
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
@@ -0,0 +1,78 @@
+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 Active Model has_secure_password
+# gem 'bcrypt', '~> 3.1.7'
+<% unless skip_active_storage? -%>
+
+# Use Active Storage variant
+# gem 'image_processing', '~> 1.2'
+<% end -%>
+
+<% if depend_on_bootsnap? -%>
+# Reduces boot times through caching; required in config/boot.rb
+gem 'bootsnap', '>= 1.4.2', 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 web drivers to run system tests with browsers
+ gem 'webdrivers'
+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.tt
index 7db80e4ca1..7db80e4ca1 100644
--- a/railties/lib/rails/generators/rails/app/templates/README.md
+++ b/railties/lib/rails/generators/rails/app/templates/README.md.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile.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
index f4ee1409af..591819335f 100644
--- 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
@@ -1,7 +1,2 @@
-<% unless options.api? -%>
//= link_tree ../images
-<% end -%>
-<% unless options.skip_javascript -%>
-//= link_directory ../javascripts .js
-<% end -%>
//= 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 c88426ec06..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. 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.
-//
-<% 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/javascripts/cable.js b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js
deleted file mode 100644
index 71ee1e66de..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js
+++ /dev/null
@@ -1,13 +0,0 @@
-// Action Cable provides the framework to deal with WebSockets in Rails.
-// You can generate new channels where WebSocket features live using the rails generate channel command.
-//
-//= require action_cable
-//= require_self
-//= require_tree ./channels
-
-(function() {
- this.App || (this.App = {});
-
- App.cable = ActionCable.createConsumer();
-
-}).call(this);
diff --git a/railties/lib/rails/generators/rails/app/templates/app/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 b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb
deleted file mode 100644
index d56fa30f4d..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/channel.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
-module ApplicationCable
- class Channel < ActionCable::Channel::Base
- end
-end
diff --git a/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 b/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb
deleted file mode 100644
index b4f41389ad..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/app/channels/application_cable/connection.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
-module ApplicationCable
- class Connection < ActionCable::Connection::Base
- end
-end
diff --git a/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.tt b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb.tt
new file mode 100644
index 0000000000..de6be7945c
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/helpers/application_helper.rb.tt
@@ -0,0 +1,2 @@
+module ApplicationHelper
+end
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..0eceb59b18
--- /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 { createConsumer } from "@rails/actioncable"
+
+export default 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..e67e742263
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt
@@ -0,0 +1,23 @@
+// 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.
+
+require("@rails/ujs").start()
+<%- unless options[:skip_turbolinks] -%>
+require("turbolinks").start()
+<%- end -%>
+<%- unless skip_active_storage? -%>
+require("@rails/activestorage").start()
+<%- end -%>
+<%- unless options[:skip_action_cable] -%>
+require("channels")
+<%- end -%>
+
+
+// Uncomment to copy all static images under ../images to the output folder and reference
+// them with the image_pack_tag helper in views (e.g <%%= image_pack_tag 'rails.png' %>)
+// or the `imagePath` JavaScript helper below.
+//
+// const images = require.context('../images', true)
+// const imagePath = (name) => images(name, true)
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 72258cc96b..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
@@ -3,16 +3,17 @@
<head>
<title><%= camelized %></title>
<%%= csrf_meta_tags %>
+ <%%= csp_meta_tag %>
<%- 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' => 'reload' %>
- <%%= javascript_include_tag 'application', 'data-turbolinks-track' => 'reload' %>
+ <%- 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_include_tag 'application' %>
+ <%%= stylesheet_link_tag 'application', media: 'all' %>
+ <%%= javascript_pack_tag 'application' %>
<%- end -%>
<%- end -%>
</head>
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.tt
index 513a2e0183..513a2e0183 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/rails
+++ b/railties/lib/rails/generators/rails/app/templates/bin/rails.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rake b/railties/lib/rails/generators/rails/app/templates/bin/rake.tt
index d14fc8395b..d14fc8395b 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/rake
+++ b/railties/lib/rails/generators/rails/app/templates/bin/rake.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup
deleted file mode 100644
index acae810c1a..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/bin/setup
+++ /dev/null
@@ -1,33 +0,0 @@
-require 'pathname'
-require 'fileutils'
-include FileUtils
-
-# path to your application root.
-APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
-
-def system!(*args)
- system(*args) || abort("\n== Command #{args} failed ==")
-end
-
-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')
-
- # 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! 'bin/rails db:setup'
-
- 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/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 b/railties/lib/rails/generators/rails/app/templates/bin/update
deleted file mode 100644
index 770a605fed..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/bin/update
+++ /dev/null
@@ -1,28 +0,0 @@
-require 'pathname'
-require 'fileutils'
-include FileUtils
-
-# path to your application root.
-APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
-
-def system!(*args)
- system(*args) || abort("\n== Command #{args} failed ==")
-end
-
-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')
-
- puts "\n== Updating database =="
- system! 'bin/rails db:migrate'
-
- 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.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 c0a0bd0a3e..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/application.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-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"
-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
- # 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.
-<%- 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..1b0ee54071
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt
@@ -0,0 +1,47 @@
+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"
+<%= comment_if :skip_action_mailbox %>require "action_mailbox/engine"
+<%= comment_if :skip_action_text %>require "action_text/engine"
+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 30f5120df6..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', __dir__)
-
-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 b/railties/lib/rails/generators/rails/app/templates/config/cable.yml
deleted file mode 100644
index aa4e832748..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/cable.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-# Action Cable uses Redis by default to administer connections, channels, and sending/receiving messages over the WebSocket.
-production:
- adapter: redis
- url: redis://localhost:6379/1
-
-development:
- adapter: async
-
-test:
- adapter: async
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..6ab4a26084
--- /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/credentials.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..e422aa31fc
--- /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/credentials.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..678455c622
--- /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/credentials.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 f2c4922e7d..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-# MySQL. Versions 5.0 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:
-# http://dev.mysql.com/doc/refman/5.7/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..b5a0efef47
--- /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/credentials.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 80ceb9df92..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml
+++ /dev/null
@@ -1,68 +0,0 @@
-# PostgreSQL. Versions 9.1 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..009a81a6b8
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt
@@ -0,0 +1,69 @@
+# PostgreSQL. Versions 9.3 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/credentials.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 193423e84a..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml
+++ /dev/null
@@ -1,58 +0,0 @@
-# MySQL. Versions 5.0 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:
-# http://dev.mysql.com/doc/refman/5.7/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..386eb511e5
--- /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/credentials.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..f7b6dfafab
--- /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/credentials.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 bd5c0b10f6..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
+++ /dev/null
@@ -1,85 +0,0 @@
-# PostgreSQL. Versions 9.1 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: <%%= 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 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..44dafbd0c0
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt
@@ -0,0 +1,85 @@
+# PostgreSQL. Versions 9.3 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/credentials.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..27e8f2f35d
--- /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/credentials.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.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 7a537610e9..2887bc2d67 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
@@ -13,18 +13,25 @@ Rails.application.configure do
config.consider_all_requests_local = true
# Enable/disable caching. By default caching is disabled.
- if Rails.root.join('tmp/caching-dev.txt').exist?
+ # Run rails dev:cache to toggle caching.
+ if Rails.root.join('tmp', 'caching-dev.txt').exist?
config.action_controller.perform_caching = true
+ config.action_controller.enable_fragment_cache_logging = true
config.cache_store = :memory_store
config.public_file_server.headers = {
- 'Cache-Control' => 'public, max-age=172800'
+ '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.
@@ -40,15 +47,21 @@ 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.
# 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
<%- end -%>
- # Raises error for missing translations
+ # Raises error for missing translations.
# config.action_view.raise_on_missing_translations = true
# Use an evented file watcher to asynchronously detect changes in source code,
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
index 6bd5e42251..ed1cf09eeb 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
@@ -14,21 +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.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
- # `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'
@@ -36,15 +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] -%>
- # Action Cable endpoint configuration
+ # 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.*/ ]
- # Don't mount Action Cable in the main server process.
- # config.action_cable.mount_path = nil
<%- end -%>
-
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
@@ -58,17 +62,18 @@ Rails.application.configure do
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
- # Use a real queuing backend for Active Job (and separate queues per environment)
+ # Use a real queuing backend for Active Job (and separate queues per environment).
# config.active_job.queue_adapter = :resque
- # config.active_job.queue_name_prefix = "<%= app_name %>_#{Rails.env}"
+ # 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
@@ -86,11 +91,32 @@ Rails.application.configure do
if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
- config.logger = ActiveSupport::TaggedLogging.new(logger)
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
end
<%- unless options.skip_active_record? -%>
# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false
<%- end -%>
+
+ # Inserts middleware to perform automatic connection switching.
+ # The `database_selector` hash is used to pass options to the DatabaseSelector
+ # middleware. The `delay` is used to determine how long to wait after a write
+ # to send a subsequent read to the primary.
+ #
+ # The `database_resolver` class is used by the middleware to determine which
+ # database is appropriate to use based on the time delay.
+ #
+ # The `database_resolver_context` class is used by the middleware to set
+ # timestamps for the last write to the primary. The resolver uses the context
+ # class timestamps to determine how long to wait before reading from the
+ # replica.
+ #
+ # By default Rails will store a last write timestamp in the session. The
+ # DatabaseSelector middleware is designed as such you can define your own
+ # strategy for connection switching and pass that into the middleware through
+ # these configuration options.
+ # config.active_record.database_selector = { delay: 2.seconds }
+ # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
+ # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
index 42fee3b036..63ed3fa952 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
@@ -15,18 +15,25 @@ Rails.application.configure do
# 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=3600'
+ '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
@@ -34,11 +41,11 @@ Rails.application.configure do
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
- <%- end -%>
+ <%- end -%>
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
- # Raises error for missing translations
+ # Raises error for missing translations.
# config.action_view.raise_on_missing_translations = true
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 f613b40f80..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/active_record_belongs_to_required_by_default.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Require `belongs_to` associations by default. This is a new Rails 5.0
-# default, so it is introduced as a configuration option to ensure that apps
-# made on earlier versions of Rails are not affected when upgrading.
-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 51639b67a0..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# 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..e92382f2d9 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,14 @@
# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = '1.0'
-# Add additional assets to the asset load path
+# Add additional assets to the asset load path.
# Rails.application.config.assets.paths << Emoji.images_path
+<%- unless options[:skip_javascript] -%>
+# Add Yarn node_modules folder to the asset load path.
+Rails.application.config.assets.paths << Rails.root.join('node_modules')
+<%- end -%>
# Precompile additional assets.
-# application.js, application.css, and all non-JS/CSS in 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.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt
new file mode 100644
index 0000000000..59385cdf37
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt
@@ -0,0 +1,7 @@
+# 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/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 649e82280e..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/callback_terminator.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Do not halt callback chains when a callback returns false. This is a new
-# Rails 5.0 default, so it is introduced as a configuration option to ensure
-# that apps made with earlier versions of Rails are not affected when upgrading.
-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.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.tt
index 3b1c1b5ed1..3b1c1b5ed1 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb.tt
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt
new file mode 100644
index 0000000000..4a994e1e7b
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt
@@ -0,0 +1,4 @@
+# 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/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt
new file mode 100644
index 0000000000..ac033bf9dc
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb.tt
@@ -0,0 +1,16 @@
+# 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/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt
new file mode 100644
index 0000000000..dc1899682b
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb.tt
@@ -0,0 +1,4 @@
+# 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/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..d25552e923
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt
@@ -0,0 +1,33 @@
+# 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
+
+# Return false instead of self when enqueuing is aborted from a callback.
+# Rails.application.config.active_job.return_false_on_aborted_enqueue = true
+
+# Send Active Storage analysis and purge jobs to dedicated queues.
+# Rails.application.config.active_storage.queues.analysis = :active_storage_analysis
+# Rails.application.config.active_storage.queues.purge = :active_storage_purge
+
+# Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail.
+#
+# The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob),
+# will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions.
+# If you send mail in the background, job workers need to have a copy of
+# MailDeliveryJob to ensure all delivery jobs are processed properly.
+# Make sure your entire app is migrated and stable on 6.0 before using this setting.
+# Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/per_form_csrf_tokens.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/per_form_csrf_tokens.rb
deleted file mode 100644
index 1f569dedfd..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/per_form_csrf_tokens.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Enable per-form CSRF tokens.
-Rails.application.config.action_controller.per_form_csrf_tokens = true
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/request_forgery_protection.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/request_forgery_protection.rb
deleted file mode 100644
index 3eab78a885..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/request_forgery_protection.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Enable origin-checking CSRF mitigation.
-Rails.application.config.action_controller.forgery_protection_origin_check = 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/initializers/ssl_options.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/ssl_options.rb
deleted file mode 100644
index 1775dea1e7..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/ssl_options.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Configure SSL options to enable HSTS with subdomains.
-Rails.application.config.ssl_options = { hsts: { subdomains: true } }
diff --git a/railties/lib/rails/generators/rails/app/templates/config/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 b/railties/lib/rails/generators/rails/app/templates/config/puma.rb
deleted file mode 100644
index c7f311f811..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/puma.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# 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.
-#
-threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
-threads threads_count, 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. If you use this option
-# you need to make sure to reconnect any threads in the `on_worker_boot`
-# block.
-#
-# preload_app!
-
-# The code in the `on_worker_boot` will be called if you are using
-# clustered mode by specifying a number of `workers`. After each worker
-# process is booted this block will be run, if you are using `preload_app!`
-# option you will want to use this block to reconnect to any threads
-# or connections that may have been created at application boot, Ruby
-# cannot share connections between processes.
-#
-# on_worker_boot do
-# ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
-# end
-
-# Allow puma to be restarted by `rails restart` command.
-plugin :tmp_restart
diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt
new file mode 100644
index 0000000000..649253aeca
--- /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 web server 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 787824f888..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/routes.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-Rails.application.routes.draw do
- # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
-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 cdea2fd060..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 `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.
-
-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 b/railties/lib/rails/generators/rails/app/templates/config/spring.rb
deleted file mode 100644
index c9119b40c0..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/spring.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-%w(
- .ruby-version
- .rbenv-vars
- tmp/restart.txt
- tmp/caching-dev.txt
-).each { |path| Spring.watch(path) }
diff --git a/railties/lib/rails/generators/rails/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/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore
deleted file mode 100644
index 0e66cc4237..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/gitignore
+++ /dev/null
@@ -1,25 +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 -%>
-
-# Ignore Byebug command history file.
-.byebug_history
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..07207e1747
--- /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": "^6.0.0-alpha"<% unless options[:skip_turbolinks] %>,
+ "turbolinks": "^5.2.0"<% end -%><% unless skip_active_storage? %>,
+ "@rails/activestorage": "^6.0.0-alpha"<% end -%><% unless options[:skip_action_cable] %>,
+ "@rails/actioncable": "^6.0.0-alpha"<% 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/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..096cfd36a8
--- /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/channels/application_cable/connection_test.rb.tt b/railties/lib/rails/generators/rails/app/templates/test/channels/application_cable/connection_test.rb.tt
new file mode 100644
index 0000000000..800405f15e
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/test/channels/application_cable/connection_test.rb.tt
@@ -0,0 +1,11 @@
+require "test_helper"
+
+class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
+ # test "connects with cookies" do
+ # cookies.signed[:user_id] = 42
+ #
+ # connect
+ #
+ # assert_equal connection.user_id, "42"
+ # end
+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..47b4cf745c
--- /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: :number_of_processors, with: :threads)
+<%- else -%>
+ parallelize(workers: :number_of_processors)
+<% 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/db/system/change/change_generator.rb b/railties/lib/rails/generators/rails/db/system/change/change_generator.rb
new file mode 100644
index 0000000000..24db92fad7
--- /dev/null
+++ b/railties/lib/rails/generators/rails/db/system/change/change_generator.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require "rails/generators/base"
+
+module Rails
+ module Generators
+ module Db
+ module System
+ class ChangeGenerator < Base # :nodoc:
+ include Database
+ include AppName
+
+ class_option :to, required: true,
+ desc: "The database system to switch to."
+
+ def self.default_generator_root
+ path = File.expand_path(File.join(base_name, "app"), base_root)
+ path if File.exist?(path)
+ end
+
+ def initialize(*)
+ super
+
+ unless DATABASES.include?(options[:to])
+ raise Error, "Invalid value for --to option. Supported preconfigurations are: #{DATABASES.join(", ")}."
+ end
+
+ opt = options.dup
+ opt[:database] ||= opt[:to]
+ self.options = opt.freeze
+ end
+
+ def edit_database_config
+ template("config/databases/#{options[:database]}.yml", "config/database.yml")
+ end
+
+ def edit_gemfile
+ name, version = gem_for_database
+ gsub_file("Gemfile", all_database_gems_regex, name)
+ gsub_file("Gemfile", gem_entry_regex_for(name), gem_entry_for(name, *version))
+ end
+
+ private
+ def all_database_gems
+ DATABASES.map { |database| gem_for_database(database) }
+ end
+
+ def all_database_gems_regex
+ all_database_gem_names = all_database_gems.map(&:first)
+ /(\b#{all_database_gem_names.join('\b|\b')}\b)/
+ end
+
+ def gem_entry_regex_for(gem_name)
+ /^gem.*\b#{gem_name}\b.*/
+ end
+
+ def gem_entry_for(*gem_name_and_version)
+ gem_name_and_version.map! { |segment| "'#{segment}'" }
+ "gem #{gem_name_and_version.join(", ")}"
+ end
+ end
+ 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..542eb4c9e8 100644
--- a/railties/lib/rails/generators/rails/helper/helper_generator.rb
+++ b/railties/lib/rails/generators/rails/helper/helper_generator.rb
@@ -1,13 +1,20 @@
+# 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
+
+ private
+ def file_name
+ @_file_name ||= super.sub(/_helper\z/i, "")
+ end
end
end
end
diff --git a/railties/lib/rails/generators/rails/helper/templates/helper.rb b/railties/lib/rails/generators/rails/helper/templates/helper.rb.tt
index b4173151b4..b4173151b4 100644
--- a/railties/lib/rails/generators/rails/helper/templates/helper.rb
+++ b/railties/lib/rails/generators/rails/helper/templates/helper.rb.tt
diff --git a/railties/lib/rails/generators/rails/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/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 56efd35a95..79a06648b5 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
@@ -18,20 +19,20 @@ module Rails
def app
if mountable?
if api?
- directory 'app', exclude_pattern: %r{app/(views|helpers)}
+ directory "app", exclude_pattern: %r{app/(views|helpers)}
else
- directory 'app'
+ 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/mailers'
+ 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'
+ empty_directory_with_keep_file "app/helpers"
+ empty_directory_with_keep_file "app/views"
end
end
end
@@ -60,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
@@ -71,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?
@@ -81,16 +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[:api] = options.api?
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
@@ -105,16 +114,14 @@ task default: :test
end
def test_dummy_assets
- template "rails/javascripts.js", "#{dummy_path}/app/assets/javascripts/application.js", 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"
@@ -149,7 +156,7 @@ task default: :test
end
def bin(force = false)
- bin_file = engine? ? 'bin/rails.tt' : 'bin/test.tt'
+ bin_file = engine? ? "bin/rails.tt" : "bin/test.tt"
template bin_file, force: force do |content|
"#{shebang}\n" + content
end
@@ -161,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
@@ -186,7 +193,7 @@ task default: :test
desc: "Skip gemspec file"
class_option :skip_gemfile_entry, type: :boolean, default: false,
- desc: "If creating plugin in application's directory " +
+ desc: "If creating plugin in application's directory " \
"skip adding entry to Gemfile"
class_option :api, type: :boolean, default: false,
@@ -195,10 +202,6 @@ task default: :test
def initialize(*args)
@dummy_path = nil
super
-
- unless plugin_path
- raise Error, "Plugin name should be provided in arguments. For details run: rails plugin new --help"
- end
end
public_task :set_default_accessors!
@@ -258,20 +261,14 @@ task default: :test
build(:leftovers)
end
- public_task :apply_rails_template, :run_bundle
-
- def run_after_bundle_callbacks
- @after_bundle_callbacks.each do |callback|
- callback.call
- end
- end
+ public_task :apply_rails_template
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
@@ -283,10 +280,10 @@ task default: :test
end
def namespaced_name
- @namespaced_name ||= name.gsub('-', '/')
+ @namespaced_name ||= name.tr("-", "/")
end
- protected
+ private
def create_dummy_app(path = nil)
dummy_path(path) if path
@@ -304,7 +301,7 @@ task default: :test
end
def engine?
- full? || mountable?
+ full? || mountable? || options[:engine]
end
def full?
@@ -320,7 +317,7 @@ 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?
@@ -328,7 +325,7 @@ task default: :test
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
@@ -340,11 +337,11 @@ task default: :test
end
def wrap_in_modules(unwrapped_code)
- unwrapped_code = "#{unwrapped_code}".strip.gsub(/\s$\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
- str += content.present? ? "\nend" : "end"
+ str = +"module #{mod}\n"
+ str << content.lines.map { |line| " #{line}" }.join
+ str << (content.present? ? "\nend" : "end")
end
end
@@ -357,7 +354,7 @@ task default: :test
end
def camelized
- @camelized ||= name.gsub(/\W/, '_').squeeze('_').camelize
+ @camelized ||= name.gsub(/\W/, "_").squeeze("_").camelize
end
def author
@@ -379,11 +376,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 " \
@@ -415,7 +412,6 @@ task default: :test
require 'rake/testtask'
Rake::TestTask.new(:test) do |t|
- t.libs << 'lib'
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = false
@@ -437,12 +433,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 d84d1aabdb..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec
+++ /dev/null
@@ -1,24 +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.md"]
-
- <%= '# ' if options.dev? || options.edge? -%>s.add_dependency "rails", "<%= Array(rails_version_specifier).join('", "') %>"
-<% unless options[:skip_active_record] -%>
-
- s.add_development_dependency "<%= gem_for_database[0] %>"
-<% 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 22a4548ff2..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', '<%= 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/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 b/railties/lib/rails/generators/rails/plugin/templates/README.md
deleted file mode 100644
index 9d2b74416e..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/README.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# <%= 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](http://opensource.org/licenses/MIT).
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/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile
deleted file mode 100644
index f1943644e4..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.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", __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 abbacd9bec..b86ef0f2f8 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class ApplicationController < ActionController::#{api? ? "API" : "Base"}
#{ api? ? '# ' : '' }protect_from_forgery with: :exception
end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt
index 25d692732d..be078f36de 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
module ApplicationHelper
end
rb
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt
index bad1ff2d16..846863bc13 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class ApplicationJob < ActiveJob::Base
end
rb
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt
index 09aac13f42..246e274348 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt
index 8aa3de78f1..21465278be 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt
index 6bc480161d..6e54a1ce9d 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt
@@ -2,9 +2,13 @@
<html>
<head>
<title><%= humanized %></title>
+ <%%= csrf_meta_tags %>
+ <%%= csp_meta_tag %>
+
<%%= stylesheet_link_tag "<%= namespaced_name %>/application", media: "all" %>
+ <%- unless options[:skip_javascript] -%>
<%%= javascript_include_tag "<%= namespaced_name %>/application" %>
- <%%= csrf_meta_tags %>
+ <%- end -%>
</head>
<body>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
index 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
index 62b94618fd..8e7d321626 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt
@@ -1,8 +1,4 @@
-$: << File.expand_path(File.expand_path('../../test', __FILE__))
+$: << File.expand_path("../test", __dir__)
-require 'bundler/setup'
-require 'rails/test_unit/minitest_plugin'
-
-Rails::TestUnitReporter.executable = 'bin/test'
-
-exit Minitest.run(ARGV)
+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 54c78d7927..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/gitignore
+++ /dev/null
@@ -1,9 +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/
-<% 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 8938770fc4..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
- 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%/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.tt
index b08f4ef9ae..b08f4ef9ae 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt
index 88a2c4120f..88a2c4120f 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb
deleted file mode 100644
index d03b1be878..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require_relative 'boot'
-
-<% 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"
-require "action_view/railtie"
-<%= comment_if :skip_action_mailer %>require "action_mailer/railtie"
-require "active_job/railtie"
-<%= comment_if :skip_action_cable %>require "action_cable/engine"
-<%= comment_if :skip_test %>require "rails/test_unit/railtie"
-<%= comment_if :skip_sprockets %>require "sprockets/railtie"
-<% end -%>
-
-Bundler.require(*Rails.groups)
-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.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 b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js
deleted file mode 100644
index 8d21b2b6fb..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js
+++ /dev/null
@@ -1,11 +0,0 @@
-
-<% 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/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 b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt
index 2f23844f5e..2f23844f5e 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.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.tt
index 694510edc0..694510edc0 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt
index 1ee05d7871..1ee05d7871 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb.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 a5eebcb19f..0000000000
--- a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb
+++ /dev/null
@@ -1,27 +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
-
-<% unless engine? -%>
-Rails::TestUnitReporter.executable = 'bin/test'
-<% end -%>
-
-# Load fixtures from the engine
-if ActiveSupport::TestCase.respond_to?(:fixture_path=)
- ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__)
- ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path
- 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/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 79f8b7f96f..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;
}
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..8b46eb88ae 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,15 +19,27 @@ 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|
invoke invoked, [ controller_name ]
end
+
+ private
+
+ def permitted_params
+ params = attributes_names.map { |name| ":#{name}" }.join(", ")
+ params += attributes.select(&:attachments?).map { |a| ", #{a.name}: []" }.join
+ params
+ end
end
end
end
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 400afec6dc..0000000000
--- a/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-<% 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/api_controller.rb.tt b/railties/lib/rails/generators/rails/scaffold_controller/templates/api_controller.rb.tt
new file mode 100644
index 0000000000..bb26370276
--- /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(<%= permitted_params %>)
+ <%- 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 42b9e34274..0000000000
--- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-<% 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 @<%= 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.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.tt b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb.tt
new file mode 100644
index 0000000000..82b43987b4
--- /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(<%= permitted_params %>)
+ <%- 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.tt
index ff41fef9e9..ff41fef9e9 100644
--- a/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb
+++ b/railties/lib/rails/generators/test_unit/controller/templates/functional_test.rb.tt
diff --git a/railties/lib/rails/generators/test_unit/generator/generator_generator.rb b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb
index 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..ba27ed329b 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,8 +8,14 @@ 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
+
+ private
+
+ def file_name
+ @_file_name ||= super.sub(/_test\z/i, "")
+ end
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 76a0b79654..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 0681780c97..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 || 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/fixtures.yml.tt b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt
new file mode 100644
index 0000000000..0fd9f305d7
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml.tt
@@ -0,0 +1,29 @@
+# Read about fixtures at https://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) %>
+ <%- elsif !attribute.virtual? -%>
+ <%= 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..6df50c3217 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,25 @@ 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
+
+ def boolean?(name)
+ attribute = attributes.find { |attr| attr.name == name }
+ attribute&.type == :boolean
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 0d18478043..0000000000
--- a/railties/lib/rails/generators/test_unit/scaffold/templates/api_functional_test.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-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 create <%= singular_table_name %>" do
- assert_difference('<%= class_name %>.count') do
- post <%= index_helper %>_url, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> }
- end
-
- assert_response 201
- end
-
- test "should show <%= singular_table_name %>" do
- get <%= show_helper %>
- assert_response :success
- end
-
- test "should update <%= singular_table_name %>" do
- patch <%= show_helper %>, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> }
- assert_response 200
- end
-
- test "should destroy <%= singular_table_name %>" do
- assert_difference('<%= class_name %>.count', -1) do
- delete <%= show_helper %>
- 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 0e6bef12fc..0000000000
--- a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-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_hash} }" %> }
- end
-
- assert_redirected_to <%= singular_table_name %>_path(<%= 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_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 <%= show_helper %>
- 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..4f5bbf1108
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/scaffold/templates/system_test.rb.tt
@@ -0,0 +1,57 @@
+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| -%>
+ <%- if boolean?(attr) -%>
+ check "<%= attr.humanize %>" if <%= value %>
+ <%- else -%>
+ fill_in "<%= attr.humanize %>", with: <%= value %>
+ <%- end -%>
+ <%- 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| -%>
+ <%- if boolean?(attr) -%>
+ check "<%= attr.humanize %>" if <%= value %>
+ <%- else -%>
+ fill_in "<%= attr.humanize %>", with: <%= value %>
+ <%- end -%>
+ <%- 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..adecf74b70
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/system/system_generator.rb
@@ -0,0 +1,24 @@
+# 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
+
+ private
+ def file_name
+ @_file_name ||= super.sub(/_test\z/i, "")
+ 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 76758df86d..c4cff9090b 100644
--- a/railties/lib/rails/generators/testing/assertions.rb
+++ b/railties/lib/rails/generators/testing/assertions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Rails
module Generators
module Testing
@@ -29,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
@@ -113,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 94b5e52224..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,48 +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
# Clears all files and directories in destination.
- def prepare_destination
+ def prepare_destination # :doc:
rm_rf(destination_root)
mkdir_p(destination_root)
end
- def migration_file_name(relative) # :nodoc:
+ def migration_file_name(relative)
absolute = File.expand_path(relative, destination_root)
- dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, '')
+ 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..72b555ec19 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,65 @@ 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
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
- "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})"
+ property "Ruby version" do
+ RUBY_DESCRIPTION
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..f74d979721 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' }
+ prepend_view_path ActionDispatch::DebugView::RESCUES_TEMPLATE_PATH
+ layout -> { request.xhr? ? false : "application" }
before_action :require_local!
@@ -13,7 +15,7 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc:
def properties
@info = Rails::Info.to_html
- @page_title = 'Properties'
+ @page_title = "Properties"
end
def routes
@@ -21,24 +23,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 6143cf2dd9..5cffa52860 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
+ prepend_view_path ActionDispatch::DebugView::RESCUES_TEMPLATE_PATH
before_action :require_local!, unless: :show_previews?
- before_action :find_preview, only: :preview
+ before_action :find_preview, :set_locale, only: :preview
+
+ helper_method :part_query, :locale_query
+
+ content_security_policy(false)
def index
@previews = ActionMailer::Preview.all
@@ -14,12 +20,12 @@ 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])
@@ -32,7 +38,7 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc:
end
else
@part = find_preferred_part(request.format, Mime[:html], Mime[:text])
- render action: 'email', layout: false, formats: %w[html]
+ render action: "email", layout: false, formats: [:html]
end
else
raise AbstractController::ActionNotFound, "Email '#{@email_action}' not found in #{@preview.name}"
@@ -40,15 +46,15 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc:
end
end
- protected
- def show_previews?
+ private
+ def show_previews? # :doc:
ActionMailer::Base.show_previews
end
- def find_preview
+ def find_preview # :doc:
candidates = []
- params[:path].to_s.scan(%r{/|$}){ candidates << $` }
- preview = candidates.detect{ |candidate| ActionMailer::Preview.exists?(candidate) }
+ params[:path].to_s.scan(%r{/|$}) { candidates << $` }
+ preview = candidates.detect { |candidate| ActionMailer::Preview.exists?(candidate) }
if preview
@preview = ActionMailer::Preview.find(preview)
@@ -57,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 e47616a87f..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!
@@ -180,7 +182,7 @@ module Rails
end
def extensions # :nodoc:
- $1.split(',') if @glob =~ /\{([\S]+)\}/
+ $1.split(",") if @glob =~ /\{([\S]+)\}/
end
# Expands all paths against the root and return all unique values.
@@ -188,13 +190,11 @@ module Rails
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
@@ -206,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
@@ -214,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 b63d3a58d2..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,54 +29,52 @@ module Rails
end
end
- protected
+ private
- def call_app(request, env)
- instrumenter = ActiveSupport::Notifications.instrumenter
- instrumenter.start 'request.action_dispatch', request: request
- logger.info { started_request_message(request) }
- resp = @app.call(env)
- resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) }
- resp
- rescue Exception
- finish(request)
- raise
- ensure
- ActiveSupport::LogSubscriber.flush_all!
- end
+ def call_app(request, env) # :doc:
+ instrumenter = ActiveSupport::Notifications.instrumenter
+ instrumenter.start "request.action_dispatch", request: request
+ logger.info { started_request_message(request) }
+ 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
- # 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 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 99dd571a00..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
- def generate_railtie_name(string) #:nodoc:
+ 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,6 +192,16 @@ 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
@@ -218,36 +224,37 @@ module Rails
end
def railtie_namespace #:nodoc:
- @railtie_namespace ||= self.class.parents.detect { |n| n.respond_to?(:railtie_namespace) }
+ @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..ab5339bf24 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.5.0") && 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.5.0 or newer.
You're running
#{desc}
- Please upgrade to Ruby 2.2.2 or newer to continue.
+ Please upgrade to Ruby 2.5.0 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 8dd87b6cc5..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/rails/tasks/annotations.rake</tt>.
-#
-# Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that
-# represent the line where the annotation lives, its tag, and its text. Note
-# the filename is not stored.
-#
-# Annotations are looked for in comments and modulus whitespace they have to
-# start with the tag optionally followed by a colon. Everything up to the end
-# of the line (or closing ERB comment tag) is considered to be their text.
-class SourceAnnotationExtractor
- class Annotation < Struct.new(:line, :tag, :text)
- def self.directories
- @@directories ||= %w(app config db lib test) + (ENV['SOURCE_ANNOTATION_DIRECTORIES'] || '').split(',')
- end
+# 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 d3e33584d7..2f644a20c9 100644
--- a/railties/lib/rails/tasks.rb
+++ b/railties/lib/rails/tasks.rb
@@ -1,4 +1,6 @@
-require 'rake'
+# frozen_string_literal: true
+
+require "rake"
# Load Rails Rakefile extensions
%w(
@@ -12,8 +14,9 @@ require 'rake'
restart
routes
tmp
+ yarn
).tap { |arr|
- arr << 'statistics' if Rake.application.current_scope.empty?
+ 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
index d2ceaacc0c..716fb6a331 100644
--- a/railties/lib/rails/tasks/dev.rake
+++ b/railties/lib/rails/tasks/dev.rake
@@ -1,8 +1,11 @@
-require 'rails/dev_caching'
+# frozen_string_literal: true
+
+require "rails/command"
+require "active_support/deprecation"
namespace :dev do
- desc 'Toggle development mode caching on/off'
- task :cache do
- Rails::DevCaching.enable_by_file
+ 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 e678103f63..8d77904210 100644
--- a/railties/lib/rails/tasks/engine.rake
+++ b/railties/lib/rails/tasks/engine.rake
@@ -1,8 +1,21 @@
+# 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_ROOT) || !ENGINE_ROOT
ENGINE_ROOT = find_engine_path(APP_RAKEFILE)
@@ -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 Active Record"
+ desc "Create a db/schema.rb file that can be portably used against any database supported by Active Record"
app_task "schema:dump"
desc "Load a schema.rb file into the database"
@@ -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 61fb8311a5..2886986865 100644
--- a/railties/lib/rails/tasks/framework.rake
+++ b/railties/lib/rails/tasks/framework.rake
@@ -1,24 +1,24 @@
-require 'active_support/deprecation'
+# 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:active_storage", "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},
@@ -26,58 +26,37 @@ namespace :app 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"],
- { api: !!Rails.application.config.api_only },
- destination_root: Rails.root
- File.exist?(Rails.root.join("config", "application.rb")) ?
- gen.send(:app_const) : gen.send(:valid_const?)
- gen
- 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 :active_storage do
+ Rails::AppUpdater.invoke_from_app_generator :update_active_storage
end
- end
-end
-namespace :rails do
- %i(update template templates:copy update:configs update:bin).each do |task_name|
- task "#{task_name}" do
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Running #{task_name} with the rails: namespace is deprecated in favor of app: namespace.
- Run bin/rails app:#{task_name} instead.
- MSG
- Rake.application.invoke_task("app:#{task_name}")
+ 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 073f235ec5..ec56957204 100644
--- a/railties/lib/rails/tasks/log.rake
+++ b/railties/lib/rails/tasks/log.rake
@@ -1,9 +1,10 @@
+# frozen_string_literal: true
+
namespace :log do
-
- ##
+ ##
# Truncates all/specified log files
- # ENV['LOGS']
- # - defaults to standard environment log files i.e. 'development,test,production'
+ # 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)"
@@ -14,23 +15,27 @@ namespace :log do
end
def log_files
- if ENV['LOGS'] == 'all'
+ if ENV["LOGS"] == "all"
FileList["log/*.log"]
- elsif ENV['LOGS']
- log_files_to_truncate(ENV['LOGS'])
+ elsif ENV["LOGS"]
+ log_files_to_truncate(ENV["LOGS"])
else
- log_files_to_truncate("development,test,production")
+ log_files_to_truncate(all_environments.join(","))
end
end
def log_files_to_truncate(envs)
- envs.split(',')
+ 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 7e15bb55a1..074e3e89a1 100644
--- a/railties/lib/rails/tasks/restart.rake
+++ b/railties/lib/rails/tasks/restart.rake
@@ -1,6 +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')
- FileUtils.rm_f('tmp/pids/server.pid')
+ 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 69103aa5d9..21ce900a8c 100644
--- a/railties/lib/rails/tasks/routes.rake
+++ b/railties/lib/rails/tasks/routes.rake
@@ -1,35 +1,9 @@
-require 'active_support/deprecation'
-require 'active_support/core_ext/string/strip' # for strip_heredoc
-require 'optparse'
+# frozen_string_literal: true
-desc 'Print out all defined routes in match order, with names. Target specific controller with -c option, or grep routes using -g option'
-task routes: :environment do
- all_routes = Rails.application.routes.routes
- require 'action_dispatch/routing/inspector'
- inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes)
- if ARGV.any?{ |argv| argv.start_with? 'CONTROLLER' }
- puts <<-eow.strip_heredoc
- Passing `CONTROLLER` to `bin/rails routes` is deprecated and will be removed in Rails 5.1.
- Please use `bin/rails routes -c controller_name` instead.
- eow
- end
-
- routes_filter = nil
- routes_filter = { controller: ENV['CONTROLLER'] } if ENV['CONTROLLER']
-
- OptionParser.new do |opts|
- opts.banner = "Usage: rails routes [options]"
- opts.on("-c CONTROLLER") do |controller|
- routes_filter = { controller: controller }
- end
-
- opts.on("-g PATTERN") do |pattern|
- routes_filter = pattern
- end
+require "rails/command"
+require "active_support/deprecation"
- end.parse!(ARGV.reject { |x| x == "routes" })
-
- puts inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, routes_filter)
-
- exit 0 # ensure extra arguments aren't interpreted as Rake tasks
+task routes: :environment do
+ 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 a919d36939..5abba7d3b4 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,22 +9,27 @@ STATS_DIRECTORIES = [
%w(Jobs app/jobs),
%w(Models app/models),
%w(Mailers app/mailers),
- %w(Javascripts app/assets/javascripts),
+ %w(Mailboxes app/mailboxes),
+ %w(Channels app/channels),
+ %w(JavaScripts app/assets/javascripts),
+ %w(JavaScript app/javascript),
%w(Libraries lib/),
- %w(Tasks lib/tasks),
%w(APIs app/apis),
%w(Controller\ tests test/controllers),
%w(Helper\ tests test/helpers),
%w(Model\ tests test/models),
%w(Mailer\ tests test/mailers),
+ %w(Mailbox\ tests test/mailboxes),
+ %w(Channel\ tests test/channels),
%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..48a8d8e143
--- /dev/null
+++ b/railties/lib/rails/tasks/yarn.rake
@@ -0,0 +1,18 @@
+# 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
+ 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 fed96fbc85..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="formatChanged(this);">
- <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.
@@ -113,15 +130,30 @@
<% end %>
<script>
- function formatChanged(form) {
- var part_name = form.options[form.selectedIndex].value
- var iframe =document.getElementsByName('messageBody')[0];
- iframe.contentWindow.location.replace(part_name);
-
- if (history.replaceState) {
- var url = location.pathname.replace(/\.(txt|html)$/, '');
- var format = /html/.test(part_name) ? '.html' : '.txt';
- window.history.replaceState({}, '', url + format);
+ 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>
diff --git a/railties/lib/rails/templates/rails/welcome/index.html.erb b/railties/lib/rails/templates/rails/welcome/index.html.erb
index 5cdb7e6a20..6750e01029 100644
--- a/railties/lib/rails/templates/rails/welcome/index.html.erb
+++ b/railties/lib/rails/templates/rails/welcome/index.html.erb
@@ -26,18 +26,28 @@
p { font-family: monospace; }
.container {
- width: 960px;
+ max-width: 960px;
margin: 0 auto 40px;
overflow: hidden;
}
-
section {
margin: 0 auto 2rem;
padding: 1rem 0 0;
- width: 700px;
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>
@@ -52,11 +62,11 @@
<h1>Yay! You&rsquo;re on Rails!</h1>
- <img alt="Welcome" width="600" height="350" src="data:image/jpg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEABALCwsMCxAMDBAXDw0PFxsUEBAUGx8XFxcXFx8eFxoaGhoXHh4jJSclIx4vLzMzLy9AQEBAQEBAQEBAQEBAQEABEQ8PERMRFRISFRQRFBEUGhQWFhQaJhoaHBoaJjAjHh4eHiMwKy4nJycuKzU1MDA1NUBAP0BAQEBAQEBAQEBAQP/CABEIArwEsAMBIgACEQEDEQH/xAAbAAEAAgMBAQAAAAAAAAAAAAAAAwQBAgUGB//aAAgBAQAAAAD6AAAAAAAAAAAMZAAAGMgMZABjIAAAAAAAAAAAAAAAAAAYyDGQAAADBkAxljIAADGQAAAAAAAAAAAANMSAAAAAAABjLGQAAAAAAAAAAAAADEUwDSGyGGQAAxkAAAAAAAAAxkGMjGQAAAAAAFS2Yy1xtVthSt7AAAABjIAAAAAAAAAAAAAAAAMZBDtJVtFS3TuYxtjSQAAAAAADGQAAAADGQAAAAAAACrY2BVtEM1S3BmbSvbAAMZAxkAAAAAAMMgAAMZAGM4yAAAAK02+DKragnKlqNLHHYi23DGQYyBjIAAAAAAAAAAAAAAA1znDEE0FnMcdiOJLJVtU7hWn2Yj1nq2NgwyGGQAAGMgAAYyAAABjIAAAAAhkVLdW1mtPvipPvBttLVtQywz1pt9a0qapayFWaSpbKlrIAAAAAAxljIAAMZAAAABptkxkxmncp3EWs6pmfeLWeraK9ivLvjEO28FipdVrEE+adwhmFS2a6SmMgAAAAAAAYyAAAACHWxhTuhUtVbO2KttTs521gs1bQV5JCrYzBJBbYya17QK89W2VbOYdpAAAAAYyAADGTGQAAADFS5rVW8jSCZJirbRyK1itaq2kciDeSvYqWs184sMY2Q5l1inKe82zRjercYyMZAYZxkAYyAAMZAAAAAabqtjaOGxtrtjNO4hkr2gp3KlupbQzVd95Kdynb1gTa7R7TKu+2c7otN5YpMw676zgGuwMZAAAAAYyAAACPbY1jk21r2mta1SvVLdO2hzHaMZq2qkmlmtaRxyb1bdOxAzY13qW8qk0mVaxUt5Yyp3KV2HMpXm2ijsgAAAAAGMgACPfIMV7JWlkilqZn3p3KlrKvvtmstwT0rla1U3zvBbK8mYLVfZCt5U7jFW2rT87pVLhXsQZmxmlZ22xTu4p3MgAAAAAAAAI4bQDGWKtuGWLXM9WxiFZ0gmhngs1Leu9WzVtw5itQT15MwWNoNoLgp28wpYYrdWTaU1zUuEUebCpZ2rSyAxkDGQAAAAAGMitYyBDNjNeSRirJNivaqW2KdyndryxSSK09a3HpOp3INsxR3a8le5VtYq63KtqHfePWLeaJvFY212o3dYpIbUUdkAAAAAAwyABhkBjIQS5qXIJ6k+d6V2vLtSzdo3oW0djEWYbete0qW4NWNblWetco3ocRXKdypbVLVSxvVsYiTV5tLFK5FLrWuAAAAAGMgAAMZAVbVWWXEFgQbRWNNZ49ZsVbeteard1hsVLlO1BpbQTbVLdffMOLVWzUu0rtaatcqW6dxUk2lqWdqdqCTend0jsGCtaAAAAADGQBjIMZIZiDaSpcU7bMMtS1XtU7lK5lTt51q2N1W3TuUrlWbbWWraqW6+6PW1Ut1LdO7UtVLdW3TuaQx3dK9uOPM2dNJqdwFSxuAAAAAADGcZCGZSt7K0shUtxxz6wZkxiOzXs1bUe2yHMdmrbKdyGancp3MVLlexUt1pcR626dynZisU7jTWSrbqWYbFO3mlcyFfeWtYgnqz53rWQAAAAAAAV584q2ypaj2QsSa62Kl2nagnrzRWMxwWqdjbdUtVbeNa1zRtruVLUeuUVuCWvaKdwR6zU7jFO7HFYrWjWtbr7RWa82mLDGQAADGQYzjIAADTdriGxrvWkkqXMVJNtorKpbpXMqlujdZVpYLUcelrancYyqWcokNnbJFLTuBpiCztX2mp26tjSXNOSSC2hxPrs0p3sgAAAAYywyACpayVJ9NpY0FuLSWLeC3VtVt8xzRzVbbOuc6Q62al2DO+spjNSxiQjxJko3qduKarag0l0sQzYhkgs07sDMNvMcNoR17cUwDGQAAAMMgVbTBVtRTYq2qtnNSTeWCWqtRS151a3ghzPjwfuN9kGYbmtVbKnL9BrmpMmr7ya74q2qlmKendabsZFS0xmpcrTb61reKtuOC1ppOAAGMgAGGQGM1LNW5FpYqzop6dyLOlgr42mzUtKtjcxjaPyElX3I03UZJ9o/M6+tId9s+W1irei6ke0dlpFYw12rWNJcVbG2aljEaWvazHnEFsY13AGGQMZAAAYyhxtvtVn2jlhbb5qW68kmIoLlO3X1s1bjME7m+Z6G3po5AxXlr+F7kPsAYpeUit2vUqk8irY2qz1reDarPWuaQWalurmaSOSKKzStbqVvaKYAAwyBgyMZBSuZVp9mKlxjFefeDbWapc1zpCmr2cbax2KVytP5T1fn/RVrQIWPJel53oI+VXudTLlxcH0/QQy17NO5Tt51zkU7mM07debeDM0GskVmnaq3KdvYGMgAAAADFW3Xnq2tYJorOKN6pb1g1ts15JKlpipdKtqtJDb8/wBjjehxFwqtnt2TTiUvU7eWvVNofU55knG9RhlptUtVrbys/a3m00lxHjeKzHFZigsQWqdvKnb1SaaTAAAAxkAxkj03k0hsRTVZpNa1mrca1rdS1kRS4yYzileR+fn6u3n7/H7XN6vScbm+kn18zNnGvWvcXpcT0TI1i0mSeZcn1fQyqWK9qncU7ulW1XtU7W+tO5HHZ1rzw2gAAAwyDDKCdXkkQpmKdzOkEmu8tSxiNPlDM08/0rkmWK1rzfb5db0m3Bp9nznq7PmsemU+Smq3te157r8zuM4ZIZalzzfd4fo4pWMo874Yp3alupYkxSu19pytrazHJpHOGMgGGQGMtdo5NNq1mCavPvGgtotJ68u9O7UsaTMM8fg35IOh2lO3ByaPQlq5nn5sXa836i287f6NLkZ5/puZYn6dHh707vpcsa78O1D2KdwjkQTsUrke9efbGyrNrmSpYkhnxHXuZAAAArSyVZZcRxWatqpbp3KlnSbXNabfKGbWtbcbqyV/OenkxV872OrptngVPUUbele55npcS96XGvmsda7wNc3+Tz/X2PLX99uXa9KHGi608UxBO03Vpt9NcS0rmallHjS1rWtbYrWgAAADFO5mpZaa52imgsVLlO41r2qN2rbyVrFbzno7Xm/Qbx8rl759LK0853IuR6WnQscyxS9HZQ+XsXJehxa+lX1NvyEG/Smg36lscWv6DavriSXbGTSDzXpLZjFW3kjkrLEGZ2mdgAAAGGcVLcFitLoZzDYrbTSMNW4cLbrcN6DyTs+W9Rb5PoXO4Hd6PjvVcbbXp2fN+kly85yt+ra48MnZ63Bodjl197fofP8Aoxxa/ek8tRvTKvfu5eS53Y9LmPXevazBNnWtPIwIJ8gAAABjWC0xVswWadrOa+8mcRTYy05Haqcx2/MWu/xK/Ugkl8/65S4/at8vgdOpU9TJ53me2n14F7zUt+POvqeRzYut5W736/f816XGXMpdDoeT7EcMfJ9jO087X9HaQb17eYU0GlmKZrUt7AAADGTDLTbWvvvKxUt51qXYJs6xTkMuvA3v1Ox570fO4u/o4+Dnqcij7CxyqUPd81Yt1NqnoOL6nOfP9WLm1p6lf2UulLyFn08cM3T836PjV7PQ5vHvcjq75npem3eektdMxBYYzitLJSt7Z1zW2sGMgAABVsYgtQZguawTwbya6TsqNzmzXsee7Xk7druee6OPPZ6XT0pw1JJ/SPM7VbGKtiWDp8rtdPHhree3yJoej2UXl6vVwg9Pv5rq17VK3y9+1xqOIqtmX2HJpW+xQ2u4zpvChs74R17mlezvjIAABjIxXswpqs1S+xFpLIVbSOPyvYr9HXp+e9BS5fRqwc6Wt0sNeP170vWxnzmApevzlpwoaN67V1k9C8zZ7Pm9ZIexp5z0XLvT9Tz/AEOT3KvX4vY4Xc53Ps9vTxdf1HXV7Gla3litmxV3nzrFOxkAAGMtNzCDeSpbawz1pt9mMipzrfn/AE9rzPf5/Q34EG/V04Vd1afruFF6U8xtXxNNi32ChDV5F7fq8uL1PM5FP21TSz41Y9Bf5Fvhepo1uF6rmT25OQin7mODxNez6MihtIJa1nSG3lhnXOQGMgDGNq6fMUFytNmtapXsNY56Xm9/Q7xX/PegzjPLxF1nkLHoOX32vE7vF6HH37MzyNmTMEWJfT5ef7XB5PX5fep1fXeUkk9JxuxTqcC/6fPl7fSt48v3uNiSKWG1nt8CHz8dr03aVllpDizVklq24Y7VbFrIDGQMZYpXc1pJa0kdmnb0is607w8bep1LHsduRdn5lzgpO9L4633uJ6Q4fcrcmGWXt48NdsZFD2G7zXb4GLuLnQ4fS8V7Pm+g4/YaePt+oo8Gb02MeX9TyOZtmzH0uj53q3NOZ5T3kyOpYnrt4bVdJDNHpa2AAAxlq2VZsSRxzZ2rbTwbyHk4ou7JT9Dy6XUn248Pf4fV810Y5O1I4utDm9KDo+hg8peBBf7mnmqXY07dHTpczbzvobVvj9lU8ta9V57Tq33C60/FqekeY9NtX5V68z5v0hp4n03TxW22is4gkhs7Y1xKYyMZADGRVmkjxHZpW9ZAcXgdvu8ypY6nA9Dlwt+15retvy/XcaejWsWqPsuff24nB6exXo5u+ml816Lbz/oePZ6FHWfz8fTqd+Wh5uf1nm5PQZ14fecPoXOPbuvN+g811LGvnvR3Xmoef7WGzBJMNc5rYs5GMgADGNcSa1569qPErYR8HqvH+txyr3C6nM9hjHl/UcL0fA1oWfT8XkXa2noed38nM8zWso6/fvdgK3O7XnupdrU+rxuJR3n2xcr+y83L25aPK723L62fO+iPMbdCTocCD0m3nOhxPUZzFZzpFY01la7AAAYyMa7qk0M+UWLGXLo1+vxL1LtNed1NvKXcT9u7xep52z24bWPNd2rzvScu1aEPkJYOvvv19zDi37nlet09OV2edSiqaYr9J1KPV8z6DpeZdahX9Dyu5H56r6Wtc4tzq485I7yrLpNIDFWxrLjIAADXNexFtDNBNHZxnkVre3Q85yvc7eZ03u9XzWIrtrsczo8Tu8PrTHF7Tz/oK9HrDHA73E7vF17eRjzvo8edn7mvC79WGeGfTh3fP9q/c4a3HJtWs628+Z335HQzD62zxKnQ6yPfOm+PI+hvVt5Y4J5QAxkAQboLUM9O1DOcSj2ub2LXmu7Ycbfrcipf5nc5NyzzO7xO5Th6RzbFrgd7HC74ee73I7XD17zGXO8n0bNqPu48/wCh05Nri+ozxO3pVjnzP4/q3azXbr8Dm3s70bGa3rvH9TuhUn4/Hk9bvpmtJOAAAARwyyKlvG1Cp0uJ3NYJuNLYj4fqYafX8707/Pi6EK1Jy+yY4nc5kt7g94OD3PNdGjF6Sral87zrMNP1vnrG8XqccbtcXtuF3XmvQcL0PP4tb09Pz/d7lLnU58FePHp/H+jrYtdbOM+bsbdrFbNjMO0gAABiGevneGzWnrx3vMbzc27c7EPkN56PSz6CvzOxy+7wadmH0Pj+9au5OL1ZOB2/M0rmNdKfpOPrPzZ9MYxSs9GD1fmMSQ49dQ7HH7OOF3see3p9nWWbh0fWUulw6VrQc3eXow8/afs+b9hLW5Nf02YJdmsctaaQAABFHZqzY0lrTaWKsFOv2daPQ81n2HIq9LzvW6nKtWKcHSp2PJWPYyI6VXjWqN3nz7xz61ptodJ5ttIZNsY3owyIut3OV3+V2s+b9JTpc2Kv3LXSh5Pc4VWG1toY5ncs1eNj0snI5fQ9T5uP0u2u+K+01bFnYAACKTMMmatiNLtW2nOJz5L/ABLkV/pec9RJwrVP0HEuc+D03k47umscMma221mKKSZrBJsxrrGuIM6a5zvHnaBfc6WTrUOrztavqvLWZu/pxYOt5XrZKl/fuefmmg2oXKHX4fpLlOxIjizPsAABjMek+Mwa62IJI7OUVSx5Ordq9dz/AFvjfSdXmz83ucDn9Pi3oLUtKLONpNwAAwyabVdsrEcWMZu9arzqdi3y7E9zrcHf0fPns86lQWdLR1eNz+/zdqtzfndfrqqxXzYyjglmAAAK208e9O3Xsmeb5vtwc67z7VjTe/0t3B6lJyOh1vJTSgMZAAAABWm71rhY2l59XSSK/wCq8vD6bm9rPC7EnN4dH1XM7nH5npPMXZ96M1vt7kGk7fGcMgDGSOQBBixptRvqdOzL5PtR6x05LPL9VfHP810upVzbi87aAAABHpFvZACh7XTn9anzezx7OeRTu9ejX9HzuvHyrXn/AFUfH4/e6XL6VfyXraXR81a9LrPFDNFtLXzajSAAAAgZ23y4nHvw0O1yPT8C1zJrPR66DlpIpenQ6fF1pU20cyKPoDSlJjbXaTMCzds1ubve5+qaRDr1e6MQWOdyZZJMw7aS9aTkdOhJf4Xcgzxe9nycfruFNFW9TnSFievtYzivYyYyAACCXXTE9bndvgXotfL+t5vofB29Pbcql2+ZLrB08XqVrcAOBBTiz0+xayxXqRdOTYOdx796Plx7dTo5AIORHvnpz8qbh9Cj6nid+jp0VWh2ccC/f4O3d1hklr7RZs5AAAAaQ2EcE0UHletJyrUGvrLWIJ/LdvfzvTxtfoOjz+nKyYyAxFtIYyAANcbZAAY51Pe9xZpelyYYu1T7p5n0ufN8T32YKnR10mgjllgxZikV7GQAACDEkM0NnSvzIYedavcn2kxwKfSitXeVPd5+nXk51/YAYyAAAADGQAItqfM6kVWv0dafUvPJegu8LHdzwu5nWKKeVjTEElgxkAABFtrHZRwWK9rEevkLF6btcyC30OdZ5st7mTdOtZyAAAAAABjIAAxUuVtqWvWh48fal8Xc79w876JDieHSzHXksGKsk4AADGu2ta1SuVrelZai4O+ZOnSnm58213n9XIYyYZBjIAAAAAMaSABVtYzjiaZxRm9HyOTa2t9oNNYd58xwT767gAAEE9ZYp2JdalyPeGxU8/2ZeffoXegj3KNCPGlWf0koAAAAADGQpcnn1/dZAAUZFnlZrTX+dD3tmsFmOvJvpJX3mrrGkdkyDGQMVLeNoNbGkFnDc51Lt7xa8uOt0Y9dqlPo3NKcLSlYsSTSIYrvVkyDGQAAGMjytWzmj67j3rc2zIxkBjWhwO50tsoN4N586Zgmrb2K2JZgBjIxk0rbWdMQW8NiPh36XYmxngc+WrvLrIADGmu+6re9MAAAAYcbmR40tkdbpUcxeylDHL5s8VbXTGmc7Y7vXKubGdY5tIMzb4xmBYAAAxVlkilp29ynzO7nG2M1PGWo9bu4AABR9N0gNdsZGMsZCPlU4qu1gAGs/pVDkwx1OhjGcgK9n026tYzFrHujxcxVsazAAAa1Llebdhlx9MdlzOZSlsdy1ny8E4AABh6sAABilyedZ2AACp1aeZ4QABrB6ufKGGbXSeSOBvHNMAAMZNK1ratPs08/1nCpYmnyq9/q48NfyAAAVuz2gAAHnqO24AACGTYAADFT096GavrPvFFvtDNMAAMZGKlurLLDPW4vfkcfzu9g3p9vu8PiXQAAGtG96rYDlcDo1s3OjeB53nzRV8W5QAAAAACp6Hpx5kginxFJNjOkgAADXWSvtNzK01W51EEuylDPcx4+XYAADTn+o6mWMhx+BcyitelBryq8vQ8xtKAAAAAAKnoeohzLSmnVJW0gAAAHNrxc2xzvV3xjI8ikAAAUfTdIAOPxNdLfa6YDXzdCeQAAAAAAFb1dhFlWt5q77TV7AAAAc/jcy9IxQ7m1jpbB5LcAAAq+zyAAAByuDZAAAAAAAYoesvR74VlnSTIAAAK3J7XjLw1xvFn0N1jPm624AANaPo+uxkAAAa+OsgAAAAAABB2uvWlgsw6WsgAABjHnPSReJ6QBXm63VzzfMXwAGsOk0l7t7AMZABFX3s+QsaZAAAAAAADTs9RVxPIAADDIOB1qPAs7gBV2s1JZQGlVYtSzXbmTGQwyAY4/PqTV5I9JLYAAAADFHE1oABZ6HR12AAAANeZQhI98gCPfIBE7t2UDGQAAr+bxuxkAMRTAAAArySUob0oAJJO/uAAAAObxdgAADGQYu9zYIOPa6iCbYACj5uwABVgztHvLaAAAAxruAYyZ6vQkAAAAOFTwAAAAFfW126Wca3+TU3odv0IAFPyl0I4Yy7BiWQY13BjIAAAABjPobIAAACDndLgwgAAAAMyR4GvO9b0AAKHmLgp9fq2HMp3YrllR50AAM7Sbb7VYwAAALHoNgAAAMRTacOoAAAAAAw6PaAAg8Zne5V9NeABitQ1333232k23zkMU+dTAAABd7mQAAABjjc8AAAAAAqd3sAAY15PN7XRYZABjIAAIufz5r80FevXwABnfrdAAAAAMczkgAAMZAADfo9TYAAAAAxkAABjIMQ1q9evqT9WWTbbIABjIGMgFDi4AAAAAG/Q6m5jIAYyAAAGMgAAADEVePobAAAMZAABV5BFGAAAAN+n0dxjIAMZAAAAwyAADGQAMZAMZDGQAADGTWKGGGKGLACa3iCHQN+n0dwBjIAAAMZAAAAAAAAAYyAAAAAGI4YYYs27coxpBBBL0dxpuANdgxkAAwyDGTDIBjJjIAAAAAAAAABjIYZAAAABjIAAAAxkAAAAAAAAAAMZAAxkYyDTcAAAAADGQABhkAAAAAAAAB//EABkBAQADAQEAAAAAAAAAAAAAAAABAgMEBf/aAAgBAhAAAAAAAAAAAAAAAAAAAAAAFbDn6AAAAAAAAAAADLUxp05ZdM1sAAAAAAAAAAOfazn1nDScOmZAAAAAAAAAM6xOfRzk20z1zo6JYzrlqAAAAAAAGG+OzldPP08Hbg6aZ6zzTTsznHppfPLpw1WAAAAAAI5+jg7zm2x6o5zpzx3xiadFN+PoW4+ucK9YiQAAABWxhvkty9fPG2WvJ20ozt18+mVMt+jm6+Tfmv1ZznOXVz9HL1gAAADm6RS/Nvz6ZzrgtG+Vdsps5+jDp04N75xn0xn04L5x0WAAAABnXakZxaaa15NOrI5+/CLZ4dNJ159HVx3rfO9Lb2znQAAAFbY7c/RzWvjpWlono5tcevltSLzFnLuNK10w3nLe2UXnn6a6AAABS+O2K3NbTn3Vjr5HRflXzvvPNtlh1xfn6mWPVy6Vno4dbU1tnuAAAM9Mlueejj6s+fcptTa05YzF+hlS91uSb03z0MLxF4vFJtqAAAOW0aTnNbbc0wr2UshaMsuqLVTCuHXy7UtLn6+ZTe+NenPRYAAGM1tGXXya52vGemPVgr1KaROWiYTCaVxnqObo5p2xUtfK/RTQAAFMtcrzScd8rt89Bja4TEwTCTmndFsUTXO1po6eLqtXQAGd8dsKzXSnTza3xp02QTjqBMTWYtETMGHRnpmzms2rnNtqNgAMZisVmrW9G+ekCJmJgSrMVtMJlAJVtnSbsNq7ZagAQ4+umekRoLwIlTSFL0umuddb1SCtLXmE897Z1dOFekADKKMuisaTpzdAIpFdLzGC2sxRnvMQtARFgnPDTflOiLgCKLYzne9NLphNZEJTGabzFK2vEkWgApN+bTOp0Vy6gBhtzXiu+DpnG15ictBMQmuRreCYi0TW1LwCYTW3JF6003mwAq5rw2uVkTndE5xOqma9rITDObJTCYmBNac+171hoAIztzNJ3hEomEkqzIqmSExNYi0xImiyYnCN1J5enQAUc+2O+kJgimgkhMSglWly0ExETJnSbaE46s89NJAGVaIx9AQSCLIjPLlr18u/NpovpbWSYrYRlFtCYtCuau9gFDnlbW8Uq1mCKYZ45KTWL1m0VtrWL9PZImLRGUa2IlPn9eO9b3AHLrXPqmGUNpQcPnAAtbMN/VuQmJmFbIi0Vtz02ta4AUwtuRNa3kSjPKzDl06tKZ30tTKcrdeggCYmISyrhrtpnoAOe/PPTaM9ZgEgAAABAJictYjnnbnnppoAM6Tha8bWBIAAAAAEAqxi1tMOkAynFZouImMq89e202SAAGHH3aSAQlzRac6xptoAji6crbzCXFyYxALRPsbAAjLkxwro29Wa5ctKUifR6ePs545+qY0uAjl2aTGfPzW9OniQAPR7wBlwckAOyOWAHo9+XJtNKb9AA566apyy0y6uHzgCev0rFOPHp7DDl28yoAAHd6FcabJ2ABHJ5/Z3WK+FAB0etYM89bjDy8gAADu9LLOI6FwAjyuVO/f0PEzBOmvX1gAjyOcAAAOz0MWuoAOXi54DbTmg26+nWQA5+OMmJMAA1vTIHq9UgAZ+JAAE+n0XjNOoHi4r6ZrpzoACYA9bqAAc/k1AAJg7/RA8rm7PRmEV4+W1rWtazj5wAt7GwADLyMwAAaevqBWwAAMufopjjjlG+19uoAAp5WdIAAnr79QAAAABGWwAAAZ555Z5z0655ZaehoAAAAAAAAAAAAAAAAAAAAP/xAAZAQEBAQEBAQAAAAAAAAAAAAAAAQIDBAX/2gAIAQMQAAAAAAAAAAAAAAAAAAAAABYN4AAAAAAAAAAANZNXGtc1gAAAAAAAAAA3mN5by1iAAAAAAAAADSzWNkZ1m6mDTOsgAAAAAABvGsujn059uWmLrM6TXKt87LvnvKAAAAAADpjrxXeN87oxd41ZrFx0ynTm25gAAAAFhrOk6c9s6z05W2znuXVzjfPpjpOdamue8dOYAAAA6cxZ0xvOmNpcaudSN43jPbE0uGue00xAAAAAaZrRNYvSc9G+O7nW+epncc+ubNZrE0yAAACzWd46TO82yzHTOufSVA3kZtzvDWJq5bwgAAAWazpOkm8K59GJ0SzDpjW+ab5ta59IY6yXM1gAAAazpNsdMXeLNYuY1omGrInRLjWTealipkAAAdJcqszuVeVlI1rnZUq659M2S659FxNXFiAAA0sa59M6kazvnpebWTWSost05m8dJnYlmUAABdZ1Fm8akzrI0kVLFQHSYqaWXUk059cRAAVrOqS8+mZq4iyzWbFJZWsUljeNZtolqZrIAGi0skrFiyksoiunTjkQLBZqyN4udZAAOvO2LkQXeL289nq83o88b9fo8HCkF6evl5ZY3Jpee2AANK1iozvMWV7d9fn8V+tOfzWu+vZ8hSVLNb5xpnWs56GEADUaWS5sg6c1m2LNeqTzR39HPyoqWKljq5blpi65gDeOkXG2J7ufmh7vERdZnb6NfP8APULCztxKhpmzpYswgArcXEN4qPX5KevpjxOntvm44A9eMc4AqWW7zM2sgBqdGWFS6yrNjeA0kAb6dPNOvOI7zklm2ZXTGQBW86zmwXt38UAAWOvbhGAddcpHr9mOHjhqTVkgA1ab4VNXNkWWBMzeNZt3OvTzwXpnMX6fTz+PAll0uIApsmZe3pnhhUyzEsWKlqXWwI19Dt8zlQnbnrKQAdM281e7W/llTPMACwNdFBB0xOt5LN3MkAC7mFO3Xy5WCCZtqKRLoKSoHXOZq7znNgA3NzB7fHksAAAAAFlQ9njXbG3PWQBqzclvOAAAAAAAssK1ZJrABqbRm5LCMtUAAAmdgAsdEWpnIB1xqYBnOQA60ACZmVXoTKQ6Xpz23iXMAOmWRnN24gB00AJnABuZAOmtdMzVxgAbuciJrPMAb2Gc60TOuQAADfRu5TIAE570HEAa6AShOcAAAN71awgAMYNbrlALdaAA55AAADfXTOQAZzkFuRd2gAmWUAAFsgOmgAE5AAHTRFAc8rYssgAADpoABnmAAA3sDGN7CZyqlYgAOtAATnAAAXpQAAAElkki23QABOaAAGt0AAAAAJQAAAkSRqyS7oAAAAAAAAAAAAAAAAAAAA//xAA/EAACAgEDAwMCBAUDAwIGAgMCAwEEBQAREgYTIRAUMSIyICMzQBUkMDRBQkNQFlFgUmElJjVTcHFigDZEY//aAAgBAQABDAD/APoHO+3j5/8AMmnK1kcRylDe8oWbbf8A4Mrvh4yURx/otjdZxqht7YfxTMR8/wD4GrRK7Dlf4/EZiAyZfC2C0IMPt1Q+kWL/ABZCZLtKjQjAjAx8f/gu3vNc4GN5QHbSAf51W8WLEfgVJTfb5nbVv+5r+hEIjJFO0CQmMEM7jqZiImZ8Qpy2xMrneP8A8A3mtUAEsttDPIYL8avF10fgBHF5u39Lvg0F6XP7ZmqsbV1+jv0j1jh2RM//AICvjyrTOklyUBfjD++Z6V7BOY0ZjYfXIR+UM+lgZNBiMbzXGRQEF4nT/wBFmqX9sHpYdKVSyI3lR9xYntt/5obAWPI52H58x6zMRG8+IEhMeQzExemYrFtpW/aDed5vMaoBJZcdDO4xPo8eaTHVIoKsHo+JlDIj5oFM1o386vxM1i20iZlC5md51HjIT6U/DrA/gvx/LToJ3AZ9RGB+PGrH6DNU/wC2X6ZCf5fSx4rEfxEQjHIpiIiYmN48x+EiEY3KYiP/ACa0qWpkR+5cSICM/JjyAhidtY6ZgWBPyY8wIZ1jp/LMJ+TATGQKNxiIGIGPEZCN6++lTusJ9aH0y1WnuhK5OY31EwYRP+KSjUmRONpu/wBseq/6C/Rni+qfRaODmN339K9nvycQMj6X/wC2LSf0g9D8gUb7ax0lKZ5bzFqdq7NVY2rr9Mj+hGo+I/FkC2r7aQHbSAejWMm4tSymI9bMwywpE+R/8eYwFjyOdhEhIYIZ3j+j+jf/AOw6X+VeMZ8R6WA5oMdUy5Vg9a/03HjrIRvXnStpUG3xqzHJDI1UnlWXPpY/u6/4KijX3JKNvS9/bFpX6QekxExtPwIiAwIxtF2dqx6k4RWgtt9JMmLEyHjOQ/t9Rvwjb5x5sOWEZSXrZa4WqWr0tpNwgI/HpVjnZe38EIn3MvmfH4GGxlwFAUwPqZisJMviuw2hzMeMf+KWl9xBx/miXKsPpMxEbz4jfeNxnVCSE2qP5/DfHYAbHyJQQwUfF6JHtviPIlBjBD8elCdhYv1idsjOmrFoSBfADAAIR8aONwKNUP7YfSz/AHVf8V2P5Y9J/RX+DIf2+h+2PS9/bFoJ/JEtY4dkkX4yniMlqhGyOU/P4rb2K4CuIkpnaN51SHlzfPz6u/PbCI+yIiI2j41ad2VSUfcgWCoYZPI//Eq35NhqPiNMCGAQT8Y8pgTSXyX5V8S/x+FoQxZBOqBySOE/Ll91RL320hXaUK9+Xqr6LzR/x6dlcM7vH6/RVkjsmmRiI1Q/SIfSz4s1/WXLFgqmfr9Ln9szSP0V+gOMrZq8cNZGfywH/Ppe/ti0wuNOZ1RHasHqRQIyRTtAGJjyCdx9LZca5zqsPBCx9DKAAin4qONy5M4iPwR+femf9F45FEjHysIBYhGpmIjefEacyVrmRjckq7QbT5IbAE6UjEzJFxGSnedFPubYjH6f/iDGgqIk52j0uDISFkI+oSgxgo+NCoBYTI+6+EyqGD9yzgwE49BYB78CgvSZiJiJnadL2XeYH+PR74QEHIzMAUGMHHwfi+H4hHjkS9KMzyePpcnixBepVxl4v3+rQMYVxgb/AJd3+2PSP0V+lOJJ7zn0u/U1Aamdo3n4W1bY5BPKMhP8vMau/TUmNIjZC41abKUyY/cgiNIEf3XS41j1UDhXCPW99XbTGrRyuuUjPGa8nKAk53Jy5aolxO0qCFrEI+PSy3spI/8ANNXbTEz9zEwxgHM+NOTDhgSmYj0tO7KZKPuqJ7St5++y7sqk/wDVVT2lfV9/9EWAUyIlEz/4A9woDmUTMAYsCDGdx9SYATEEURNlXdSQR9y5KQHlGxaIYIZEvimUxBoL7vQxgwIZ+KBT2iXPzqsPZuMVHgdXFGTUkMTPpdjgxLo9bg8qxxqoXKuE6seLaJ/Ez6b65/xqr9NmwGnWIUaw48pyEfkQX+YneInTWsG4tcT9HoCRBhsifN3+2PSP0F6mdo31jvKjL0P8zIDH+HTsk51jh2RvrITMisdZGf5fbQRxAY05IuDgW8REREREfGQmS7aY+YiBiIj49P1L8/8AZ6IeIjMzEfHiPW1YNRAAREzq3+a9SI+PWZiNt529f7i3/wB1amfc24j/AGtWm9pJFHgq/c7Idydz9bdiUjEB5YHPhHPaT1YdCVSc/NNMrXJn9/8AzDXLTEEydoiYmImPj8RCJRIlG8VwYkzVtur1vK7ieUfchkNUJx6FYWLRTO/LT/ybS3R9uQEpRyGZjSi5KAvSPyr8x/p1cGQNdmNRMTETHx6Xg5Vi/wC9c+aAKflrRUuWFvMRIsDf5GhOwmmfuuzxJB+jrPaaC+O/pfGSYmImY9LfhqD9A8ZA41YQTTUQzGrkb1j0md0hOm/36fwXv7Y9I/RXps7KOdY+I9vHoquYWGOIonVqdq7NVI2rL1c8urjq+LDlYCMyPqz68gsf8ekztG86oxJQx0/PpZYcGtK52I5kQIojeaQky1zPzOqv5r22P8eppYyyJlP5WrTu0qZj76yu0mBn7tQMRvtER6P3sWhRH2OaKVyZaApIIKY4zqZiImZnaK8TYeVkvt9B/m7HL/Z/5lyhcuQLVI54Sk/v/qI/IsGifs1YRDojzxLVpfcQQx5lJRYq7T81FsUmAZ86vjPbFo/cBwYCcfDAhiyCfikcyuVl9z4f31EveQ0yOSzHWPPkjj/mwHNJjqgfKvEf5FSxMjGNiyH6ETqJ3jfWQjYVs/z86YoGSMl86yEfkiXoXjIB6Wv7dmq07116atk3FMEdx0yz23gnjM+l3+2PVf8AQXpv6Z7/ABj/AO31JjBQMzHLVz+2PVf9BerU/wA3Xj8NeOdxzPW2zt1zn/NUOCAH/MztG86qmbBNhT9Koll5hl6UY5S1s6uM7aC2+ay+0gAnxPqJCW/GYnUztG8/Comy/vTH5XrYdCVSf+aSZBfcL73KNrlf/a9LZkZDVX9ywFYQA/GrjpEYUvyxCoSqAj5/5p6D7kPROzB3kYko2n8fZKLPeGfp1MxHzPpcXJLhgfqJbDViyPwV/wAmyxE/FtxJVzDbcZ3GJ0YwYEE/FA5lUrL50X5N0Sj7fWrELtuVHx86x/0k5f8AnV6N6xaVO6gnV0eVY9JLkkJ9b0b1i0E7gM6dPG8mfSxG6GRqnO9YPV8b3k+lz+2Zqv8AoL0/ylmsfP8ALRp8fz6Zj0vf2xaRGyVxpv1X1R6XHGs1CE7TopgYmZ+MfG6zOfnIScypYTqN4iIn5vTy7So+dXD4Vy/7pDtqAPSwztpM/wDNMOFcIn5fu22tMfbdaQLgAmYNQECxEp5FZcYSta/vtulSth++uqEqgP8ANphMKKyvuWArCAHxGpMYKBmYgtMn3VqFR5VprIUsjnVQnGrm2d9OaKVyc6pqKIJ7P1NPeCA5T5msk+Uvd5b/AOAcoidpmN/w2kS0IkZ2YElIDJRsWl/y1mVT4VqbG1mEFG0aubrNViNNULlyBfCglaxAp5TpZdm8YF4jV0OSJKPuUfcWJ6mN421RkgcxJz5d+XeWf+NL3XkDH/GrUb12RqpO9dc6dG6WRqiXKsPrajeuyNVp3QudXY2Yg/Q45AQ6x8719vV23vk/99XZ2rHrz7DxO0omSpRv5nH/ANvp/wDeon0yH9tOl+FhGl/XkDn0bEsyAD/jVouNdk6pDxrBqfzMh/7ad9V5I+luebkK9bk9xi60a+I/9qe7GtfPxKVk2GzG5TMREzPiK273FZKPpNQGQkUby5opXJzqoohGWs/UtNIeKVfqjEKVEEW8VhJzCtM1Zb2kkcfNJXbTEl9+nzNiwNePsiIiNo8QxHdaJHO69NZC1kc/FdRtP3L/AJ/58HNG1Km/bq6iWB3Q35obDlCcfOp32nbzNWxLoKDjiz8FtUsVMj99dveUJ/5ujMQDx+4Sghgh8xYX3EmH+ajO4gZn513l93s7/WxEMatu+06mImJifilMgTK8+nAOfPjHLIBJJg4+Un3FCerk9uylvo2N1nGqE71h1PmNtY+dhYufRMlF9ozO8HHICHVCd68RrI+FhOoneN9T8aqINAmJzE6IxGYgp2nVj6baC9L39seq+0117+Yb9KT2jWP/ALeNWY/mq86IhGNynaMjP5ERoY2GI1Wje6+fRf1ZE59L87Vp0oeKgHVTcrDz9F/XfZP+ND+ZkJn/AB6J/NuMb/pts7aCmPmsvtIAJ+dXDk5GsH3LAVhAD8ab/MWhV8rYYrCTLxFUJKZssj67CicHCC4wIwIwI+IZPu7MLH9LT2wlUnPzTTILkz/U/D8eZ0mwxzy4RHY/4OZmNto3/ZbxvMb+fUrAC6EzvBetpMtXuP6ldsOUJ/50P8rYkfhPo+Pb2BsD9kefMamYiYiZ9HWBSQCUTtpH5No0/AkMEMiXmKhSEnWKfOq26rLET8atbqtKf/p9bH5Npbv9PowIMCCfjHlMCaS+XIW4YE41EbRER8TG8TGsdP5Mj/nVPaH2I39I+nJT6UfHdXqwmHrkN9tKGVqESneUWAfBSMTEauzsSJj51d8EgvS7t7Y99V/0F6s/27NUI/lh1c8NrzqYgo2mN4yHmFD6UfLXlqZ2jfVCJM2un51kPKgHUzsMzEb6x4nEMIxkZ1QjlDHT86pfU17Pn0acLWRzqiEiiCn5b/MWxVH2aawVLky+KYSXKyf3ae2EqI51TVK1ci++yiXwI8th9HCRqIQnYqyIQqA+S1P81a2+U/isNJx+2T8qWKggB+P+SCyBOJPmC9LIElsWl7zAGJjBjO8elxMmuDD9RDYcoTj8Bfy1jl/taemHLkJ8aqukxlbPDZmBjeZ2hgCwJAvioZDJV2fddWRLhgffXdDlQcfNlPeVI/6qje6mN/vvDIwDx+RKDGCH4txKyCyPzEwUQUeYuR2zXZj5iYmN4+LCYeuQnxKgIFiBTymZiI3mdoiYKNxneLa+4goj5qt7qRL/ADeYxahIJmNRO8RMfHZYu73Fx+X61PosPV6LritxtiZ3076b6i9EzwvNCfSfMTrG/olHpYSxpq47cNX/AAC59L07Vi0iNkrjViYhDJnVD+2HV75T6WpgrSF+lDz3Z0zlKygfJUlmtHE42nV36nID1YJEBCM7TXV2VQvfeSmBGSn4x8T2SKfS+c8BUP3CMCMDHwiuS2tYUxPpennKkROhiBiBjxGmfzNuF/KvxW3SsIAPLK6YSqAj50y2IuFIRzKZiImZ8RWcbpYW35emvNx9ivpKQSHENS1cMhW/16X3mWCMtwX/AMhZQRyLVeG//v0mIKJGY3irMpYdYp3/AATHtX8o8J1YYalSYDylLIasWRpqxauQL4qtKYJDPDajjOCW2d22gICiyr7ihdpG0T9NSXQMraO02wKONhf3gYsCDH4VPt7ZKn7NM/lrMN/2mBDAIJ+KYOWEg2NoMIMCCfikc8SSf3OXDVkE6oskk8C+70eHNJhHzji3Rt6VvybTU/6bKpckgj5SJCoBL7vS1Yclocf09R9OQn0IoGJIp2hbAYMGE7jd+lyD9O0vud3j9ZtWExBlEToFguJgI4/gyH6QelhIuDiUzEDZVzhQbnNvxWZqpG1Zerv3oj0OtBWBfymJZJCBSMcioqNapk42n1b9WQVH4XiRJMQ8lWCQQAlG06j8+9//AA9VTFi4TY+zTmwpRMnVJcgrkX3zMDEzPiKzWvaTN5hPqRQIyRTtFYZc0rRx41afKxgF+W164pD/ALnc7xyKVxPFYCsIAfiy05KK6f1EpBIQA6tP7K948nVRK4ljPLfSSiPmYj/lJUBMFkx9XpyGCgZmOTFiwJAviqwhma7fvmImNp8wtQKHiEbRq2so42F/euFM2eMRy0H8rY7X+zqY38T8JmUWCRP6dxEtXuEfmVnQ5UF/qcoWrkC1TaRDKT/U9Hfk2wb8BqdkXt/gPWt+XZcn0uiQvU4YmZ/BeDlXLSD5pAtWZ4W0M/xMxETM+Igl2UlwncaS2LVIMjackM9oSjQTuMT6X4iTT+LIx+RE6GdxidEIlGxRvERERtEbRaAjQYjG5VAMK4Cfib0/mojTWgoeZztETvG8fgh4y+URHnQblkTn8TGCsJMvthwkmXD9uPiZWbJ+SIR25Tt6XH9pUxH31U9lMD/q0/8AmLIoj7NWzJhDVX9y1ioIAfiw+EjG0cjHeRiSjYtPKbDYrBP02HxXWILiJOWdtPcbtE1VkZTZb93pYfCQ/wC51kSuJM/LdEAFtyiJ9WMBYyZztArO4zus+lX/ACNiWIdFiJmVDMFEFE7x63EEWz1+GJbDlwcaspI4hi/DUPhwb/BacwlqIxHlKmC5cHHwn8h5In7NWEw5Uh/mq6TGVs8NYwVhJnOwvCLCYNU/VXdDlwXwTYmq7vj+lEwURMTvFkZS0bI/ETExEx5i93BAWgUxpwRZrfT81W91IlP3Xw3Rzj5Q3uqE/wDNxppVyD5AoIYKPiz+VZU//T68ogoH/Po0eajCPmkLATwZHGchGywPXgw/9sd4Bgz86uDBVjjVQuVcJ0bVrkYMtpciGyEzO34sj/b6V+kGgcBmSxn6vViVsISON5sIF4cJnbQxtER8/gqRytuZ6VfqtPOPjVd/fgp22jRub7wUhMcNWlG5cAExEXCFNXgPjVZfbQAz8iphWpaf2amssn98vM6nfaePzVrykZk/LGMFYEZfFMCmCsH9/wAedVv5h52C+3Vp/ZX9P6lVHZX58n7cfcd+Z3mf5t23+x6OcCQkz1XUZn7l33fga0FDyOdoBbLRw10bK+PEaKyAtFMbkX/FsY1VkZKd0+nx8+hDBDIlG8JKa7fbnP0arg1fMD8j6R/Ku2/2NWAJLPcq0BiwYMZ3GY38T8Kj21iU/wC3bTLF8g/UrOhyoP4nU1o9zD4LbRCJjIlG8VZlLSrHqf5a3v8A7ZgJjIFG41kEgSGS5CYQYSE/FMiHlXP7mBDAIC+FhCwEI8wn8m4avgSESGRKN4ERCOIxEQwBYEgcbiIiAwIxtF0YKse+qjYagZ/z6Obee9982fnV3C9C3DO4wYSXGCiS9LgSdc4GN5qyU1wk/mrsFp6/Qx5gQ6xxflEP+cjGwLLUTvG/rk8gvHVCsH9RdO5i7fsuVZISHRgDBkTjcYiBiBjxFTzZsTq2UjXOYnaaszNdcz86tXalMYOy2FxXsotLhtdkMDQMWyN1lBxqZ2iZ1jx/KJk/MztG+sdG4sZpvPtFwjcqSiWjYo2LVOJY5r59TWDNuY8v6FuZaxdYdRERERHxoFguNgiBiZiImZ8QjezYl8/Zq00t4Qr9RShUuAH40RCAyRTtCQKyz3DY+jUzAxMzO0A1tlsSqeCdOcCQky0pJvOH2PjToZKyhU7HWrwkfPln/Gw1i7crZO4GAmMgXwgirN9uydw09IuDjPiaziLdLfDdXO6HF65nZZwwBMfhqxaEgXxWYUTKG/qTETG0/AT7R8qKdk6tK7qp4/fXbDlQX+VVpU82CX0etpUkMMDwyeNyt4+6o6Wq+r7/AEtRKmBZGNCUEMFHmJIYmImYibwFEC8PuWwWAJj8esxExtPwICEcQiBjT3BXSx7PAYlyr+RtqfELitmH46pbxrZ2dVr2qmQx7ef5v4IUuGS2I+v0ozIvcufnIRvXmdK/SD/OpmIjefhNiu+JlDQbFpsZjJtM95x/SbE+9fzLZ/qmsCTMxmZlqgaEgfwIwIwMeI11bA+8qSzft2sfYxEjk8U2SqsyOXzcsXX2RXrMdTugOHM7LMBlbGQBwWRgXOiZUcR80x41wjVguKGTqiPGsPq4+2oz1RHjWHf518encDn2+Uc9U5kya6dEUCMkXiK7pcHOR4xqZgYmZnaKkSxjLJaY0FDyOdo9LbCMoqr+5axWEAPw5opXJlqsoo3c39W08lxAL8tUErXAkUlLJ903tDP5MRERtHiNOULlyspmIWsVhABG0OcCQ5nOkKNx+4fH4ZmIjefELatkTIFyj9nH7WCGd9pifWyiHL2+DrOlgyJ+GvSLg4l4lYkIQJlyLVtcxxsL+9TBasTH4mImNp8whEIghGZkdWESyIIJ4tQ+GjO8cTchbhgTjURERER8an+Ws7/CdXYZKeS5mJSyGKE/VVeFMMxmeLf5a1Dv9ufMeNVDaDjrumZIxgxkC8xTKR51zn6r4FEA8fkZFq4n5GmUqaysXnUzERvPiPRFnusNZDxL06gW1mJsQrfdKq9VeMyI766ixD32gt01k2GKAup6leI+nTHCswAvn8O0b77eb8TNYttV53QudZciHGWiGdprWn0z7tc+JVadn/pvjUCWO6fqkjPEmZgp/Hl8aGSqSqfDbV+0rGxiLAEtjW3gxwBCPb1Kl0krGhhVydnp2w2peLHvr7P9MiWyIHSRgVAMeuQPZMLj7gGBARj41fkpWKhjeVB21iHzpKiK2xxDIxoAEI4hG0GAmMgUbiACAwAxsOrplwFIfesIWAgPwcd+7AT5DTGQtZGXxSXMxNhnk9R/NWOXynVdZscdhsbTZaRFFZX6iVClcAPpLVwyFTP16fYBI7z5JVczPvWfJEUDEkU7REsuMnaZCuxoJDkc+BmCGCj4+PM6YZ3DlSp2StYLCACNo0gGjyJxbl+/87/+34mAVR3fCN1RMFETE7x6WQICiyqPqWYsCDHzHp8+J0veq+Vl+j6iYlvxmJ1ZWQFFlUfUtgsCDGd41ZsdiBmRkoasbCeOqjZYEif6mkbosmj/AEakwEoEpiC05QuXIFqk0pgkM++6ueMOD70s7qhZttq0MqaFoY319DV+PIoV2VQvflriPLltHJ6+6og1TZJp2L79HEJvCf8Ap1ScxsM5zvLAhizXPiKlVKL7cHf3ZXUoEqBS42CkgGdUWzdM93V6J4rkY3n8VoTKuYhG5VBIa4CcbFcTL6j0D92GxSskt1dpSpjWV8NjPH6fStUu27IN3lmnPShctccLW7qrFrmYDuN0fWFWJnhXYUU+pMbaMV8pSz1z6qnsDsWK8Pms+bfT0lWrxJY21brz7CouE3cXiFY+CYRS616WK4vgYkpjURtG0atHPdQsZ2nTvzbq17/ToGm29MAU9v0IwCORlAxEwUQQzvH4Ffn3CZ/p1AjEyURG+nz7h41x+yIiI2jxFtpREJX+olQqXAD6WX9oYgY5Mro7QzJTyZqzYhAePJ1UEO7m+WvcKVyRfNdBSXff5ZMwMSUztH13T2jca88EqnaNgQs7B99/2+Ij/wBjMrZ9pXhS1isYAI2H/iSGCiRKN4TM12+3Ofy9DYmLBJZHH0j+Vdt/sabDJWUL8HTsk2JWz9R6RcuQn5qOIolLP1NfPjUjNJ0GO8piYmN48wUTTbzj9CJiYiYneGLFgSBfFPurkkHE7WImu4bIx9MTExEx5jaN9/8AOriYYvnHg0GTEiRRsWnqaNkHpHf1MYMZAvMUmEBnWLz+Bcdu4wf8avL5oko+6u2GqE/8oWarbYgZ7eup6ZQKsnXjZtC2N2mqyPjV+RpdSVbO/EPSzar1Vy2wyFha6vSO41Ekcq6izbpJykixVbq4eXG7XleqmQp3R5Vmif4GsBKjac7B05K1UbOQeUBEBb6ku85mV49awWArWMCGrQzmM+VN7JXXp/w077MdWogSrIRl7x0KC1Jq38Lhjcuus5rXMblbmJsxj8nvCYmJjePMa6hZ28RYnfacGvt4mqO204oBu9RWrRTyH8MrAjhkx9eqMcza+flhcFkWseviqWT8+nUK22s1XpmyQVhnPxuSZh7BSYXMhUohB2WQGqtpFtMPrnzXp0GSihfg6yYSqB/1TMDEzM7QJCUQQzExZdCVSX+qomVK3L9RzRUuTL4qqKd7DfLNOcKVyZarJKSmw79TT3gkORfNZJsObL4+pzgSHM50hRuP3D49DIrjO0vwkREBgRjYdTMDEzM7RJMulIh9FdawWMAEbQ5y0jyOdJZ3Vwe0j6NaChkjnaK7jdEmQcQ/4Z6RcuQnxNZxFupnhrFLbx5xv6NWLQkC+KzCEprt+/TKwG0WxuJ6tqIZiyr70tFy4MfQhEhkSjeFkVVkJOd0kImMiUbikyrN9uydw9DAWBIFG41DJZFWP7tNI67xZJTKvwpeDoKR9No+fxTETG0/FOWLexHGe36PSD0sSf2dOmdLIWsU2d9dWII6K3jHmhZG3TTYHVmymognvLiunVs563767EjSy+DVbVDK0Qm1XB7YK9RHs5PH2aObrwb0gbsj0+6kRX8WwhnC5YMnXkpiAf6dUWpTj4QE/Xl+4unQwqI3bSqhTqqrB8aMpECKPM4CJdFzKt/MtdKzvStnvPd6SExr2jMZ36ZkLVi5cbG9nq+rutNsY84e17vGodP3a6rKBxcRPzU+iknfxHSUCRXHbefxFEyMxHzTUak8TjYr58USP+UB20gGu8HehPnlrqqP52jI7CfU0WKt6rkUxrILaLJ9+RtyGDolRxwKZ4b63ZNkhXCNLAVhAD8Ss32tziYVMxEbz4gd7j+c/oamYiJmfEJGbTZez9PTGCsJMp2hAFaZ7hsfQ1oKCTP4Uo7Bw98fTpjDtMlKZ2UtYLCACNo0RCAyRTsP13T/AMjXEREYEY2ixYFMbfcaa5GXesfUemkYrkgHkSqpEXdszzNrlpHkc7arssNZLCjin/g0WIaRhMcS9LCSmYcr9VLRcEGPrYT3B5B4bXfDh8+Gesfylnb4T6NULQkC+ENNZ+2d82Uw5Uj/AKqZOJcw4ZiSsALxTPidXVzHGwH3rMWAJj8OVDVkE6qMk18C8Hac1MgQxEriYKIKPMei661MJg77+jWglRuZPEF5BpDOcuMMK+Pt5BolZvgutXGzWPwDgKfwVZJbWIOeU66kAqd2rlExMSyE5CgUD9Suk3TNNtY/vyhzmMurHIKZQAAsBWEcQ1aGKvUtZq5gIa+pj+oQsIcPtpz+Hidpsjq5fRTy03MRIkNC6q/VCyrxGnMjIdQz3J/lMIs8nlHZZ07D6ZW7NCiyzAdyelqTorusmXFKukhHfnbPYenGqEuxkHgWBstqZKxjrccrHVA74g53210ocli9p9Or95rVgjV0uzjnlvxnpEIjHsP/AD+D5+PwOrrdxkt4mZiImZ+Kc91rbE+nVcmp9KzAcgquG5UTYINoxagu569aaEGOnOFXHeJmfwvVLVyvlx0tYqCAH49IiIiIiNoMhAZIp2EIO6fM9xRERERERtEJY90m+Nl6e0nFNdGlKBIQAR49IW62yZbEgqIgYgRjaH2ZguymObUVoXMsOebfV1yBLtpjuNVUIi7tmeZ+kOGXSmImZ0xgLGTOdorPJ/IpHiP7zkMFAzMctdlcN7sR9ejate3MoH0aM12d9cfQJQQwQzvFhjUlDY+pIkJjBDO8WVksosq+5bBaEGHxqHrlsp3+t6RcuQLVRxxM12+GelhEODj8FWfJxK2eG6uI7i+Y/qIOWJEyjaZiJiYnzFaZQ86xT9OrESh0WRj6CgWrmPkay2KVAMmJn8PVDpViiGJ2k5vZga9ekgoQvpa2/id63MlPRyNp2snuzDZvG/nUrEuHD9QrulFa1EJteluYVYS2PHpeqLu1WVmfHTNiVi7GPni8rg4XL344fT0vR7NQrjInvW8vbJhV8ZVN5ljM/agjuXorgnpei4YadplgTwOCppl1gJ7asJhSEWrriQ9PCorGRqGsYLAWGY7IuxdkoEcxkBx9I277NeptHHLqjEzcxtIaNJVeNuXp1ZYkhr0F7yxrnY1FGnXV3CMwWBMOeIYvKDkgawFEtd/uL6qqkERM+9s5PI3a/Du1Rxmcq0GPgzQnDZCcjSF5DxZ1dMQqp52nM5Gq7DPKs8WT08mU4lETEROsjmKeOkRfMyxeWzmWaa6EClf/AE7mHD/MXvOSxl/EJWxdozXieqYnZGR+YmJjePMekxExtPwpS1RxXG0a6sYQ40Qgd4Q8KeCW+fEdMVu1jYcX36ifcXN4ndfqbVgQgU7T6FZjvikB5F6fHmdGRXG9sJ2QACAwIxsNh/aGIGOTFQyFxDZ5G5xuP26J0pIJDgEaIhAZIp2gGAweQTyj0ZYNpdmt8oQCR2HyVmzCo4D9TawuFf5xbkZisZI52iTfb+lX5aU11pjYI8kQjEkU7Qm0DjIQidtbRp7wSHIvlaG2C7tnwERERtEbR+5sgYzFhU/WpotXBj8atoOSh6p/Mrvhy+UeJ9HoB4cS+ajCGZrN8HMRMTExvCymq3sn+lMRMbTG8RM02cZ819FE02cxjdAlBRBDO8XFkBjZXG5LYLQgx+CrATxfvMFozEBkjnYYmJiJid4tJKJiwrwxDhcuDH8DqwtMDmZGdGImMgXmKhysyqs+70zPUAY5goUENclsOStseI1duJo1jsun6atS11Db97b/AC6aUJrrhSQha/S9kqdBcMsHtrJroZQTuYuZ91gMr7+twbMRZ0a1s25jBeuaXFDI1cuEbDaIeoMuCExtUERAYAY2HG5UL9i0sIiAsJixXYgp4xRqDSqLqiUlDXY3Jk3GsLkaSXIyC4kQYS8V1PLDLgjK4qrl0+4rGHuF25/iCBz3OIornMZtmR3mavoxgqWbC+3Fc8vmGZJsTCbnVDbE+2qDFbWJQj2JV5tReirVr00wiuHBfUHukZercUmWxXq5lIy5FcVXLxZXH1WzetA9fTYdjDgbPojL2v4zYFVaNq0UZv24r45W8Ymg6hVhLXy6bVlNRB2HlxXWGeocyTmjMVbGCxhO2oWYq24/6npztMLvLPI/xGu7HvrMrWcRi6+VxjQmIC109fsV7Z4m6U/i6qnbEzG0TrKskOmV8dUFdijXVPyUTIzETtNZHYXx33L1Gv8Any8y5T6CsBMjiNi9LYvZspcfQpYqCAGPGu0Hc7u312AYapFZcSrIhC+P+rXFt0vqiVoABAYEI2EigYkinaCNtuZFf0IWoFDxCNoZz4T29pNFaFzJnPNrrYLLthHcYFU2FDLU8piIiNo8Q1q1DyOdoFbbc83bgkREI4jERHomqUsl1ieR/vGQVRndCN0xMFETHxpwlXb7hcbgJCYwQzvBlIhJDHKa7xeHKPE2Ud2IIJ2bXf3RmCjZjlC5cgWqziifbu8MYsWhIHG8V2Gs/bOnyQiYyJRvCSKq327P05jfxOv7N3j9DW8fH+WLFoSBxvFcXIZ2CiSVpm9J/MfKRKCGCj4cxlezBlMyn5/BZrmwwYqeJ6yNwaVNtgp83cbNfBnbtbnexq+1j6y5+dZ/e7lamM5SK0pWhQpUMCuZiI3mdoizWk5XDQljuqscs+ChY7WAVTu5K0dsBI7wV8d1BWKltGszXbiMgGTpxsurYXarrsL34ejXKSEsccLC5btZ9vsqAyNPH49GPrwhMayVW3aIVi6E0ujw82mxH0zMRG8ztGTDIkoDxxiLatWMe1mXy7xizhshayHuLDBgKtZQZjqB1iRg6mZsWcKpI44FqrLx+XzoLsWngNehRVQqjWVvI+lpROquUP3YPIOp1bdRa5O3SyWGyEEWTSlVpJdN445tpYsSqZuL9kV065mixZTVSTnnALNubyZTZoTNRCsFbafvM5Y/Ku3jype2qxKsc6DaxeLoR9eOxyMdXhKY+rXU2WG0yKdct0pjJY7EzaW0FouYhVTEV8gDC9wfVKV0FGMQ25iOoYyD/auV229KjwbfXrqejsscmjcX4y8F+muwP3fg6qmIxW066gKYwdQNttDGwxHpavVKfD3LYVosnjhjebStoy2MmdotK0F6mY8geshEwL7SifxMatQ8mmIDbyNSnW9y04leQz+WaAuUM1KuLOyePQdvy/1//WjMVjJnO0QLLkwRxIIEYGIEY2j1WhSpmQHadNtfV2kR3GLq/V3Xz3GasWe1MLAeba63xMm49y9GMBY8jKBgDhgQY77fvZiJiYnzATNNnAv0NRWkLPeXPEdWElBRYT+olwOCDHVlHdGCDw2tYhw7F4ZaR3g+nwys/vB58MekXrkJ+abjmSQ2J5sWLAkC+KzSUc1nT5tpOZh6pnmlsOULI9SESjYoiY01QNCQON4WsVhADvt+Gwz+K5tdQZiavVZ7Y0VxG5LgUVxgvpFnVJkTJp1Ccnp2PfWbWUeO7dQl+dy9lD7BArMYCvjqfuUOOTw1ZFfFpIYBTiqPZlvaudEuxfT9egcuYXuH5SsNqg9Jba6SuSaGUiid/TMYgMosBlkqPI4dmLpSZ3SmOn1Wl40JtGRmUchkfjVG1YwGRZXeO6rNetkakqOYNP8ABs9XjtVL26VdNOe4XZS1Nic5k1gqMRjo3dh8dGOpCmfLbVVNtB13RyWluR6cbCrA96hWt17aobXOGB65rC2gtfxPG/qTkqNiZ/iNKJcmz02ncprPaVXJX2wBgpWNx9jK0rQSCBh+mfxgSkk3CKGBlLhgu6czXt2QqKhSYjudP4oqSJe/za1mso5rYxWNnm/N0E4zGV6wbS1lSg3CITZOFJX09k2yutZsxNGuuhj+oDCxErQy461npu4xM2I6Snmy6yRgZcoXKNRxuPSr+2VnHH934OqwksVvGs1Pe6crNjVdkNQtkfGs7WPIZ1NMC20GORQbK8wlsBFLpNsDA2ZCR6dwbo3TbKYZ0nVmYmtaMJtLy+GmHhdh4YnJRkqcPiIBkb7efn0vSebyFkROYRj4W6RK8c+xx1BuYs/xO8PGr6NctI8jnVc3M3NgwAMYChkznaAWdo4a6OKvRltYHCxiTP0NgLHkc8YknWvAbrQpK0jxCNvXaN9/86iYmN4neNBVjl3HT3Gfv2LFgSBxvFcyWc1mzvPq4Crn7hX2iQmMEM7wysUvFyy4TqwMobFkPgSgxgh8xtG+/wDnVuv3Q5DH5leWkqJbGxwIjGwxERot+M7fNVptVyZGxamYj5nb1a1aVk1pQC6fU1S1cirCyCNZS0dOg6yERJ9JqkabXGEwedmHZLG1N99NDuKNe+2sS5tH3mIORXZxWVtotDi8mvg2JiZmInzkunzba99j2+3sDg8tfYM5azunIYapfQtJ7r1S6dx9FvuJkmHjstN+9aSuBmvlH8FQoZ+rp4+xnGqLx65DK08cG7z3ZVrPy1kMnk5FdRGToWLE1UOFjdZ2pj7FeJuMhJ1r2YxU8BjvVk9S23R9GOYU5S71ENQWWYGqllRlAfdVWF38VkQyNQXxsLMg5qKL3JjdkdVE5Eqt04bCFXm2GNxaWqGlFmKiotzE2NMYCgJjCgQ97ks2+V0imrRv4tZV4kY7hvrW6yycLRmKaItLGzaI3MiIiNo8Q+wquME2doh6pV3oKJXgKRXrx5J4z2tdQZhuOWC0D+b0/fw9NUk9kjb6kv0L8KOqyTYC0ZzCAoSiGPGxWMkWpYD62VB1kZzHCymnarPxlo8fXmuHSC9qLmbej4ih1SowiAX+DqEOeHsakfedJxA/OCfD8VXKPnSrXPP2ridpEcoJjIvVBDNTp+zP1oAJLpbFumZSbAj/AKQRE+LRxrI4cK15FBDCc+nTr0kQiuPEPRzVoWTWlAL6dUwslauIEoo51tZrhxWOUHLHU4o01VYLlPpNUCd3imSlzwSHI50pTLBQ6x4H0sd+YgExEaRXBA/T5LTrIqmAGObQrEwu7ZnkXiI/7QuwtjJANy9TMVjJnOw/n3J/ytC1isIAI2H98bADbmUD6NCWLIBLjNRrBIq7t+dpMmEGHhtd4vXBx4n0mImNp8wEzUb2yn8j0IYIZGY3hJFWd7c5mV6tNakRMIiRid4iY9DmRAiGN5q2IeEztsXrZT3l8YnYo32jf5Yxalk1hQIcH9Qu7jJlOKzGDrnTF+OXAOwuRjIUhZM/ndVO40ARG+6lipQKCNgO0hnVPNzBBcTExvHxl8OnJBBb9uweTzOLP2rxXZZhLTEwy2c95le0qwO4T5yIXDpsGicBYXmeoVBCDpkxsUuosrExbZ7ZFRePxFfsKLmebsvYBPVMjqh79TgzRJlyal2vcQFhEyQayXT9XI2IsGZrOelsdXEnWbBynpmp3brsgA9uvkMjWx6Za8vqWL7z/f3Z3J/6DNdP5GorELF7wXPUuToW6gJruhjAZDghm20YWuUZR7VkQRSy1DJqlfKAOsbenciSHxJURISGCGYkfTPPddtqw1WfPBeNx5ykImMNk5yVaXEELPqKp2arGB4XS/tE+mTjekzxvqlisrZqrFO0069dVVAITHFemKU0eLQExuVsPVSdmzXVAWLDslb7VVfbXVO109koiyO4FGJzCoiZW+F9OYdc79jlrM5KlXx76iGBDunkynEoifnXVMSu1QeH6msln+22KeOGLFvKzm4xXuHthDV5vtTj67FEc5FB2KD0L+/pdgOxRVy8zToXHm/GLsykyudSUFyuyj3IYu0hEGLS4ytqmRus4KNCRDO4zMSu/aD/AF8oxdtLcpZyt0xWtbFtCDUUGHpeNmaycY5MzFLLZP23DE4wP5rDYZeOVyPY7X4PawT5cyef4jexxyqtpNcEx4+o5mIiZn4k2XTkA3BC1gsYAI2iZiPmdvQhEo2KImPj/wDTLkyXbrj3DrqeJybmcp/due5DuRRyQJCYwQzvDVA0JA48V2EBzWb92rKO6MEE8W1390fMcWOGare+uN1iUEMEM7w0O4sg321UJoFNdozOmqFoSBfCGko/bOn6rTTUmTDbdTIYsTj4sIF65Gfupk6VyLomC/A0faWIcMflEYAEsIogG9R4hX+/zmrmsZbPtpfEn6WmHncjFFBT7B9Ku+pNKR4oOxkMBdGv3panpebDMhZeIcK3VgTFetYjfZTVuWLVlBBkem6y0XLncMz6eaTcRXkp3nPZP2FTiuf5nIYaKWMC1YMpt4holXlW/wBa2Go4MJ2IMqjjHMSgm5b/AAkNNt2G/ec7aZfpzLEsLx0pfASZQbMca5Dhc4dadxp+nVdm5yCqAFFapkb7q4U8TSlAZ7FRVrhZfYZYtAMAAiMbQ1cNUSymYhWJqr8nuyQrV1/YsY9McyKvUAx/pyfTlW6cvUXt3t6dzhh2CeLE4PH2sfWJVlsMn0XZmlknMczuFdydvIgOPqn3IwLzQiDD5y5Ls4WwY+YpgQ00SUbRq4EnVaI+Z6VPlioH1s2U1UE95cVui3nifc4l7LD5QMd3dqsus1rlTPmdO9W7T29JLEoKraJch03kYmYnIlAZrHVqaKuPqBu5SxUoFD9uupik8jQr/Gs3f9jj2MGdm9N0RRQGwY/zCb9e3bs441zyy1WrTcGVNnHWFdefRF17bng5mtnL1Q421fj+HdQV7v8As3bHtajrHzqgsMjiTuZCtFhlbpmxY5NZPshjB56oM9mRYLH5evE9+oURXyblc4fyZrGZLGIoHVvVyfrp5r/4qQ0YL2WsxkmAQ4+j9d57wwVMaFP83I4TDTT3t2p53dZLM08dHFxSTaeXoXIXCmj3fUmLEoEiiC1ZJ0BEJHclBCFzzOSkjZcmQXuCVrBQwARtHpEQMbRERDnrSO5z5gG2zg2xwVpr1Jjcy21+dbnzupC1LUPEI2jW8b7b+f3RDBRIlG8KSxDeIfUjVhPdGJGdm1390Zgo4s1YWQF7lP3gS3q3+RXLKtiEzuSvTkO8DvHKyiHBtHg67oeBJbH5iUikOA7zHrXW1T2DO8q9HLhqyXOiqwymVVk8hpVk4zJTVyqANV/pmjZCTqx7d2NzL6bv4dmNwZnbntMY1gT9fT9QKuMVtH16zEArPY95xHGBEY2GIiM0tDMZYh/2dK1boLmwxkxVzzxRirEz89LWUMx8Vgme7Yj3/U6k/crOG2++1ZgvyMliypgvK0ZiExmIJoRAbLmYiN5+AMGRyAoKDMFhJnPEUvU8ZJRcosjUXkNz3NFnHsbhE3lpBFh6hz2EW0fFjp7Ie8owthSVj16skibRXHx+G/MpdWuDoSExgxncfVpwCjOZ2jpaupuNbLgFg9P1UOZeuwEDrDT/ACxxp7zXSshH2YpA28HXiJ+shkZkSjaZjeJjXSDN61hPrkmnmsqGMRP8rl7Y1VJxONIV2FKhYxHyeaKxjs2vIIVyhGByWS5Wsg8kmnD9RpX7ULYgjG0JLqCV9wnrx+dRfuNqAsgnqLIjVpElbJC1ivc5DKVQayT11JQbcowSfJ1OqWV6q6xVubMBTuFasZS4HaPqB9CupTrSYsOWUmsCkeE56CoZSplA3gc9T97jD4eTvZT3XTQzJbuw6uzi6oay2Mdfu1CW6ADV9mUbmq1avzXWkAn5GJ106uu4bYmkCkFrXEwsYCMrl4rTFSpHevsIcFXlhz38vhcNKpjIX9zvaYwFLJhzsDHe5OxlbAwWlrZUUq6tkrs9OXTtVDFzDZY9F1YFpOYXM9MYCxkznaIBluYNm4IEYGIEY2j1ZaIi7VaOZpqQJdxs9xpFAxJFO0S9r5ka0bCqosJ5n+Yz0Zz4T29uaa8LmTOebfTaN9/8/tTasCETKIn1mYGNynaNWElyh6fDUOFwco8Tot6buUfoRMTETHmCKBGSLxC2raPJc7xbSwpFyv1AkpAZKOJW0kJRZT4NDweHIfmWBBwEzEF+PI46vkUSl0bTgL7INmKuFvYzWOVepMiQ5Pq4bOXa3Yacpr41Dq1FKHzBM11ebBOoQj9NJzLFNL2DwPqgyHEnETtqkAhTQARsL0pcolvCDV0wIc79sB4IxtiRq5XL7bGihMdKun/cwLRfiEb/AFau9PY+xXMEqFDToZ8Bmp2ZMSrX8KwDsL+h9xt7jVqLIijpNoCPZuSE5rDVcZWRASRsgB7fDb6OnZKrcu4sp8f/AEzqfYY2T65ye7na6/8AElAxuUxEMyFRfiWQUjZsuj+WpuZA0M874UFcR6eyZ/rXYDVnpgvbsmLbGn0ywjxQzzJheubfCMVZPfacKqa2DXJfPSsf/C3TrCz+W2NPHmhg66WvQFea5z9OSqco9wuPq+fEa6YZKslbrSMx6Z3IjRpEIzu+D/gOLhYxtkwoMjJUVtKSs6ydmalB9gZ4nhjtsx623C5NsvGvXY8/to23GtteruV7vTgsqwa8Q6We7x6RydwYdfxmKq48SJIzB66ivRSpbLLhZxhPjGoO2fJlNZ5zMndLxU1maUXse1Mff03dm1jRAy5M6gw5UnFYTERUx8iVKsYTuNuonKNK5irPbvYVuYljlZIfGRyVfHJFr95i1ZCvUZZn7elRhePdZaW0WMvcyDSqYUd4eVTp5MzEzYyeIw7pdGTyRdy1qZiI3mdo6lyQDVGmgxljJU5iqayjsQ8CZN5ozK+kZ5zdZttHq54JHcvlaWPKG2Y2HREIxJFO0Ltd5vBYzK2MBQ8jnaNnW/ndSFqWoeIDtDrKk+CncoS6zPOx9CxERiBGNo9CKBiSKdoiYmN4+NEQhG5TAxExMbx8fs5mBiZnxAmJjBDO46tVoeO8eGVHyYytnhvpYTD18JnbVZhqL2z/AJ09RAfuEx9ami0IMPghExkSjeFEVZvYZO65iJiYmN4Sple3IjG6vUFrXvwGB0+vLWLYM8S1lMknG1pcz6jO/nchucN9upWazVDb3I99ONylbJJ7iZ2O9kauPV3LB7axeUjJCwwSal6z9S6N+tkKKpM432jfxPrlctQrwVdjYiwjMqFEssTuK7tLqGq6nEkliFQhC0xMlFhUuQ1MTxmvas4yb2MYyO26IT0kod4iaCwnG11zG4dMHKjuY8vn0MAYMgYwQ9NrCuy9UmI7muqkQzFyz/VjX+5oV3T5m6RUepa9iI/L6sAxTWtBHmpYizWVYGNo9LK7t7P2vZcZKcVjKs88xd7zF5fC1p40aZM0ecyjPCKoJg7Wab99sVRKbp/qXnzqKjP82rE66ddNTJOx5eF+vUzjcdbFp/UJPGpKF66XdI4+4oo31ibKVwSTnicrYQTMARDhZnZ0f6VZ6khXC02JIcqbyIMLSI2Lq2sTmKZubyZMxEbzO0HZTey7sg0t6OBrHkrzctajlCt7XVjDj7ddSyTK1eoH3CMCMDHiMtTZdoMrqPgeIw38Lrm6Yhtwdzx93JtGJs5nuG7Di2efqURm+oNo81epbxQsMbX+qxjKI0KS68bcvSSThOoZgZ2rZSnF6i2v/q6bygqicXbmQbbxmTx147+M/NXPU1rbiOOZ3UY3IZm0NzJx2UdU3xhQ45P1NqdP5F6V17rOxUt5Wli1Rj8YEMsYjCGB+/yUy62RCIyRTEDe6kIme1xS5e7+B5XIcn5OzK9Krc1OcUyIGogADmY1WiXLI2THa6VR28Z3JjafR9qAntqjuNRWmC7r55t9GqFoSB/BuVWGFLjkaqxGUNszyIiEY3KdoJ7nlwrRsKaq1fVP1sMwWPI52ibbWnA1w3HTbS1zwjc2Qhr5g7M7D9Ix/iBO2RlK6w9wopSc87ByZREDERHx+zIYIZEvMRypN2nea/z6WUFMw5Ph1d8PDl8FoLcd4ksjhL0A8Np8Eh5QXYf4Zpm9R3cj9ESgoghneHpFy5AtVXFMyhvhujHmBBvtqsZodNds/T+BslkMxYa6ZNWn2Cac1qwwZlzxdhR02zL8b7TIZA2Zh094ABYwADAhrqPIWaNMCrTxNl6vVrLdbZC4WwGgLFlBB1FfsUaQnXnizG2StUEWD+/PY69NpttSFElQQKRCRgYymPLGkjLURgZq2V2q67Cp3DWSHuXss+PMZQP/AJXpzG+2NLlj6pfOuU0OqZk94X6nM0uphLb8vWRrxZovTMb66TtKmkVaSiG9WPWDanGf5gciOcxd1RK7bOlLMNx8okpk/SsNqy+yxTiSpVCqraYCCKIiPEfH4L5HWsV7yvBLYLVg0J3D0x8Ff6kfan6lRkac3PYwyJsdNkQZe6nbYWY2te6jsVijgmekqMfY5w6DpHHxO5MaWkdP4lEwQogyAAAYEBgR6oHiNKz/AJy5NHGWZTG7E2XHSnG11cjprjG4kIZG09KrYyLV9vk9XIh/UNJUz49HFwSw9LgY6WbJfOUKSbhJmPOszcKljmuD78S1lKlzEuGunknfvOytmOXqRCAyZzAjSrznsq288f5TWYwIZAoekoTZB/UuKmVmubKj6oycfTNOBIsl1HkI41a8oVSxNTED/EMk6DezIZPNESMaEoqY3C08eETAwx+s5afeuBhqZbaxeMTja8KCeZ6JKSAgIBkLuAqunmsI3sV317jaYxxKoiK1VVePPqtKlzMgO05PL1cYI93c2U7S7lZdlUTAasvJewLHkytWhUcz+pr7AJj6vJChtiYOz9IxAiO0bQJ2pMu3XHmQVN552C7htcpI/XO2t7Nn4/JUlCkx9EeWXBieColrIrOdPKyX0gAhHEIgY/bMAWBIF8LCFhARvMej1Gs/cI+5LgcEGGnV1u25/OnoBw7T4JDzg+w/wxiwYEgcbiACAwAxsOrSJOIavw2tZF8bfDNWq0PDx4NImC4Fhci9VKKvk79U5+qy43s9pXnae4QlFDGLljcNgF0trFn8y3ken6N2DYI9qzi8nYo2pxOTLzrqOv38S7aNyNDMz08jtxBWMRWbUx6a7p/NtVUW0kiwEGtKVoUCVRxXkse4LU3fcmS1jyYI6YsGrJTI3Dp5pVrNrEMnfWSvLoVDsM+aK1vwWSbJwdmEHkemFLVG7OlsiZAWOfMQfVaN6qbYzsxDYchbh+PTqZRQhN1X68dV0IrgZiXef1Jkbm66Ku1A02V7lSOX5nUuMvOvw9Ciauk1eCyj12Jkx6SrsBdiyQyC9XHQio506xEbVZn8d1PfrGEfd0vkAfTioU/nacRCkyH7unLK6uOvW2fdSx2WcA5apPJ3T+Ht0mus25iGYqZb1FkGzO+rNlFVJPeXBc9R4eJ29xpnUuMiNkkb2JMmKBhhKy6nVLMSZRq8829PG9c7kkKlhdGpRAvfdS2ZRijGPuxVeK2OrpiNp0ie91LZOPI+mVPt420UfLDAel1BMfVkuHewhD5PXVLJcVTHhP15youhjp2ZuWErzWxddcxsXp1HZa2VYmrHJ9KoqlWXWVH066oexFJZqaxTMbfzNiQFdys6dTMDEzPiKS/4/lG2bIyVNawWEAsYANX7YUqjbJ+Y6Zpt4NyVnaWrygnlGY3tFBaEhKNxmJjR1wb1bsa+Q+rWrSsmsKBCy33kWspaH6cQSadGtTc0Asa8fM6Zbki7daOZorQue4yebXWFq8T9Rwl1j6rE8QABAeIRtDOfAu399epMF3Xzza+wCYjf6j7Nmx5cXbWtK1RsA7ehmCx5HPGFNho84iYFYu7hEwo4+vn9s1JqOX148pcDg5j6A9TDIBLcnoFwcZ8TXcclKHeG6sk5DofBSShITGCGd4FKhOWCOx/i6hx5kv8AiNaeFiD2FdLHz3X9Kdj2jIFfF+sXQv1LNgrFjvJ6gTVvNioEFOQ6dyTLtSVu8vaoHKNTI5B0q01lboMnz63slL7raIjHbx6Ja+C/06v4DJW8m98NEFYOByFJ2NyI9wvZRjc37JE7oxeQfV6ecaQ5ttZE71iu9CpXfyVQ7uMYgojvYbqJFarFS9yEm9X04j8lLCJmfzVvxWX2gmhdszBXHzOlYyorzx5yIiMcRiIjIzAPqM9LmDx92zFlwzLAAFgIAMCGup7yk0CqwcQ+lkAQEV7AyvQMBgwYFBD6vvVkfee5Ny7f9oIEa93JWp7VdXdbUwWd7hN7o1SqIOvXBLGk4tWukzO1yrtgaqErrqBKh4rMoACOfjpTdpXbRfcQiUSJRExnlqqUCsV6yJPCpWOOrsgBhmsyPLFWo1heDcPWiYghq4+lTmZrJFc9YPLu16+2w0+pnQIg85iV9QKmREpWU4W8P8QvveJBoLdY/tYOomJ8x51nS44i1OnL/wDliue2+snO54Up+mNVZi71S42xEx1b9Q016iIGIGI2jTGApZMOdg6fQVp78w/eT9LWWeiw1GRoSdWpjsek4tV64rZrqS9Naj2Fz+diaA4+itEff6dTOOzZrYxMzJJUCVAlcbAWas1c42bxGNdTzzlKwuVsqpxWP/h1OK3PuTmbLa9AzQwFtv4y5UqfxM70sfh3usY1Dn+WemStuzN2MXRL+W6hotUFWtWXPtMXjLeRtc1FPB1laY2LyUKfZnk6ZWsQWoNhiBEnsdMhWjwmuCvq+4/wSIzMFMRJef8AHo6zIl2kjzaurJl3LM8z/bmUAMlPwlwODmHxYUTV8BLjMRMRETO8wxZHIQUST1Go/cI+UuBwch0+sQnFivH1gUkAkUcZsI7owQzxbXsdzcDji5gCwJAvIgArCAH4/A5y0KJzSgFh1eiXcTrkKDzGMCRgrK4nJ9uMVakdiHCwS8tUkvp1OHqrrWk1R7ZY6k7H0Jr92G2Rv2cYTbGQf37uFx70k3I35ibWMqXrl2yzHP8AbjBdTY7cjgbybGUQnJqyVKJE6712Eg9U7gqzXcRClosK1YCrXZYZ9uLB1jm8o5NrIhCYXHzirF+pmWY+82WxqlEI6otqHwNoSZ1XWjh4ep+DvlDORY/o9ozNhEjG+sj09jzRZcpXF+MBc1ALjElp1+qneCPcqVt2QuhWriICvEqj9Q5LXUtVKKSGrGB1ExMRMfHrlcFWyRQ0yJbsjh7uOjk2O/WQsiKCqN4si/kFDxamSn+KWznipHlrLsjPuXwkUpJ7l10DsTun6WPxrrNmZe/pmkNbHC2Y2Z+HIOhFGw6ddKJleM7k+nV1mQqprRO04eeWLqz6ZEeWPsjtvrpo+WHTHp1StU4sjKI5rDHvrrE5DmimFmyxaz4LYi1VsglTZIu5mFTuQ9yF5i2qfqSQzczl+xWOuQyKTRw6RiWfOR8YfFOj9QZ3GJ1ghkM9kBLyXVy59tXfA76rOGxXU8fjWYr2LWPbXrbdx/UCMWAUKqu6VawuyhdhU7gwIYslzMxGMxmRoP4Ta7tKJifj0iYyXU3/AKkeuJKb3UNm3MTIBaZic29Nls+39/gnWJcxyTZUydW1bOvUgjHXUk1XmmsCysXgxGVyJpXeEatJSwUsVLjiEzERvPiLeTtZewWOxc8UqTjsFSKCZx1UTczc9hc+2xtSoimga9ceK/bK73emJk3WFp8F5KEtsTysTxARgYgRjaNM7nCe3ESaK5iXdackzTWQpcnMTOkG1gyTBgPWOEzMjtv+5BQLkpCNvWxWKC76PDa7xcG8eCeo1H7hHylwOCDHTWQtcnPmFsBoQYTvFmv3PrX9LatmXRIlGzPXM5eaUDXrD3boYjP8Yf8AxCRfaV1LkIihYVArvYlTMUVKsAwS8dhUUXjasgd7AS+zgrCC3LUY20eJr3FAaruIzSMkuBnYLOXw0ZKVsF0pbj8JRxw95mzHZbNlemcbjAlusJi/4bU4F5eOcxpS+Jbw0t01mGx4BZrULT8W9mMtM2r4PFZBOUBhgS1Z/IRfcGMpzyjG0BqJGJj69dRVC4LyaI2sVLK7dddhU/RmamUZmodRWcFjMxcXd/h2WHi99dFhcqesWBUx1KlJzVVC51MRMTE+Yw1ARt3KDTkZDH1Qjbhy1T6fxtQpOF908SAj1Lej7fTqRXcxDtvnGt72PrM/DcQNmq5BfGMwf8Rom1TO3ZLHdR1vpEO6NNeYykHCDiIo9KJWfdvM9wVxIR1DjlAMCHVBzNFdYPLErhSgUPx+Hqux28dCYmOWJrzWx1dJRsWstB5DLvVG0r6cPnh0ejo5JYM/HSs74qI9M8EHiLMTG+qnT+Ns0UOkJE2dKAM8kMkZbgsipsNEyIpPKKnY1AzX8QYP6tZg6vX1WK/aCCE+oZleOx9QCgQyo1LGGSio8HN6eu+7xq+U7soiSuqrQn41l0FYxthQB3Dh+Vx1WlZJhiitnEPyJUJCVlvvG8edVagWMpl6pRG2EzlSlR9tcIhYnP4l3gbAjNjM277Tr4qOCel0WnWm3TaZKuWRq1W2C+OlkFFRtxkfmac5SFy1xwtdnq6N+NNHLWHt/wAJyBruxAhZp0cgse+AuCencPI7e3iNV61WintJEVKyfUJkz2WKjuuw2H9jBWLBdy76Zi+/I2RxWNLkLLVbAIjH0xmxeq4B9tsXM0yWsWtaghaxgAIhCORTERNhtgpCtGwpqrV9U/WxtzY+2ge4aobAfmzEn+IigRkinaOT7PgN1JUoFDwCNo76u7CuX1/tpMYmBmYidOtwlsAYzx1ZcaQghDlCmg4IMPiwkgL3CPvS4HBBjpyjSc2ERpTlvDcfOl1SU7ko9laiIiZmI2nXUOTZRqitE8bH/Tl4wg2ZFnfcjG4+yGRuOnunn8d3AUgistbDJWcKmIYnqO/XYarqIeOMiq3MAHal1cABYwADAhrKYRTckI0GduzFbq1EyIMlkDgs1f2/iVqRWiE4hxoSuO0l6nDyWW+upcYCbAZEVwSLOOGvXm1QibOLw7KF6Jxd6OY4pNVg38OonL0FbF4FSiMZI9FkHDnQoTEdkwFgEs43Dp8jqWLWIbO866pqydILYeGVXe4rKftt+C6MUs7Vu/C/QI7HVp7+I1eXDaVhc66ZZJ4hUT+LBT7bLZCjttBlxAi10kEeyc3ad9Wf/wDKKnnxkPzuoqCfmPxZ6Pd5ijSH59MKPurWWd8x0me+NMP86KNxmNdIsmab1T6ZNXdx9lesC3u4isWiyWPEpErKoJ2bxSR5FZAtKNF6uLoGeB42qfwMhOax0BaoqAuWslg3ZBFYO7CyaM4XKNGAIw6WXNXHutPmASCKGQtpydZvI3vBC+4fxcZN+eDh5A7AWHcLIM7Tem0Pr47t2AlZ4qlaXl79p4cRVjMMGRbBbNt38BjbK42Aa05CK6+nWRi9pr9OOS3FKFMSOup2wGIYP+cWrs46sv4nIX0Y+vL3z4KbWVZFm8X5SrNbv+1UOrNZdkODNMJ9CyaqrmrFOW6gYQVltEyu4m+ddl/I2oOcTmk4wYCa0GVK/VvKltY+Y66nyM1qkVlFs4XBhKoUaY97KYbDzT5Wrc9y96NStvHuRvqIgYiBjaNAta9+AwPoRCIyRTEDmM9ZtchpSSqmGc1+MrtdMyzTbUwfaQPcYhbh3Jp8pmImNpjeHWFJj658/wA3Z/8A+Kk1lJj6Y3L9tYrA+IneRNItFcQ2YImqBwcDjwlh1ziu+fExExtPmK9bsGciX0aaBVWd9cbqEhMYIZ3FVRanS0Zn1mYGJIp2h/VdIHStKzeDbQZ/L1BQBRXyj7o0pbjOLWUMbayj/f5kZ41cbRpzJVkistZNLsLdjJU95rU/aNUNqssRH0we1m1fyE+df9R0ZvxSGCn0yo7WInQMNZQQFIkFtV1RVLYbxirJ4e8zFXS2QGOoCSzBC4nKhGMy9fKhEwnqBEWsS0g+osZai3QQ/fecyXs8vQyBfpaygzSzFPID4DWXDuYy0O2+un2dzEV5md59c/VmzjWcP1MdbG7SVYid51miirmcfdnwOiGCGRLzHTJTXbcxp/d+G8z2PUlexvuDf0j10pMTi9vQp7vVQxpJ+46qaQ/b+G5nMdUKQNnM7uUY/KFkEFK5q9TZQ/planS7qLICk5mlC9dLIJWM5lG09ObLfka0eI9OmJgbGQT/AJ02OSjGfjpYpLEjGmYPFMaTTrjJ9N0kMvXGEsSD0ys75vFjPmNMSpu3dAT1k4t+z4USBbMRjbGPFoufDouhDhGvExDKtJdf6vuYXLjPDbnbuZMbTJbdWlvTuRe4m1LTZc63Xp1HszLeXPFZX+NBaruV2xthWw2DZW58p6VqsTjpaeuo6F28lC6owcAMAAjHiOpLHuTXiqy+9YuY+9iEqCHicqojj0BXnaX6JnNHt2gLFzjKUsk+3tFdSq3L24QuTWthSTBgixDf4ZmIRO8o1YfN3IW8iUwSOnMbxX/ErP12fwm57TJdeOMJV2g2kpKc7l7dV6qVGP5g8vlWJZiXblZsV1Mt1MFX24qUtKxUoeK9AsA3kYiNGwFxuZQMTYc/6aw7CmqC55l9bP3bkg4JA9IcSj9u/wC6xLoXun7xmZGJmNpmImNp8xvNNu3/APrxO/mPgWAczAlEzrNnIYqzIztPS41JxwmoR77KaZQ5KohMdInxC3Xn5rXatvn7dkM1kFWHUnKrFwdhK1qrjxVbnduSq+8our/6umsgCadivaLtwzq6iJ8QUwxrdR42yMjBSpvSpgWMmImOeSpNdlbx0xiIxN+MhRW/x3MuPlR6r1m2C2CPFaoqsP0+S6ltUrpLRVgn2um7c2MfCWTPevVAu1GVj+MBYl9I6NiPzenj9s23i2F9WWpe/oNrx9+AvTcoDDP1uoa/fxTZj7se/wBzRQ+Z3mzx9u3l9vSpyWKiJ/BMRMbT8Yjehk7WLLwrXUNP3eMZtG7MNcG5jktj7tMOanVIlMbB+HqseNatZiPqiYMIKPMdOZGlSRYVabCSLqbECW3dItIctnVIuWXNWCuVhy95jWCGnWa6Fd5zBBYZvEn8WgjSrVZ0RKmgyNdU3rlckoScqUjGVIGCme9r2lX/AO0GmYuofwMhN9B1lQMPMxw9D2VQYlhmZ5I8Tmb0wO8R1W4vsTJa/wCo8htv7WYjp24P8Wtd2IWWi+2ddKf/AEw/TpYY9o9vrlv/AK9jI9c4xmTyScQidoSoa1cF8pIaWSbfzEvCdqkzAxMzO0YvLDkxfKlkuDpVjMjNcSdex/Bcn34CSr+4x+TrMStwmAvw+AQSRZya68GVyK2ZEyRVoYm3TeptK53aWspkAx9WW7cm1FrwtRmRyE87uKa3J5BuVu+VmZMMjL59brJVVYcTtOMBg1oJhTM2R7mUoAEbnkn+3x9h3+YA14hKY8G3N4vGcaUkREXV1XeYVXaev+r0RP1VmRqt1Ni3zAkcpISEhghmCH1Yub/VAkkt15SMbimsyPDld6exhoWV6z5taIhGJIp2ibRsmQrDykKccuby7pxER4j406ytX0z9RqMzDkYcJ/bJs8zlZj22atxY4wSJ21WsQ8N/g3JBwcS0hxgft3/fqxaNDRiR/KMQauRnyNZhKP2zfl6iqs9wn7BmCGCj4z7e9l61OyUhStWl46Rx2BGCsIzffwVh9qYh3SY88g2JjcbAx03UZ7c+4/EXWBxp37IOuaTar2CYKTg5srn+MZCurzrFW8sVcUY2uuYtqssISyeL3i3ihqpRbS4iVjaU0eojqw45Bf8A8Dznb+2jYrBYgROZiLeSx+MXxYcDMRls5MwW9HHU6FWirtVggIKJxvUEHtEVtP8A/h3UKnD4T1JTP+KV2qKFkFTqlw9p1kUhVX/BM6NcjkkXOM1Hwf24y7k11TitbWtdN2Wal0ZMVqr4ikilSFaGd4PwdQoYuEZNEfm1rC7KF2F+QmN/E/GDL2WUu4svAttVkfrNBeupMnTedYqbOb46hzTGRAv8lZzLJiTuyOpLIzG3vna/+JbbRfdppXgCSbkXQLYyFsC7Z2LVfEZZP8KTLTgnNqKJhMREJmaZzO82GTNXJMx1neyubKnrQ4i9nWbGq+PCVbWImTbi+TN1lALKjXrjyaRlI22p29veasbWTu2FSp7xsLgzD7SmNLsT9Me5auU3KNatJttHbbfuULR1uzJjCMvjGxArtLmcoIuzrYS2IEcW2Y2ZZKYHFVY+7kemYiZZJJZwFWTyeMZAC+GrmzDMdNuI2jpXb+FRt866W/8ApzI9cj56lx8T8aIoEZIp2HplZ2LFvJsncuqMlCURRUezaTqdeFCtobZm2scTZYoxPXTa1hiEyEef4WsmkZlMi6hUfXmsxcSp/R4fNaxI6pYgRzg0j2sgynVYj2xqGU461UTaLDVwPYzBYEZzAghq7LmZ279FSzYY1DM1ajaUo9jjk1P938GWbEkqvJcRG3aPkVWqba3TvK5mDtEO0dWtkcetUb75ZVbH4unJ7xcwtWnK3ZTJTBJIcnl3zcx6vbq6eHGwbAtmUW7GBxVgOPYFc18rkcK4qR7GutYXarrsKncNZC4ujUZZPzrBQOPxbsjbnjOMqtzV0sneH8j0dWhzIJhTICIhECMbR6FEkMxE7SmspPmI5HMxEbz4g7g7yCRlppJhLiWjxP8AaPQLhj/SarBCfZsfSenpJZ+4RH1KaDggw+HoFwbT4Ku8pmUu8NYsGBIHG8VyJDvas8jZRDg2idjrPlm6XRs3WRxtbIp7L4nWLwdXGkTFyTGz0pXK4TjbPYkoxfU25fRXydKneTFeyUDNTHY7FKkwiA07K2cmU1MSBCKl1cPj5jf8vGSbrFjJHEidKxOIykTP9tExMRMTvHVCpPFEUaeztdQ0bE/p5nFjk60LguDRy2emgS1iQrwOMoFWXfn+Ye+/TrgRucARGVvZY5Tiw7Cc1SKxiyGCkn4y5F2imx/q6lrd7Gk0f1MuM5HBKuhH5uKuxeoqfvufUlGbNHvrj87D3Pf41bGbEacRYaVuWCSBrOzOSpqxqQ/lkJBCVpD7PRthCf1WAvTM/iF772RmXdR49wEpSW2YoZ2xilsq9mSCtmcrkIZA2U1YyaLAmNmLR2WrqYkRgjrm05pWQjgvtkNWmutHM5iWNlxyMV2BGpXkPkXBOk97tx39udmuNhfAp46FMwHZdcmUA+kmOAGIx72r/wDcjT3qbI9uz2oiSiY43onSmnBfmOUQQxc/BjreJ+NO70h+TIwe9+PkVnpRMPeGp4aKugvuWM6CuhcyQBAyQVGT5gCkVKEYARji4VwcwVOePChM/LEkIsn9C7vEjlY8iyTjk6TmLrHAGMqdOQwTba7p5a3UTjGkwo7fTeVoV6XtnthTV2qrZiFOA56bcpdA4YYhPpdas+qqojO/ply44u1O+2ulpCcTED84ymjK5a86wPcSjD1bWcbVTExTyFGrL7MUI41+lEvDHkxhT22ZQv4urHIgTjVuyFSs2wf29M1j7Lcg/wAu1g4h+SyN2I+nqbMDJfw9P1ClxZ+1XqCHYpZcVWszTpFEQhzSa0mF8+pFAjJFOwksix1jJMj6rwBjOnJREbFgcb7ClEFO7epOEuxwsmIXlGPykPyc/TVweLnKMkGnI1rf8QrvppxyhirfxNC/HKwGxzStUq7LGNykMRcVbsY1eTtTBHi22cZYppE+7R1fKcxmV48JmalmSz2UGoneKC1rUsVLGBCZiPM/ANWyZgCgvVluIPtpHusSp0F3HHuREIxuUxEFdGZ4pGWl7dzp5WS2EABY8QiBj9m160yMH49bNeHhA78ZAZEBGZ3neI+dNWaDl6Y3FTQaEGE7xZR3Yggni2tY7sSBxxbIjJQUxElq3X7g9xfhtVjGK3bGxevU2NO3WGwmJl3vMVl6yhvtmtdXjsTyibuU90BZ/CU0wuuUENu9dzjIDbs01LFSxUH23Kw2UyH+vpnJy9M0nz+flE9/HWVR83yNuBx1xe/KpZXbrLsKncJASGRmIkaAlh8sWPL+0HpvGRYKwYkycMHss3eo7SK9YmJx+VtY0vCstbphUdXc8Fn0rbh9RlJswWsQ+cbmX4w/CZiCiYmN4pe8xObOmgZNGazV0n2qPIPb0MxFaoqrSpPsxezucWI/yntoDM58hmJFQyxuVsDs+6UCNGvE8jiWFEVl+YFYSV5MRsT4mGNptODlZNKHmO/aqlELsPJkAdcgjT0d6IjmQRGPrf6ok5XVrqLmsOJehgJjIF8Bj6oR9nKYrV4+FDrsq/8AQOuyn/7Y6muiflY6mnVn/aHU0Kk/7emUaYRyKZCIx6txNbDj1dbUg4A9912UP8AcTM0Kk/7evYAPlTDXrs3Q+10HoDsScLekZgqlY/uWOv4eiN5AjXpLkAPAnwySr1HR9olpuMCQkVGQ6rGtIdm1jYfqpQw1pO7UlTdR6X5LYd4ykR9nESMDd5qbAnADkLNTX8HzBXJt1mhYPHZ4tnoykQmwzIZvLw72KYipjUvxuGKHiIt6ab7fGXbZax7fYYGxkCL+YsIJONoUIni38jHUv/SmnQy1s5zNSRht3I9SVkg5/aRrqNjHlSx0FsxSwUsVLjiFtvYqudrGhdR07LKY87VfpdjaDDsTxv4jEKxiJiJ5vr3JXkHFaPkcTExvE7x62+5YarHo/Uu1wZksdilR+Tm97uRpYsZ+nWeZOQuJxFcYI83FWjhooLHeaFdWFxJMZEyTslbyGP8AcYqeLa+UnK8qDKrkay9VeKxS8cguZ5pAU+nYrbxOr8mvG4axGs5lYpJ7CZ5XIJ1dI4ilsd7FY1WNqwkJ5HM7RM/OoU+0Uk/daxFFcfGwRExMRMTvGgWmsE7bDE2jbPGsHLUVOc8rBywhAQjYYgY/atUDg4HG8VltUMrZMEPraRL1cRnYqbyYMrZ+o1R1yl6I+lbBaEGPxaQUzD0+G1rAvDf4Ky5yWCcRumJ3jePj0sPCuhj2fZicsrKLYYBKyvWhp1G2S86XjURiGZG54dRpVrQTMkQnGPpVxlpDJai3bJ6iXsK/S33aVleRr+CqWk3awvVO63ufXq28TOwh02mFYlUwUl6Z2iVulJJj+Zxd4b9JdiPuzkTSv08qO/GJiY3jzHVItRFbIILg3Bpw9mGMyLOVlbum8SROQYyy5k4v5ZNtaCILnUmVE4QNaKzZXZsD/PWmtitNOoP5FUJNmRts8dyRhzbG/wBAdydrxfMrXr27ij67Bzr2KJ++SPQ06o/Cx0KlD9oDH7IhE4kSiCGIgYiIjaPXtL5QfGOUxvG2vYyM7g9g67d0ftaJ67lwfuUJ693tMQxTA9CQk9+SxnU0Ku+8DIz7Rg/pWDHRndQEmRAwVWrFrYKVcmsRgcw6d7Nn2w27tulwAKjLS56gw1oOzbGQ1TjKUoIcY2taX9UOsHmQcptF16gP/wAOtJtotZHOW6zav8OINSy1Uw01GgS4zDIOwjDDIKr1zXluoFEmZ9r1RbmELx6p3dQrRUppr+N8qcXeoqtSBkhx8xkeorFudiVrOt7WJslvtONT2KFdXx6Z637TGNIZ2PF4CsWKhdxe7bGMyeJmSTE2qiL9Z8REFxLTWCpZMPwPTtMuLMpYjZmEn3l+7lS8Dg4K7ft5U/tvWgp1G2T2104qRVZy9udivNvZEm5dEyNfH5Onlq8rnbuO6et49sW8O2ZMczn5jhOOnmkchkM3XTekSZ1YfNdWmHlmRyaKiV4ymEWbRieLLvP/AJnM4HElSUT7Mb2/R7ny3sIHaQpDM8nlLTmQWPmYEZssbMjWDlA04KebylpRERG0RtHo1oJDmfxD7T/CQ7YpSYTJsZJl+1tpcziSp2mtZh0cS+luuwvu96PB6MSqM7oRukSExghncZqkNqHKmBEhgokSjePXqDl/B7PGZicWL8ffot7kdrqtshjICI8ChdnOoqMHdLEfw3Mmj/bMBMCAvthFbHHB/U1q7MnaZX4+NOECUYs+zpEbMC+fPtMh09Tv2YsmRLNawUsVriBD0QX8JzZVJmIqZGoF2myue2/T16W1ZqWJ42cnlcNIxXsfzcvxdvKt7qKI0l1Ok6StitGTyQqlWnsVxWsswN6tlW27Cpka7u+kWbcf6P8An9/cuRWiIiOR4rENyRw+8UyiLuJoMij3ArkBqfEMS2CGwHU9JksU2Liv+olTMrytAlwCOl8gQwkoS08Lk0jMVL5GuxjbcDPuMUByF+9TMVostUN/Jxku1F6JUbcdjXU4nH2e9bxTbmPdNhdM3HiKtnKZU8hdGR9OmEG+/auu+pl3E3cIM361nxjrU3KSbMjxnqTdsU6MTqIiI2j41fiMjnEUfuQ+yisvuPZCwiYmImJ3i/gMfdkjkO051LMYwpHhNqvYv+5kEtAlrytynVx81yb2JslGJ6fXXQfdZjKg06Ka8fPUxNtuRjq25utZS22knESmUMPt4yiqqqILWQ7Vd0WKzezZo9RZjt7lXm2DepMkQH2aBL10spjZs5F+5My4WMnnpr1fvY/HYBZKrbWMlgcUEwOVsn37MtWJQJFEF6ttCJcFx3GDWNswdmeUkS1DuUwIqaLR5jvtqSEfmYj0mILxMRMf1+8Hf7Hnn/QfWhn1rng2vY7m62RxdqWBBwElEFMQUSMxvC5KpZ7Uzur0yeQHHVfcEEsjNZZiMWqzTmImvlaMVFG62ojuwFrH2BWUFETP8Ip3hmOfU8ieOrtidwQf/wA2vj511bX2Wi6P3AUGAnHxkEWCap6Igio1pQJSwuTpmIjefi5bZZFiqoySsHARiavCIiPwZrHKv05EiFbF5K096phM2r1bp2xcYVzKNIHQnE4lcHxXXiM3ZtzI4umThjHZi15u3u0FLE0aM8kL/M6gV3cRYjxvjiiagRvH7mbCB8SwYn3tWP8AcjR5OsP28i0i9Xd434F/TGueRyo11xyFCQQoVLjYbNKpbGBsqFsH07WA4bRaymyI2iI331kr1emsZsKNy10+ncwP8vEKatJoqwlJcz/i2crQRXMfzCvmcRkg7bZESd07iHj4T25tdIPGZmo4THuZ/EFxnuAFXq6xE7WUCY1uoMXZ2iHdo7ODtDZZexNmFyeN6hycCq60QTWrqqVwrpjZdvFWX5qvehke305oISbmTsHTiiJLsg39TMprvomt4kUYJ2das1UWBKa8PhIRYkZdkLE1qbXxO09N02uNuWt/U3IWGDk7LbqO8WHzVe1KaUhu3IWip1GWBHlrp5BvZYy742YdRM2wtMTDG5aVlw2KO4Fa57hgdjutwONPH05Bk/mdS2zbK8TXHk2TrYXGByiZVi6eVvMc6uXYXmMbWx9JNSsHcs1kDWrqQH2hTSLJZO5Fp1hSY3OfO1iz8/kqBaa4eNhibLGzxrDvAVB5c3FLTNi1DucwMe4e/wAVx4iNIZnk45YWmOWqNzKI0l0OHlAzEfuLFeG7EM8W17EnMqbHF1hAuD/saO72h7vg5EZmJmImfTLZimtzMdernKcNgf4kBPaZLr2cBg67VJa5oMtYW/iOVzHvk11m74K6rxrKzBdMqlpbHGXGMlUvAM883X9xi7AR5nGs7lMP++vchUtWSd5mpSyOZnf9CnSplRv3MQc7q6YZzxQhPz6WrtWmHOy0VxOZvXZkcTVkh/gVi1PPK2zdo7+FxI9mvAyzv9QZH9BY0EI6erdyHXWHdcRprq3ORUqvnqtq1FestrY1ZRmskTa5wFOnPSeM8/U2NXOm7lUe7jnE0VuyEDMtptmIvFM8YrtkvcW5+2k6Y9xkpKeNM9oLI7eaDt+eQmNxoOmDfkh+abBlOQd34U8IH8LGAoZJhQIzkWsKRrK5aGMyyJ7dfloqmfYP6Jjr2+eTH1VzZBXLKdos1TXI5KrPzMjqLlWf90dFarB8sHRZCTnhWXLCiles+GsmNI6cMvPaMoX0wPjkIDqOnKsR8xva6TUwZJDYBn8Mz1Of0pcupYGyzsTsp50LgfKinVq4usXA4mT/AIlU/wDVOpyNSP8AXvpT1OjdZQXq20hM7Gcbhae0ZmpTY6emsZZqQ6zaDgz8I0aYviyKQF2slkrNJgwqmyys7vTuRna4marwxWSqjDcPe7qQz12rERlKRrh2SK/w/hF1YNXh1XlSeSqAizPSy++Jd3vJLp51Yu5i7hokr3UdIdrFUbIp6poTsNkGV2IsIsBzQwWDrqV5BQGsuN2VkjXrrQPx1C4EY42T5ZhaHsKAKmNm6yxnk8gvEJmeytYKAVrjiF6ii+ia9jfhUo1KS+3WXARmbDsjeDC1ZjglK0KBK42BkyKyIfm7eJRwtUdyxh7N6MsAyfOLDhroY8/t6aTFpz8q84Y/qmxwpBUDyxWaTi1DQmq6dBenNZyoS1kKvRkHITC54mmoC5ljZ5sK3ElwQPdMapMnnZLlJGtQ7lMAM2XP3GsGwhTHl3HFLTiIiNo8RoomYmInaVU1gXNkyw/29l5IgSgJIQMTGDGdxY9S5gTKBmxXh0QQzxYixJT2XRxdrI3QoU2WSjlODzw3t0WpELWurhOaKSj7MbUYGGXVJsQRDjKCEKyDxc6jkX26ty5YGApIYpeNtLKZ7tnF+5woUdoFmNwNs7YPyawhbiAUsJn6dO77WWAAywWVs5ZQx3alKGY9KMZUyiilkhIyAyHgeoapFWC8neLPTNtZ3ril7wFzIU6IcrLYDX8QyuU+nGq9tXTg8fVmbV05stb1FX5yjHqO40sdm8nM+/dFWtUx2Pxq91AK9BmMc20NVToNurWKq3LAPs8mQtS1BC1DAB/T6nx9uz7exVCWEx+QIBh+JOWHYsr3JtNwD/Fa+32nvXffvT26dfkVfpY2FDci/mVfGUq8RC1RqIiI2jxHqzHUGzMsrKKX9O4l0T+T2yrdL41DOZ8n6GpVAYAUgIiIjGwxER+HKYarkQ3KO3YHGdT7lW9xPZo9MUa8Cdj+ZaWJxhRtNVWv4FiOMj7Udsh02aCi1iZkWI6jfEdk8fzsyHUmTmAIIpIpYHHUxj8uHM/pWqVS2HCyoWQ3AW6pSzEWiTH8Zy+PnjlavNUVen8yMEiYQ8E9QYyeKpjIVsddO6iWMQdcnma0mxa5adbqWsTOzdWdN2Upzlag+0cvVLp7JLsEEmdVnf6koT+cobyW5ddzNU2WBmulT0vHklgsG+P8Rz9elPlGstkIoVJZH1OwuPKnXlj/AKrfpkro0abLM7TPTdKVViuuj+Y9Mh03XtXZYqxCjxuGrY+Nx+szATAgOOQl02+qUtxdslGnD5KzfVcyjQKNCtYlJiAiTXOJkqrj5QDQDZpcy0ysTjmWsmVkyvWHaZgI79l36AcBCkPLm8pacRERtHiP3Te52y7W3OpZlkStvhuoiIjaPEPrreOxeCQ40l7d/wA2K4uH/wBJ1rDZZNdsbn1ABHiLAiHOarsayrFe5yU9WTzOPCO6EZCq/qPEXUHWtqaIUc7YxhSlRRZqtzeHukLr9Ipdby9i+A1EiNOlepQuquU/ONvLv1AeExvqYiY2nzCaFKvO6ULXMxBRIzG8FXlRZHCz9uEsxaxiDifqvZOhSGYssjlQxt1ru9jVlSV7LD4qO/fbD7P8Uyd/6cVW7SVdPy0oZlLJ2zNmOxaYgpXWWWZuXC4YmtJivAssFDctZKyQV6NMIIVrSL+oQM5RjElcdVN7K6zshC3EQjG5TER/WbTqO/VSs9JQlA8ELFY/tCGCGRneISoUrFYzMxtG+/8An+tMRMbTG8WsBjbMb9vss9r1Dj53rui8lfUq1H2sjXZVZkIuvqQWMaItPLPXMJztCCBWOqOjv4K7KDHM36MwvK1S2qZOjd/tnCZZi3YVbOLGOGzRTjcdciX4W2VZ9U8jhLbrF6ub4r53GWFG0W8NY1bMtcnLWR2R63mTm8yFBZfykRERERG0azWXcETCpmNDDIsrKs4ztjO4xPz6XbQU6rbJ/GDXZ9rNm0Uk7TGrUPI54wqyDjkQiZjTbalzx35n/OP+dkAuqlfnbkWm3FB9IfmGA2mFBsLthpz1Jjc58jPIYL41M7RM/OkLcTJe6ZGf2z60MmGBPBqLEkXabHFrhYS5hc8TqMduSnxPNyQcHE40lpqOEWNbR86a0EjzOdoKjjbap3QshPpNEFJVrLEzPTmUCJBdwDAcPmaowI1qtqMnjb9SBbaAFhVxyA4tkpbOmWHUXOnHMIQqNF1VLRKTH1zpVE3at3viD8bQzDO57QioUfY4bDB7mzPcaVzNZSNqCvZ1qWBp15hrt7Vmzbq0187DBUE5DKZTccYv29atgKizh1oiuWLN2pSCCsMFQjlMhfnbGV+CTxVcI9xmrkvlC0AoYriIq1bw6r1qHWmGxIAKwFYRsP8Ay9isi0uVWFwwFKWlYqVHFeWyFeioZsIN614zDZT+YxrSrOxlfIoWarzheN3A0bZdwRmu+V9Q44NlEOQSM4TKHEzvjshRjMIsRWt8bNbIdO0LkSQD7dyjzODCFGqLlHH5ijkI2SfFvpcqXMDfm/WiGV8flqeQCCSezNZLDw0pmVyxdLFFEwIr7SojaNo+NdTH3YqUBn6oiBiIjxGiATjYogobYSiOP+eNqz909lSq6kx9A+fVSFK34DtLrCkxuc+d7dnzH5K1U0rnlMcz9d432/z+0e7sr58ZLSmi0IMfh6AdHnwarBgcJseC9GqBwSBxvC2nXOEvncDAGDIHG4oruQ6YGd0TMRG8ztBFAjJT8V7APGZHxN2ou7VZWZEbDbPHSVK0EyfdsXV7Vw9tXxhY9vcxNJROr9PuNBPxL5/N1dz9KsfZVvZsQrOZPy4/4fWpYPH05gwX3HaDDqK8d60c2GWr1SmHKy0V6jJ5LIzI4tHaSjA1+5D7xldsWLVWovk9gqCcreyJSvEp4qq4Kss+/bKbln48R8WMbTtWAsWFww4iIjaPEf8AOrxVJVyLil9tukZCnYeyupsE65YOtXJ4KJ2or4vO1hskrVLF3aFgBRallHVqx7aub+BN0yph83HfpthFuMjl8QXDJrmzWqXqt1UNrMg4bK4Wct27fshOpYzEM9oNXO5ekpTLyZbVp5ahdiOw4ZP1UcZDqgijyr0s9+QgUfKKq1fVP1M06ypPgp3IZtOmJ/RXpr1Jj6y2mW2bE7JHtrVUWE8y3Yz1n4/763uunaIhIKqrWXOZk2ftVqWqJgI2jTVA0eJxvEMbVmAduavBh4nwsm1HQk5k1sWLAkDjeBM6hQDfqTExMbxO8OSLg4FvEIYVc/bv+1yirM9wmPpU0GhBhO8W6Va4o1uCJ0qEVDnCZiOdasbMBfKs/wD+ndQPTXtVrtVozdGtmMvHK2c0alPHUqAbIXAauZ4OU1saPu7eIr5NQsPIu7hXcnSojvYZEF7zNZGZGmn2dep0/TQfesTNuxey/ZeNKkv3Fx4WGVTFJwqxVwKROLF4yuWoiIjaPEf8vFhBH2xaEn/Tdjab3hYYqIfqIiPiNvV+TzNOy0305ZTBGGzcS6rM1rk3MpjI4ZNfu6c4ilcGLuFf7d2Tv5gEewyIwoaVT+MOX9MhiWpU5UpaEGu30tQdMnXIq5i/M4y9NOu/3hf9U31DvYpba/i3UGQWRUasLV0zj21KrG2AkH/h7a+fPjHN1hSY3OfPdtP/AEh7S1U1hPM/zD9GOWqNzKI1Nl7fFdfhIMANmFzOZiI3nxC2rZvwLl+2YD+6JrPcdXEMbAkudirWe5+Wz6WkMFEiUbwUMpzyDc66nqdG4Tv6EImMiUbjBFSZxLcq4lBRBDO8WEC9fCfEiPEICZ30qoCnS0ZmI1kq1GxVKL20KUdrIVyxdDk6pjMHUx0QQx3X3stSozwaXJ3ssplyk75zUpzOMw1b/Shfvcpl4kKATUq0cLSpbHA91/oFdC2G1axFk77ePmN9o3+ZmBjcp2ixnMVX352BKT6rTO/tqrWxPVL4iZ/h5xoOp7j5gK+PIzbl+oJnbtpRqb/UU7/zEapdT2UFCckvlFeyi0qHVzhi/wDhv8/+34beXx1TeHPHnY6rg4kKFcjIhyN/6r1owBtJQ369egbIOImIiJnef62SxSsgIlzJL8cF5daAvmLHBjqYW/eAuBfMRMbT5ixgl92bOPaVKxWZbsEWOy1WC0zBWqRy/DvkJx2fVYZ7W4HtbV64ulVZYZPjAViVXbk7cxDuDOoLXIpkcUAAsBAIgQ9DYC43MoGBtE04hITIaa5So3MojXds2PCh7S1VFLnkX5huepMbnPlTbDSguEAprlqjcyiNS6y/wgeAKpLCebPzDNi1jucwMTcNk8ay5PUVWt82WTMLWCx4hG0NtIVvBFuXctuj8sYUMfHn9lFti3SFgeAahgSchBRz0+sLdiieLFWSguzYjgyYiYmJ8wpK0jxXG0PsPQ7ch3QJCYwQzvDVC1cgXwpYqXCx+PXI5mtSnsjEvtpxV3JMizmSmFrUtQQtQwAaiuiHTYhY964VkKrCqjBvpYQ2s97mC79i51MC2xWxqvcH/Hs4oYY2uogp9SY6wrk04rstdU0EOha4J4h1djp+5bR1PVmL2mYhszY6ntWI4Y+vIaiqy23vZOww9V4w1baVU+RRmgGNhRtB5pkx9CoGXXLLv1GTMejVLaEgyN49tcpn3KTC0HUeWUMcx30PVV7bxWgpHq+Y8Nr7SPV1SfuSQ6HqzGz9wsjR9W42I+kGlJdXo2+iqydB1e2J/MqfSjqrGtiO5BqNL1vHmveRid/+Bz+SsWL0Y+qyQUrH1l+ZHnMRAxtEbRpjZp5Svcn9N2SpIrxZY4YUzq9EH+VWM11OosZZDcmwg/4jQ239yrZTlOHmkxYP9feP++pasY3IxiMlWxN9fCwxYscNiqysvInNrF3kOyi666rY/h6lLSoVKGBX6sqLa2GHMzo2pQP1TAx3bNj9GO2tVNYTyP8AMP0lK5Z3JGJM4khmBnjKqSxnmyZYZsBY7nMDHuXvnjXDYQpBvzdMtOIiI2iNoO6G8ikZaXbtujdh9oVVUq8iO5f1/q5f44+rVA0eBxvAkynPBm5oZXB0w9B8THfjHLzLnggYI99mKW9exeYFzaxQt/1LiYmN4neDATGRKNxABAYAY2H1e9NZROecLWWTyWWOU4oJTXoYqpjFm+ZljhyeTyzSXjBivWqpNCBUbScXr1DmxSsqNUuVimkaqeAffpmOrsPn5GVU66vIhG8gBeJGJgUJGdxAYn+pMRPzG+iroPySxmfZ1f8A7Q6Faw8AMD62aKnDPCIBnTGT4TOMsfSX7z/96u9T0q5SuvE2Wlns67clLFYjkOojjjL+EH/F2Rsd49VaMIOWGXNnq9APXIH8LxiQKCMpPUWnQvtDMCuxQQ6eW3bIcdQiIg1lOt34mzFulJe2rWF2UA9U7r/Fmc2rGhAL4ssx1BmlxDm1BJIdXVoj86s0Cb1eufCkFGi6ouFH5UEUzmM86Po/Ljnm2Tuy6Y6KvcZvDbjTiMWr/JnOoxdf/MlOv4ZV/wD5aTSBJTKzLgv3mNZ36BTIYzqGrdIUsjsWfWwyzz7SQ20qmAzzbPcZ6GYBHI5gYXaU1kgG8+hsBY8jKBibDneKwbCFMN+bplpixZFICUTPoIiP2xEejbqV+InmVdlhhTLA4B+zmImNp8wdc0l3K2kWAdG0fSbFgwZA43hFcklMQySWQiYyJRuM92kXj6662CwIMPt9chlKmOXzeX118fdy7Rt5XddYFgsIBYwATETG0+YAAAYEBgR1vEfPplHWEUHNqjzdjyogZtuMMWlkqkfZJHor1go3VXKYK/ZXEE1HEUthyhYPiP2t+tMx7lXhuDykZGruXh/4yLjG+0z6f+/9CN9vPz+Fz0oCWOMVhY6pxiigVc3yfU+QKYJNLYGdQ5o52XXAIfGUvf3liYCvSTXnkO5H/UMYYs1lG49LXCUxuMdPn0u5rHUt4a2CM+p7liOFCmUmx3UJRydbBJfxHqOI4w6J0mm9lkn3PqLUxE/Mb64B/wCmNRER8eP6Niop8bz9LMdnLVFoVcjPOuti2gLFlBh6NbYlvaSG2o328/Om11ukZZvOifXRHHeI13rLvCg7YhUCC5tmWm24pf0B+Yfbt2P1C7Qcl1j7NdfNsb7Rv8kYgMkU7Qd0znhWCSmQliuDPEpQlX6cfV+zPnwnhtyVbYsu3aHjPz5j4s15LZqfDl8+A9zbn6TETG0xvECIxsMREemQzcw2KOMGLFyvia1OJyOXbDrANvZpn5cnUxpsUod2HACTVAPMzERs9R45G8QUt1Y6lvWvy8entR7Nzpk7bzYYBfrzE1rjB0nqLI052vqh66TKOSRFsEjMCpYRsICMEAGPExgh6oiojt1kjMOqJJCBAp3n9rO207/HSET7qzI/Z+0mYiJmZ2ixmsZWjdlgJm51ZEz28emSIq9i4feyDSYSwWqOKghcfsbVdkmNmtPF9LqutNeIuiQ2Ludt5M5qUo7CK9TG1IiYV7pzLb2Dw5cV/sGLBgSBxuNW1dwzJJX51SlerXkw6uXIfWw6UhygZLQhbseWF2lrrIV5EY3ZbUJcQjuM7Vmx5cXbWpClRsA7ejGJTuZzAz7trZ2rrmYimTCg7J85OxXrxwHaZ/nLG/8Asrr1hRE8ZmZ/aMWDBkTjePzac/5ZXAxYMEE7xqxYahgzx3SBiYwQzuPo1q0rJrSgFsyOQzTTqY0ezVx2Lq45fFI7naxirdlbrBSxWVzV1t08fUL2y2YyDHkbzY4MWqI/MMikKFUPPDfUQIxxGIiPS+1ikxIDBR0/kAuU4GFwotTMREzM7RmbyrGWKwn6gid43/bXmwquXnYulqvZxvdKNi/adXWmLrprBO0Lp0krEQX3GCpQTuACM/tJq1yPnK4khERjYYgY/acLNCxFyjO2sVlq+TVJLjg3SvcywibsIaZdCC4KiWn2LD/1y4AtKlRsAxHo20lXiZ3Llcf9sdkF01DPI/zDbYSmNinzPu7Px+StNZSY+mNy/alEzExE7SuyxRdq14nwUf8AeDSxByyt8IsA6PHgpiCiYmN4VW7LJICnt6vX61BPesFtC03+oGQ2zvXxqEJrqFKQgF+mYwC75zZSfate8fUada6E9wLll07V68loMbmbPkt1C3prIguXqPk3H2qjZlN5s1nWHVE7SNlbRsXlsjsIGWn09hrNGSfZmILXVVw0UQSsuJVKK1iDDjkz9oZgAyRzAiy695duoM6x/TjrZC+6ye2ACsBAI4j+PM5VmNgCGImKubzi92xMvWPWCoXPOsUOPNZ6xPNUAgEZrOoj80AsCnqVsnA2KcrjH5VGQkoTE7fi6uqkddNodVramrGZKIPuL/8AVGiuVR+WRorrnl26YTOrVHJV0e7YRyNZ4vVBj8/8O+u1TYt05kH4bNKyIdtn5drT0y4OHKRhSVqHiEbejrSlTx+4+Nux9/5K1Vkq+0fqbZSrwRfVB2n/AGR2VpqrVPL7z1yHfjvG8zERvPiBekj4CcSX7QwBg8TjeEpFIyITO2rFblPdV9LqptNW7Y2LWXzK6EQlUd65SwzrDYv5gu6+71FQouKsQmbD6q4RBzRbC8f1BQvHComVO9GUqjWw5qQNggARsAwMelrEY64fcsIgmf8ATmH229vqpisfSnlXTAnqZiI3mdo6kvLu2wRWmGwETAjE/P7NrIWsmF8VkOvs7jpmQx+HBQiThiP6XUWKbkK4GiN3VLe38vYjttkRmd5iJn0syMV2bztrpCYkHR/n8RgDAJbBggZ0xiTLeFkvTOkqcz+WwhhHSuMXtLIN0yuhjq5NgASq5lLuYIkh+TSQhaA4BH/E2qx84s1p4PweX/iKSFsQNn0apjS27nBaq6k/YPnUlasMIB/KWmolXmI5F6Sm00phjOC01lJ8jG5NrLcUSe8wtKlfYMR+6zGXDHrha47lvE40afK9kmCV3JdSriJr43851BR1pJ7Ig7JsNhcjKSK/V4xFhMcSw2RjI0hbMx3v6N5lzK5ksa1vCutCU7wodo/aZRmyxTH3YbHLqVFSQxLv6edwg5AO9XiBtzUzyYgDRvrs5v57WhjNHPbhBxKumb1iIKwcxOHwq8Z3ChksP+gZisCM54g/qi08mLoV42YGTuDtctFIISCFwsPj/iimal1N1U8JrXEWQggKOXo6wpMfXPlLe6uD4yOmuWkeRztru2rPhQ9pYQQhAlPIpmBjcp2g7vIuFce4S2MUuStEMSix3+UwMwOksexpSQcFft8vllY1O+0HYOtZxlM8xZHu5Fdd18/dWmycqQpMbLGB9ZiJiYnzC2vxF2Hp37QdW0pj6lMkx6qrbSR1niFfqPEvKB7sqITExgwKCH8OeXNHNBcj7f2tBP8AEM2AT9Sv3XUV6KmOMY/UoI7NeN4+r/jJiCjaY3htV9ZsWqZTBYrKJyNcSGYh+iSozhhDElogA9uYwWiMFxucwMTcJk8awScxTNs8rJyUgsFxxAYGGV1MODOOUxERG0eI/cX7qqNU7LfMYam68+cxkPJ2ELsoNDY5A+u7D3/bGXcV+BiwYPA4ghWpSo2WMDqJmJ3idpfWRYHYwiCq3r2InivexToZWlkB3Qf1/g6xiOzVnbyqZlQTPz+yIxCORzAxYtm8oRU3mencdFOpLJ8s/cmYLAjOYEMnfLMXwBUT7X/jzQUMh9Y5TYxOZ9yXtLkQu5p1dzmeWcVduRV2wnaV0Y35PKWEIiMbDERDbiF+N+RQy2+foHsr/cmYLAmMKBAIPqHI8y8Y2IiIiIjaLVpVRJObMRF2wzKX+Y7zH9F1NLZ5R9DEZPNUth5RbVjc+q4UreuazYmJjeJ3jWcpIuUpFpws6BWgLsNCeH9YjAY3IoiDv1R/3N9Rf7k8UJY2RpZy1+mia4L6YuFPKyXOU4FgbRHbAQEQGAGNh/avtVq8bvaCo/jGL2mfdK2HJY4igRsqkt4mN4neH3jzl/2xu9vSYqoo+FQeKv8AkGVZs8IWXbsYfL+8ia1kZVe9G3djlSRlh+2e7zYZtC0JX9gRE/uuoXvsvTh6s7HTqpoVAQO0DkswYJIaEQbjr3LhQy+8jlalqHisYGP6dm0xDBjtyS1ZyERMrIxmOpck8dq1cN3Wcw1nNsCyUE8hmXBAT/QY1ao5MKBickJTMJUbJ7mSZP0LFcLxWcfO8FAwvprIlP512AgekkzP5tpp6DpXFj9/cZKcRjEbSusuJiIiNojaP2nxq71PQrMJS4J5t6iytmZ9ogUgVrqAZ7nuCKY6myZ15rQqPdJoCZS29JNYePoFEwCyCZxlbff6tpp2q886bzGaVUkwTGTu3/gXWntdC6fnR/xMRlkkMRRtTYCYPbuf1n1JyaRsVyheSw2U9+iRbHC1oQAZmRGIn92ZiAyZzxG1YxyrUWlrhtyxbfZndhePV3ekPyZiDjfaN/n+kxCWfeEFoREBgRjYf6NknCqZSPI8bhitFzeYk1OFrhEdwpPSqyE/prEf3V62NKoy0QyY2Ll7MxJlM1ccKKyxgUqEY/rEQjEkU7Qm0hxSKy3n9xbQ14iAHwBKVpCACNo0/HjO7ETKzXat1i2siRAqyl0zCy5T/UrvOu4Wh83hhNsM1j/rhLQcoHLnkH7zNvIKvbXtLA58Y57cv3hMYoZYreWYzqZDR7V6YS1bAaEMWUGH4bV2rTXLLDIWJ9XUoLZaWnFLqLHXGQqJJTPQ71JZwB2FiYMBkbgUFH9XNALMVaEvEY1xFjhVPx/VffSg5CYkibdtmMSlUgIU32Jhls52ZjFzMEkpWQsyNctiGWhOTkGSLFSMV76nn24iRL9z86BKllJAMDP9USICghnYv45Tr1RZYngVd67CQeqd1/u8wU37I42mPKwVVlTZDInf97ZqKsR5+kkov1o4VrhrCuzMgcunIBJ3brpr744lMeAdUOHfvV1a9h1JPzkAjX8IzBRMPyhCC8Hh0l3Lb5ssG7jK4QtO0DllYu8sjUqVWq+Qz6kikTGBmLlreL95nC+vHJiVVwMndK1WJFpnMxP9XLrlmMtBHziy3QQ/iN6VzsZiMnkaox4KTkclLJ4pQbCnJ8JkXJIC/iqNo+kt5IYHlM7C27WV4k4mQyFUy4wXGZQkj7khEn+CYifmN9CpQTuACM/i3neI28f8DMbxtqOG/wBYwY4+a/tFhWjiv91cKyNVk1BgrGAbSkGqr8mOs1lWV8GRq1UbVPicbj/wAkQ/bMxr3VmB4909iMy+4pL8LD4LI9Y5BGybJx4wwbViL+tm2knFWTH5xgxFbePn8DbLXsirRGWOodLVxDuZCZc5ONx6I2VXWOgAAjiAwMEIl90ROrvTmOtDPAPbsX0in4faYwVdOYhY8ezzm50xjrAj2YmudTF1qyAUQw0oQiPhYxo0JYPEwGRfhlF5QUhL6VlHkwmR/qQJT8RM6iu8vhZzqKVuf9ktRjrs/wC1OoxNyfkYjX8Ht/8AcNOrPROzQkf3VO2dVvKPILMGBBhO4/ufnVapXqLlVYIWGmqW4JWyOQ3aDKs8o+pX/DxEDEREbRUzNZNhGMgZI/6tyqu5WZWbvAEFrEuOvYXJD/Eyn7UFMIyKmnwOJWWrDGPYNKrHN2LxVfGpgFxyb/Vfj6r95kOJNwzhL8ooMYw1mfkgjUYVv+WjqMJ/6najCp/ywtRhqsfJHOoxNOP9MzqMZSj/AG99RRqR8JHUVq0fCgjUAA/AxH45iJjaY3h+Kqt8jHbJ+LtK8jHcGYmJ2mNp/b469Nc+2yfyYmJjePj94QiQyJRuN/GyndqY3V/w/tnose7ps7bqefWUor3Bldr+sxS2RxYMFGS6bqWxk0T2HR01mJntzZCF4rC18YMkM9x//BurIfGzQgtPw3+UHptWwouJrmJTi7bfMj2xTh0B5aUsksdTKNu1EaZhq8/YZBo8K+P0zEoPHXA+VTMEBh9wyP8AWgDn4GZ1Fd5fas51jCtCPZesoH99fxe+7q0eZjbxPz+z/wD1+yUlri4qGSmvhR3g7MxMgAgMAMbD/wA7MRMbTG8HSqM8kodzw9Uvtkg0eFZH6bILR4y4H+3yg1NXOxgQ/grU3WS2CNhViagDEHEsKMfSj4VGhq1h+FBGoWuPgIj/AIO9jRsbsVsLTAgKQOJEv+CWlrS4rGTmnieUc7MSOlrWoeKxgR/8FOpWZ96hnR4ioX28gleGSLORnJiIiAwIxED/AMPcortDv9rJxt2JmO3M6/ht3/7U6nG3Y/2p0VewH3LKNTEx4nx+8Ulri4qGSmth4jYrBbyta1DxWMCP/GRvPzG3/hZLA/uGC0dKof3KHR4mmXwMjo8KqfsYUaLCuj7GDOixVwfgYLRU7Q/Ki1IkP3RMf0VUrTduC52XhXTG7GCEnhrEfYYlo8bcD5XM6NLQ8GBD+JSWuLioZKa2HiNisFvpa1qHisYEf+D/AM7f+OzET4nzoqtY/uUE6LGUi/29tFhq8/aZDo8IX+hsTosRbH7eJaXh7JffIhCsPXHywiZK6yFfprEZ/CddB/esS0eMpF/t8dHhUz9jCHR4V0fYwS1Xwwx9Vgt9LWCx4rGBH8AS2TODGBD+kI8d/Mz+KP8A38f/ANCl93jPd25f8tO+3j5/5n//xABKEAABAwEFBAcFBAgEBgEFAQABAAIRIQMSMUFRECJhcQQygZGhscETIEJS0SNAYvAwM1BygpKi4UOywtIFFFNgcPHiNFSAk/KD/9oACAEBAA0/AP8A8dmiYRxHL/wYDEfoS0jwUnz/APBlHDt98VKOxjz+fD3nn+yAgf8AgyMuaArz2XgfP3A3Du2T6jYMSjgdgxQof/AU1RAPvkNPuPAEbA/6bIHmo2XT5IuP/gJpB9EWjy9+4PTYwwD7geNhBgIASNl0+Sr5nYIpzTgDH/emvuBHMIkT3q6K9imCiNhaYQkHv2XThyQJCBHmror2bDZ+uwO9T7gIRA24q6fJR67C4IAD3hiSjn72p/7nxHYgACiCJ0TXV/PYnAjvTXYc/wD0jiEKAIEH0RaPLa139vRYQnCY5ouJhU8wro8tjmkeex+WmxmZ2SPNXR5bINVepKhRsvDyPvuIHqgK89gguH54e51nDX/t/VHA/orYeOy1Ejn+Z2kU7EKd201/PegQVdHlsunwUR3U2V9xzyRy2U81dHlsKGACMDxTGiidWFeCiipQ7XGuwOlx0G0G6D+eXuXYA91m86PX3GiSj1eX/aoEjmE2QdpwKaZ9D71m4HsREjtVm6vJOEjax5/PhtLa9ydomgAdmwgqT57JPp71PMK6PL3CQo2U81dB8E53l74BPcnuJ9PfeYqhUq0cY5e4zetT5BDYaN5rEnn/ANpneYNjhCY7zVqIPP3nCEwkJ2aGe14DhtNJ2tmDnTY15GyT6bXYDbA81dHlsaPGmwuG2nmrkd4hGT47RUlHMbSI76KJ767GgnuUwI09yxEdqebo7U0AbTRo4lGr3akpoq7IcEBMDFWVTz/7RJgcztszvcQnCR27HxOlFZmU4A7BjB2HDZaCRzx2kxROEjtTmEefvObPlsD/AK7A/wCm0CI12MGHcqeYV0eWyY8TsLp8RsBhEgLdCujyWA7URJRp3oiT2122jq8goACIklOzTRG3BvMp+85Mrd1OwGSBntNG80/edKwaOKfvOPE/om4gZf8AYMxARw9xxgA5rFvMKBI47CIKsjA5bXAjvVm4jY4S3z2B0GMq47AYPntAnuqgI7qIyPec0jz2Ez+e9PMck1wKIlOFR37XxPYqeYV0eWwuPlss2yfz2oNPknOJ9E5yLggAFM0QoE939vVCg22Tacz/AO0DJGvuPOeybzuXuGg22Hi7ZY48TswbzKiT7j6NCisa7MGjUq0qf2ycEcPfOIK6zHenuWdRyzRFeeex2Gmx+49NNYRaPLZbDx/I2MMO5FGo2tgjsRFeyiGiePApjig/Y/PtjYTHlsD479jmD0THSZ0oqeaujyV0+vuU8wro8kGnyRJnY6gA0UKJ70XeoROI19xgnz2hWjqchteakZAIAkBNlxnXZN1vuMqB+LY6jeZRq7mdhqY2M3noYDUoiY02CpKbu2Y22WHE/to4HQqyMRw/Sv37P6bGmQ7YKjmERddzQJiNNlm4FOEpwhWRunlkphwGHbsII8Ewkd9UQYTSR6p3WKDgdjXfny2MN4c9jXA7Cz67IV0ICCe/Y+N7nsp5hXR5K6fJSUagZ7IHmro8lPr7rd0fns2kQO2iiT212Ocbg4BWdG+Wx7sfzzTt0dqiTzPuAwY12WdLManX3MGjirSpPBM3nc9r+sdAmiNlpQDghidT+2xQg4OCio4/oC2HjyjYcNlmbwRxGh9x2+xSBVESnAjvVmY7NlsIPMe4agfnnsa7+2wQfFFo8kIPcUWjy2iD4ogIiPPZdPkqjx2x5TsgeaujyV0+SBKI+uynmro8kBPnsca8co2ASnOTiaDM0jY939vXY7dHagBsApzNEantTN9ytDAhAVJT3c6J9GrFx4lO650ahQbDgMzss6u4nYBTiU4y0RFEMBqVaV5DYeq3Up2A+Uf9gnAe8yrVAkcdlrVnA6bCJa7U7GGHcijgU0ROy1q3nsZvA8k4SihUTwT9088NloCR57InuUR3UV0+SEjx2xPcro8EHRsIIQcRtg+uynmvZ49iunHtV4qPrskKB5JjY8hsaAe6uyI76I171ZN8fydjRPnsLrx2uN53IIJxhvJNEDRCpKG7Z8kyrUMBqVaVPAK0oOA1TBVxWFmDkFgOZT94+mxm9aFBMwZx47GiV8DcgP8AsB9bM7LOojRYO57MgmGCPdZvN7EceYVkZ7CiJCIpzFU3dPZsiYTPHYcUwy3kdhpezVmZ704V5rAnkf77C0jwQJHjsa7+3psInyhEEJpI9UHem0mRCcYHE7DI/Pfsp5q6PJBpgdikqStSi4eRQAQp47Gj0A2EgeKAAUx4nYxseWyzbH579rBcanbo7VEnmdjzvcAmiNllvP5polP6o0aid7iEBAVnVxyJ2ZDUq03ne+2l7j+xD9yGI9w4HL3GbzSsHDjstag6O2vMWmw4bHmL2Q2P3mIiCrMy3i07Dvt2Gh/PI+47ddtcIVmfNDAjFDYHHZe9Tsc2nd/bY15/PgsQU0QSmmK7A8bA/ZTzCujyV0ok+aveoWhRdhsLvU7HGB57HPCAwTjnTXY93589hdAPadjRKeS49qst53PYFaYcBsGA4q0N4oGXDXaRAKxceOyx7i73z+sdkAh+02658tuFoEajbZ1ajiND7lsYdwdsyOhVnRw14rUpwhWfVOrVZm8I0WDhxQq3mm7ruxWZE8k4SEww7i0o1BTTDuSKxB0KaIJQxKOBCG8OxCjuYV6sckap/W9fcm8Pz27H5bCI8xseJHntveg2MdLtdgeNhgeKDR5K6VJ81fGwG8e/+2wuUGBxRJMbC6veNpBAKGJ4lASnOJ2WhjsQEdyeZEaY7HurywQoBssqu0n330aM+axcdTsJAdwQxKBhh2fG/IBZnMlGoGyzJa1vzcf2izDiNpoQhvMPA+5a0OjXbBjy1Ry4o+BVnTmMimGDyTOuNWp4odCmUDtQrPHi1OEq1q3nstaO4HVOEJp3U4QrIx2IinNWZunlltIMJriPXY7eajUcwgADtOPHY9n58tgqSjmg71GwUlOoBrsJkxr7l8eR2AzIQpuiR3qFHqr/ANNjckBQapxmDj7jWz5+6QQAgKjZYjx9yzEN57BgOKtDeKFSUKNbx9wVJWFmNBsfRo04o9Z2qf1nJohOxPyhZnUp1Gjin9YnLhtOE/tRswee01AzRTMDq1HFYxss8Rq1OETstat4HTa/es/omVbqhRw4o4cCrKh4jbabr+ey2Hj7hN5v57dkgU4H3WkFEDvR3T+e1CpTgWyg4xxTXeaIB2Xo8veDh5FELQ7CKDtWmlVenxCmPdAknY1voPeGKgmvBPdKJgcTsfQDPmjV3M7Gbz9j6uOgQTqNbqoqOOwVtHDhkjRreCA3gNdE7qDQbXUY3Up9XH02AyJ12hN6jdR+0jDXt0RqD7jK8wEcRoUyrTrwQo5uh2NrCOIT96z+mzFp0Ks6OnPihihvMKFHDinmHt0OqOBXVtOSOCYagJwDm88UKO5hMIM+COPMKQOxOAPejuuPuET3bXAgIEwDomuCcPNB1dgE91UBHdROoEw3ufvXh6q6PJMx9xhkIGQQgIn3ASB2n+2wU8f7bGugcdkS4bCRenRHdaFEnmapgiz7c9gwGUjZlOEp5lxTRKtMOWxtGDTY6jBxTqvPFRAaclZmv4nbchmSj1G/KPdyGZ5IdSz157DjGXP9mWkNHA+6aEJ1bJx8tgMsdMmDttTT8Ltn+K3Uao4bLWrOB0TKtIxWDhx2Zt1pCNCF1rM6hW2PByNCp3RonCCrIwOIThCaIqrQXmjijiChkEcUKAIVHYhunmNvRXtJHy710XRoCFaNDgeYWk120MciojuoiZH57djgR3prvNB3uYWbMLzjkg2+2BBFQIEZbDkhQBT6lRA7So2OMNnE8gEcCPXZq0g+WwJ7vJBOd5f+1BgcUSSRsJgbWmRP6BxvOPBDDZjRDFMpZjZaYnRuqHidgqSh+qYfPYKkphq75uGzIalfBZ5AcdmRTqud+zn9TROoU6tm702CrToUzHiNdjOs3IhOEopmB+ZuqKtKsJwB2Nqw8QsHDiE74ePuWdW/ReTgmG67a2jwMwUago4DVWZqeCcPcK0GyzaXO5AL/iDHgRW64m+InkrC83o7xiDeukeN4LpRbacbr3Rvcx7pEE7ce4oEFXRXs2DEscHR3LoDXOdlN0f6iO5Wjfs2AQCJvO9PcfjKKAgbC0h0aBwmEQHOaflMRe+YFMZNq28GtjMuea1V37YAE2b3DGG03RqVYOAMCJBnEa0RaY7kRJ7Srp8U6TtAKdJ9w/Dnse6ByCAkqd3iNgqSnG6zkFhtf1zoE0IYDUq0qeA0T6N4cUMXHVWZl51OiGA2HRBZDMlf4bNOPujEoUMfskUMbRVp0KZR49Vi12YKGLtdlnjxanI4omQDlsZVrvRNo9uhQqIQ2W2OgdsYbxjREV557X/DkFaUfzRwKxaSZ7k4QVZmnEKzNU8eBQJLT7jDrjltADnAZtaQXeCNqRb6A2bxh/CrYAWl2okQGnkQuj2bGxNRcY5wk7LQkA+8cSgRPero8l7J1RyTmua4CcCIr6LptpvxEtZN3PLd8VYC0DnDCRu07T+gbvWT9HfQqyfUnNmIb3q0ID7RrSDbOFReccfJWwHtukEbxP4Z6rRqVbEutLUk36C8L3D67XHyqgBttCAOxAAd2y0MdyaIlAw2c8p2aBGhCGA2WpjsTRCshJHHY0K0ryGyywGTnbAbrAcgn9Y/KEMTqdjhIGw9VoxK+FmTUMSmnKhchTiURI2N6z9UNjshgBw/YGfvv640lGoO1nXHzNR9y0MsOQOnuChhN67fmajsOJ0ThLTxyVmbrhy2P3mbHdUa7DgdCrKnYrOvYnDBCjwNE8eBQmvasJzRFOeSYbrp2Wog89gdTgntLT2iFaODrFwkEPjdcNJbQqzaGtGNAICspfZAUEEBtf4XbA8e+cu1CaHmrSzc0ZVIgLo7w68ACTe3S0/yqwbdYDi5x+pVu4hpOYBlx7XbBi5xgIZtbA/qIWRJAnzTsBaCBOl7D3LKLtS01IHWFYTWFjLB2+3cpS91qLpDrr7a1oQMmtBFFa/rbZ2JmpA4bWmaIIuBMaDYwXjzx2MFRkfydoxJMBGoIwI92yF1vPYcTrGxm9aH0QVpQcBqhidTsdRrfVOq92x1GhP8AvhGpRwB+EIVJTT/ADFMGAQ/Vsy2Drv14Ifso0ITq2Tj5bD1DqNlqafhdsjdTPELI6FWdDxG19HcEVaHeHylGoKcEyrXZQnbtoPVHA7WVaURXYaPH12uEFNJg+5aAOHZjsZvBRXmE6s5TjssHC+4YxO67sKtGyRo4UI710hty059T6bRmfTVZPtDdE8hJVn12NYSADrBlfMyv9LoWbcHDm019yzaXOPACSra0c57jQBrf7kqxdAHD/cfBNENaMABssJDbOYLruMcT5Lo4+0t3AOhwy3gSa0xVj+vtwxsyNP7K2G5Zt3myB8Taxhqvhe6SWDAEHNqOGxwa0fxOA8kWXv5je9VYlzmTXO42OQ95tAdjnQPNNBKefAbbQMDMwC4kEwnVsHmchIgaEeKPVGLjyARpNRUZV2EQEauPFDElHAhGjRxT6uOaGA1KtKj8I02ZDUp2A+UbMhqUeo3QLIZlf4bDkNjeu7VDAbBiUKOObkFkMyjkdnmvg1I/Y+LToVZ0dx4ppkHYU3qn5m7AakZga7GdYahHEaHYaEJ36txy4FGhCd+rcfLacUyrDq3ZaGHA4NPvNMGf0BxQJM6bbRpa7k4QgbzDlLdB+IGVYPq7MNdTzhWjQTzwd4pmJ9ArM/Y2WToOHLUqxH2RaLoMVDfoV0SR0uwiBaDUNwyqFZC7aMc0G6T8s5GFZ73sh1mjO6c+Ss6WjAe5w4Hb0lwYAMboq76J4abRv4jh/USrNoBOpzPadgBIHJNMNLj1ZEkovlxivVp4yi8SSKkgV80XAVxa100HdCafZvPA1b4yrtx8/MzdJ7Y2OtGgdxKbZMmcoaEXNAPA3jHvkGFJJCeQPVACeaInhsrvGnxNiSmANvfiBLrp5gq1DSGDCzvGgPGMgnzaWg0Lsuwe4+pOUBBWWE5lDEqzO6PmOwVJTTFm07AhRjeSGA1K/w2cNTsHXfrwQ2DEpp7XFDAI9VoxKODcm7Mgsm5BZDM8kRuj9iMOHDaz+oaLMaHayrHeibR7fctfA7T4Ifq3fMEKtPFNMAnEpwkHLlsszXknCURTmrPdcOSmH6o1B2vxGW2zBc48AmuLOidEYYvk03tU4A2TCSHj94uouDgfX3RvNJzB2A3bQjO7Ud4kLpFnu/xCnaFYWhocg7+4KsCTbkYAg7/AHYDimANaBgAMNnSWEWwmAboNT3BdIBNvccC0F0zN3jBXJx/0p7Je26bsmb1KaSnUc04tcMRs/4cC8kYC4ATP8XkrJ0WTcpIIA/hG1sANNBLjFV0mWtsx+EkXp7wjNGC7XvKg3awJ4wVauve0AklwBcZdjBFQmvYedY9U20cBSKQD67HWh8v7pli6OBDU61M9jW/oW4EIVRN0cBss3EmcCQWuunnCtWh901gmqsTdZNQHA3WkTwbscYAHvHEpu0YAIVJTcG6lDAJh3Ga7PjfkAs+J2sNGaoUAR7m80cXHLl7hpAwCybkNoEuOQ2BA7p1++nAZ7CIJ2GgnY79a31RqCsHtzHFHApvXGoR2DJZHQpmB1G0Va7QplHDXjsZVpGPJEVRoUa2ew0tAPNPHmgTEae9auazs6x8l0RoY0zSYG851BkjVzRLz/M4+i+GWiBzqmVuNmf/ANZkFYNFbryMccDw2zDuX5Oy0EA6HI9hXR3ktac25xyPmrVl6yAwvHeb2VK6SZBONwYd5qmm6bZwLbNrgYIrE96xLLMmAP4bo8U4br2kQe3eTIvPe92Zj4YTwHNJLiCDUGpV4igEBpLmlo4Ik3Zwv0iP3mpwLbEavOfZiv8AiZD7XG8GTus7Sapol5HxOOJ22rr5aP5WjtJVoW2RdButgC843e9MBc4nAAJj7rXO+IRKeGUiKG8x3grGyeB0YmL7mG6DORmqYb7rEPIcdXXRogSx4GF4ZjvXtHGeQCcGs3SJ3iJEck8G0MZ3jI8NjgXNY0SSPJCCcJaOLnegTus289w9ESRuXmBhy+LNUDbcCn8Y9UcDtKNTsfaNBdpAJVnYNcOd2neV0hxeTnHVHlsscOfuPoNvxn5R7jOs7VDAJ1GN4rMr436BZnMoYkrCduDrTII9ZxxKd1WhEzyCGazecSs3HEoYkoCbxw2FfC3UrFtmtPvTBvNyLUctDsZlqEKOGh2/CdCmdU6hHEJ5mzdpwRxCeaH5Tsed4fKUagpnWHBFNyGewYlHApmI1CzGh9xmmewiCm9Q6jbQ2jSYDW/Uq0aHAY9YTsZgM3E4AJhutYDjHwt9Sm4NaI2kw1rauJHBWO9a2QaQ57Zi+BqFY0eNRk767BUTtvez6RGYIif5ZVh+stQKuANTPHAd6aAGgZAKwcAw5uFQXd4Vq1zCRiLwhWYi8aTJn1TDv2RlhJacsJVkfZgEEDdpScQraXOOQFoM/wCJNoy1aZa6PhcWrojYYC01dMhz9ezFdGIFkSCLxHV/3bWNLjFTAEro8ezbofgb5lPN11vaGrQaTQUVpe9ob14Q6haKzCbJipqeaY0BgANXAmhu81060c61tThZMo4C6ZiSSukWbrMWdA4PdSW0FAnF1oS6gice4Lo5M2x+IuiY7kwQ95Ju0+IkzCpAPVZTqt4Jgkn0HFWYkt0aOq2R8xr3ptQ1r7xnlN4IYVAd/pKtLN11rxu0F6bxjNWLyLO1GjhIDtRKZLbEOyc34QdCMPeNowVyzon2di08oafRMs2g84REToiZJ9zBo0G13WO13WchsiJRzRq48djcBmUMAhUlYOfm7lsynBOxcfRGl0eqysxgEMlkMysWszPNDIbZ3RkPvr+u0ZFGo2O/WtHmjUFASBqhRzdEyrD6JtHt4o4HQpvVPzBFD9W7UI0ITq2btlpj+E7SjVr9Oey06w0KcJHan0Og91tCeGxohg1eeqO9dIex9o51XNk0bwpim2TJ/lGww5/N015wEwQ0DYBeLLwkDWFMSwCOy8QnEvsrJ+pcXOocYTi0Wtkw4FxukcJBwVod8fDfPWaeDlaNDhONcjtbi5xgeKaftbdwoYNDy0GKxe84udqVBPSLsi0dGQOhRLWg0ykxrmhiSmOlzHRDxpJVoIujBoMCABieSvXejiIMDGSrAXYcLzXQLgx1q5OJvC6Sb/HmETLIiQMDAb6lNklxxcTmdtoxzRzcCEwm0s7MiQS2GvFKyMVNXFsNcMq6804Xdx5eY4NBKB+1t3Q0NpSBWU3FxTGkWTbTG1/FdLSrKpaXXqDIuwA5KzIBIobSMgMhwT6OjIYmTyqUYNo/N7tTssjNoRg5404BdKN0MibQzIkGKUnNPLHGojfF4XYzCc0B7KhrXZk/2RBLSDLTAkgymvaO4vCsC2+4Y3Z3TzBREPbo8Yj3XWjQPEpxsgeEMKAA2Whhk59y/fb9Ufxj6rCQ9uPeuBn3tXEAeKPUukEvOjdU8xZml58ZyfRObLicandmOHuhCrW5uQwHuHE4nZnoOa/pHLY7AIiLgwG3ijhNPvxxCed13ynTYeu3U7G9YfMFmNCmVYfRNo5qbVp9E2jxxXwnQqzz4IodRxzCs8uCOI47dDXYU3Cfe6CfaWsVDrQZdmHerS1aB2AlWTBeOQDQrKr7SSN0ZwGmO1OeG2RNbgg0aeVNliXXWiohrroAFO9BwaQ4iodSkAK2ZeNoQJLnVGOitLQNdbMN7ednkplr3CLvISa8UWEtJwDhVp71Ym812V12XftspLSBIrGIpoi6LOxaC0Occ+torQl7Q8yWtOAr3ogieaeQ12QiaWg7FaiQ5p7nAhDqhxLSOyHJpM2YkgjS8YjsCdFmRZ4MHyCMynb9qfxHLsTxB1ByI5J7qOblPy6HgUdMRzGXuA3nsbRwcPibrOYXxWtiTZuJ4twQO620cCO26QmGXOtPiANQ3q+SaQQ9zdwHVt4VIRM3Xk/3QMuAdQxnElOoxgy4wukQXzi0fLz12WkttXNxb+EHzVq+/avzcWtju3kGMuWjjduvih3o1VmZZddekH5Wqxn2d8SL10XS7zVkBSIkRdJ4Y0TnMMDASXmArRpa7kRCs3l7Ty3HeQ91lo0nxb6oeyd/Td9U9jXDtE7PZY4hvWdqiYZbWRBZ5fnRalxbPO+1DG7aMd6KKXodX+G6hAc1z57LjyfBAlto3EBw+qz29Cs3mzAE3nN4fiPguiS67q59Qxv7xCaY6PYDCGmgj5R47chmUeoM+1BDqWevE7dG5bdSs3HrO5LM5n3dRs49Uch+wChWzdqPcP61nqjUFDr8dhpaN1CIkbW1afRZrQU2RTmgSDlhsOG1glzjgAE9xbZ2hIg6SMp2MAug4SSG+qtbTruHWaBSO0lG0vvbwkR5FPaWzzEJxJsHWnUc+LsV1GCNLJ7QAMKCGUjiEMRoibzhUAu1BGE5phoxpknuAA5qxEWTm/CKCK8kzea60IAbFZpCsIuWgxcZidIOStMf3QrRr2jLO+PAbSJZZCrnfRMrY2JIgjEdms48k0TDaiBo7A7AYsrX4pPw8QmTDCJbBrIIqE4fZlpLgT/LgnENcbOA8zX5iQrN14vOJBoaeaG7aM+Vw9CrNhc2a1ATxDrpIa4Z0IPmpo2zLjdGQLqINHtCNezYwS5xoAAmmDbDruA4/RMbD3OAc58CLx1KZEENAdjrCMx7QlwxylDJEwKSom8rM/YzgXjD+Xz2W0w84NA041VqftHuaTA0BEqyJa7dcG3XZyeITWtBPyWrBmOKsY9iCd1oGnoQnMO9cF9mYqwAqzY5rCWhoeQ0wZGKfaxOe60fXZ0oC9kJfLT/AFCfdaGuHY4JllP/AOp1fBqY32Z5s3djNxs1yDP9KOMVB7CuRs/8kBZXXBzcMpBKy3R9VaxeJAAF4wKeKFTmSTmTtYJc44AK0vBrngAuJcCI5VTrUOtSwR9qd2Kc6pg3nauNTHCdowacAshmV8FnltdRz56oR6zjidhwYPVZM+EIICS4YbRiV4uQw+/uoJ2EUKbVpOYVnVp9Fg4aHaVaGWu+U6bSIITv1Z0nYDv6wjXYASBqhQj3AQWnis0wEuccAArF26MDaxn+cFYAOb7P42jzOcqz3LYfiGfare0A7G1+is2hrRwAhdGYWNLjdF6IivFx2Wf6u07cCmNLrO0O+5rQK1EHDVdJM2pMzTJDFpxCI3SfECcE2ntHWbyTGt2iNCw7sj9xtT2rF5FXOdx0QcJjENwVm433NiYi6aDRPMYVBGTtNkQ67BBAwxVmC50w0QNSmlzLJtcT9GqNyzHWeeAR/VWZwa3JXXeSsi5pDiAcb2BxxTLQOIaDEQRjEZp4mDxXRQ5/smVNoPkTpY+wtCA6tKaq3O7acPm5jMJwkEYEHPa+HW50HWAPIV7lYWZdGE3ROKa4tLQZ0Mp8dhkUVwbBGU5hdJcC8gjdumDemqsxDRs0cAR4oVcbjZJ0FMVaENsuj2dGwMCQKTqU9sPu1DmE9ZvKERIye0YcHBfic4jzVy42yYRQEhpwoKHBPl/8xkeGwOME4brmuFO3YTdNCWtOEUxKsny9ljIvNJaGy4HIyulWdmTaz8Tt3trirRjg3nGCs3Oa9p0fX1XRrQvbZmQ13w3hFdFdIFq3eI4ks9QnEQYpRcDOzUUWjqpkWdm51BedQR/C0o4OaZB7Rt6MZ6S8YOcMvQd6dDAGClmDpx8sU/8AWWmn4W+78LdPeHWtMhyR6zziUKlDrHMoI0E7NDs1yCcOqMB98OmSOBRz0Teo75m7GVY70TaPbxTqWjfVGoKcMUzB+UI+CHUd8wQImdE4ShVp0KYYBOfuvo8aJokuOAGq0Y0n0WTXAtJ5XgNvRyD0h7TR50HkO9FoaGtpAGEJkP8AZybrmumlcFaAl3yhxdLQOVVZWsH+IT/pTwHNcMwVDrWzBjd+Ig6poc2eDXEDwVtu2QxjV3YrVzZYSIaCCSDmSmkm7nBzQWYAp5rV30C+UUHhsaCHUx1AVrvWQObohze0LpkGz+VriadxptcA5zgOu6cOxNAHtrQy1up6oE/mFavDC53VwJMBNAACcIJGK44dwXIbOlNLTzOHi1Gri0S1x1IpVNMtDnuidYIRdLGtktYOBOu0uINvZmakyngue4UN1omCdNUHEEailFcvci0hycwEcRsLTA5Jlo5vk712sqT4Lowd7GyFC9wGGdde5PMSDF0fLF05pjSWOB3hEXoJEg8FNLwk97S1OEOi8ZGnWXSLTetDV7o3anm5MaGjkBGyQZieu4N9E/7OyIyc7PsxXSJc5x612d0eqsRvB4Ba9p4dq6Oy5YdHjdLwDcDdMVaEus4F03DhI/NE+85vY6R/S5dKHs7Q6GjT6FWTC4DUgUCBtCy4269zRkC2DjRO/U2Tjff24IHqtcD4PhZG46B21CdhqCnWntKAEYBsySDKcT7Rr63WfDP4tlvQQf1YPxH88VbQXkbxvOzj/KFazekzcnHtOewiW2TauI10CtBPsid8RiCPcdgNjjE6LFziUOs/N3JDbwWTcym1DczsyGa/qcvPZp97OITqwTVp2MqxybR7eOxvXb8wThUJ5F3hO04DNNq13FNo8arGvuO3geO04HQp7CwngaeCtaWds8S0QaOHA5rFpb1Dzb9E2Ay2cQaHAOIy496tB7OzPF+fdKth7V51vYdw2PFxxOGJH+paCiawuBzDh1Y7U9pFnZEk1nrAYBWjfZtHF9FYkm0BEddxIhdDaHOGUje8yF0FzbFo1LiQT3jyTmsNpZjK8BXkUYDycQT6LVagym1JQMHmiZtG2ZEzmJ5qwPtWNsgR9kTImcSMUwFzYH+I0Q5vJysN20vYkfCe73HOdTOZYPesLQE94PonAEEZg+41pJPISn2pkPF4Ua3VAus7Foo1oIM+EIPPkFaWbgR2YpgcGniHGiFCEUy0Dv5hH+nbYGbZ7cyOsezAcVaObZw0wbNp45EqAHviC4gYlPZGG650FpFM4hOJLWESe6RdCOJDiS0cN2R2LoUudaPOL4/3FWYJa53xBpg0yVrFwN612d48KKxN4OdvENYb8SrAl9z5hFY4qzaGNcHRMUEtjRdIkNsyCDBMkwagUomuPsLMkgTS8TwTmglhxbIw7E4hlrGcY97VZ/as/hFe8Jz22NrWst3p7Q1ezDu1296qxN60ZMOAkG80DPLYyH2jx1XCd6TypC4hWdu665zWzDssOCNSGgDyVpRlm2tyc3LplSTWCTXsr2lWhvbx6k+sd2xgLnHQCqcbtlZnCOqEXTZNAyVm77UvGF7ADu2k0Jy2BDqszdxKGAHuZuyCOZwHJDElDG1Pos3O2xScEcXH0+8uoB7mp2NyycNEKObmDstDUfKVkUBJOyzy1CIqNCm9aMwh1hoUagfoBVjx1mngujktsyfiY3KeHkrNpdYlsXrw+HkVZmWWdtIqNBE5qzbdJbhTDHhsYXEPyvbtPBWjA5zeJCc9jTymfRCzYB/KEaua4SKVRdDAMABedA5AhWhIszoXGfNwVt9uc6NII8GprTZuBr1TdjuRqy0aIgjhogLvtBBBB/ESrYVAMjlIzCtMWxJOcBEfaC6ceEOVo8h9q4xQDANwUXY4RCs3X7Pl1fEXV03LKXn/AHj3LOzDu2XO+iGZWjd7yRwddIHfVauIJ9Voxv8A/Ka0uax3VJAkDFNeWkPPUAjdbjSPccy4I1fu+qLHWkcHS4eCNo7/ACtQcPJFpHgr5unQmEOuNRrscCcMCx0V/m2WwLLJudaF3YuliYiSxs0nl5rpBbb2xdN4Sb0Gc4bsY03DjvGjceJVpL5IAN0ndw4KyaXkawJhf8QtItH13LPGZ43jPBMYLN17Nzmtc6I/EulOu2bLWTcs43t3U4RkrWri6pAxujgNlsYs3DrADrGeSuXnudQwa73ILozhdBzDeoIOuJ2AX7M/ibXxwVgbjpxu/D4K0cIAPVcQaR2I2TANKALo5uPIlsxgD9VZwGWhAF41wLaFOcGhrceJ7ExhfzpRWloSXOMCGgVQpadKdRoH4Z/Oi6QD9o6pk/EZwE96fvMafgnXjwy2DEldJIvEEQLPieKshetHA0MCSmEMsLMaj6JzmEf1n3D1WjEodSzy7dgxJQ6z9n9TtmTRivhsx6oYAbRiSjs1NEfugqSjgdjeqfRMo4HPjtxB4r/DdkRpsHXb8wR8CjiE79U4+SOIVpPYPcNTCYanhsNLOzmrj9Fi1rJYD3b3ehQl2P8AOPVN69mes0/RHqsFXOPAJpAa9+D+XLYyGktE7wNLw0Mwsx7gghsExPEClE1t4PaJvAcFQwYJABkOGvFWbQwOOJuiKq0Y5gdjF4RKbZ2l2gEvI3SDjUK1eC7jLi4eACNiwEagtEqxtC4d9w/5drhBaaggqxtetFS0yB5bLF7XDt3T5pzG3uYEHDiultFm7t3PChVhaY6TUeLVatDo0kbbMXC59A0NAafFCvsr0D+Vu8Vk6zswJ/idvL5rV17wbC0smDzNVwcW+qz+0KtRes5Obajvb7lu4Odym63xr2IWdxuXw3QrIl0Y9ZuH9Ke6QTgaYSoPVBKF0ycpkJtBd3yRhW7oiT9taDdbyyHaV0o/aluG+6HN44oVJK6AAW53oMMA/edVNd9m3K/H+kLozTx6rbnm7Z0m2a3sCAgDknQW1gGDN13BOaZjAZhjVaWwYxxHUJ33Fuh4pwZeGbnEsvHt29ExIwN0/wCp3gukkBwGN0mI/iKAm0cPiecTt6Q0Xmj4A86cHDuThLD+JtWpriLG9xxZzlWji60shjE3i0jMcqr4gS6P8kpnUsYgkAzF04cSVakF7W5N+FtMyVZmfYsMl5xLjln/AGR3WsZUNc7Nx+I8E+oa6tw6nU+SAkk0ACJj2kS3+FufNRIsm75p+FpDQrGmGLjgFaAkDOJhdFYS1mrjJqrZ7nTqBujy2nIZc0e5u06L4WDHtXwsyahiSsDaH0RxecUMypq9wpsyY2pQqLIeqHcs3ZBaCgCFB90IghPOPyna3xGiFHN0OwGGnVDquzBQ6p+YbLTrgZHVHAhZHQpn9Q2OBE80asJ92wcW2YOAAJDR67P8R5qxo+qpfYPiqCAQ3I6IkBlk+WtJ0JyjRNENaBAA5bLR92/EwIJpNE8CtauImgEp4lrhUEFWjwy/Q3RBOfJWjAXR82BTg0F1C51IqHHHkg0AsxA1Csbot7NogEYTTXAq0Ej6dmyyY1s8XOs2ehTfZE9rTj3o2TK/whdMENOUuj/U33On2d08HN//AJ8dj2OgfiAlvimPJuk1uujBWRNoBldkerVZMvBodMkbwIwzarBxEHJrqt9dtq8+0LSQ50kuinND4nVPvWbgCfH6p4DmngRI29GvBpOAjcbHiVBNwSYisE4SiHktGALXx6pjA6LOG1DWcNSubf8AatJaP9KFQXku8DRDAAQPBWVsBPPe/wBK9mY5HreCtrS+9zaueAN1sRkaqwsi+0A+YC85W74nlvO8XbLCzfbEcTQeW1rSe4J3SBd4nd9EQwmsYmz2QGs5uMT2J82jzAwGHgmm7ZE/N/8AFu1olxOAAVkQA35o6rfU7GjrRR8YXowjVCA0kG0bHBza964h+WKeboeAQR/G5CTJqAT8s1c5YO6Q6WkjMSPIL4rZwrP4dNjv17vGDwAqUTL7QgAun02P6zSBB5oCAw0EaBPcGXODiC0KyY1s6kCp2uxKf1bNsTGpnAK0EgHEQYI2P6vDinVc5HqtGJXw2Q9UO5Zu+ELT4R2LJox7kc/iKzccStBh3rKzbghkPu7qJogTt+NnzBZjMFDAjHYOq4YgodV2TgihQbGVEZ8EOs3Y3qlDE+453tmcRJPk/wAF/i2g+Eac0es4Vrmf7oia1azlx4p1faNmC78TcE0xY2xwM4AnQ5Hs2WUWg/hNfCVZQBJirNw94TAS4TMXiTCdiOOoKswGtGgCcbvsDgKZVwpoiQO8p4LXDUEQVYOL7I/hmD6FCjG/M84BWhv2rcwGm+D21QYLjZxNmbseCsq2U0dE7zY4KweADwd/cBWjQ4fxCdvRbQOb+7n4gJwl1k2sH95GhfF495oFaWjZIwBvBPa1oLBMEaoWV03c3lrXx30VsWizBzDb0xwrss2Od3BOeT5D34lvMVXR5oc2TQ9kxsa0kZ1ATXSdTTdHaSnWjjBgE6u3qEEkhWgu3Ab01DrxITQWTyc1o/ypuLoJx5LW4+P8qPVs2MdJOlQE5oJYcWkjAqyc1/jd9VaWDSTwcBe8JTnh3SLYTLf7DGitiLPsNT4BBgLv3nbzvE7Oj2DbOeLiHfXb7J47xCtOkGDymqu2U6RLI9dltaBxGfyN8ynxZsbEc89Ai2+7m83vXb0kgvjJk59yYKnU5k89ntAG+zMTSd5Or7N8h8ZyGtBpsFSVYi7ZtMgY7opHMptA1ogDsGyzFBq40aO9dKMtdndklx7SrNt72mRoDh27DgRUbAwPziQyju/3GAlzjkArQ+y6KyY3qVp8jfFXZ9k5wvS8lwEduzVZu+EI4uOXJHBgxWVk31QyCjdnVHWoCPVaM1/0249q1z2DMqaTnxWDWDzP3r42ZELMZg7G4hCrXZgpuB+Ya7DALcgjUFOxPv8AR2m+RS9ZkQe4K26zxiSfz2Jj7tq/EuzGy0M2YJJMk4kHDsVnZm0s4FC0VuE8cQujkNcTi4Hqk8aJ4LXDgVYuvNbpUtf5D3OjwTaTUuIqPFWZDieOQ2Wgo+YMACGFo5Loz4Acd5syMcaGV0yxcy64yQHA4/xNXR7QtrW6HQbxHCU2j32dfaOEXXBuqcwEDK+N6O9WIIY4CZAwaeK/FDQO4lHNo/1OWklx+i1dXwwQyFAm2kzyLTsEXgDAdd+YJohrRQADZbXdzO5NT4JuDudahHAj3PlbUo4F1T3BTi1uA45DtVpR7g7eIx/w/qmCDaO6zqzXHY90ua6bzQTUNpB4JghoGiaCT2K1eJPO84+aNCDUIOaHF1m0w00pTVPYHOc1oBM1yA2ezJ7qo2d1wNQYJaU7EjHvNU1ptJ1JN3whCl4gOB9QnUF10H1Vs8XTExdLt3uWhMeey5FOJAQt3V0m8PRezs6aVZs6MHCzGUsIYPOU60NP5R6oCAOGxgLnHQCpVs4tsQcmfmm299naMbfBblN6ieJBgggOGhw2dJNxoGN34vojvWp1ece7Da43ngTG9Rs8qlWbQ1o4AQmXmCya0GW/Acucp8CytTi4YzHYgS4uiMdBVOhrHPcG4mDdnNOLQHMcTLThD5qnNqdYJAPbtaZt7UYGMewZaldHsy51oAYDp3nPIzpKs3Av6QZxFRE1JRwaMSsrMYnmgsDanDsRxecT7owOaz2aZDmsm/CPvDRJhYEHEKQZQFSm4hf4jMiFmMwUDLm6oiSNE2rHJvWb6o4ptB7rBLnHIIugWkyY1LY9U8AiuTqidO1OsnkEGQZGKc4EE0kGR4rpQMmSBe+HkAU4Oc28d2/GDZrAVsLtn0RhkMrILooF0gSQf8NuMcE1xJdJALXON0Q2dEKmDLuzB3mng/8ANdHIIh0w4E4V81aAOaeaYYeGkEg8YVm0uPGMu1dKeXeKxcdSrUFzC4yCesC2cJANNltZ341cbrvUplnMnAgB5nvK6SXBzW4PY4ERX4myhFo12dd0jYWue0gmA4Vo3CqkyYE47B8LalGS5z5m6McFoKBMtmyc4IP0Rw9xrboe2CImagoGBaNy55t8lX7Nxh3YcCh8RBHlRHmfojS6IvH+FqtXAMtHUJy7AmsJBJLWBxoIAgmpzXSN9xzu/CO6vvNs3RziB4q2tHOHIQz02Wri537rP7leyaO4RsNk+n8JTS9v9ROxrm3HEVqagHkgwAmQHTCYTBNSU8bsUFTx5IcAf8tV+Eub6K0gOLqmhnGAi8WjOEvjyTbwDjjAKIQv73/+is7Qgu0vD+ytGB3eJ2WkCpikiV0aLO1cd1st60ZzKtGhw7U8FpIoaiKISRZkb0nDHDv29Bbjleb/APM+HuWYcGzl8DfCV0r7Rto+oaTUE+LUWhoL4o0ScXDimC8+2aPs5+XYCblkwndaa78Kx6tkyKDCAASZOpTAGtaMgEMSv8XpBkS3PkPNOq55j2j3Ybo8lZGS0EmTM4mrneCb2kk4klZTgEcGjFZWQ9UMBsynBHKd0bBkEeqM447cCR96cZIy2ipGqFHN0K/xGahZjMFN0RTeq4Z8Ezrae5bfq2ATE0vEeScJNmS66DpmPBAgvtQA1rozLpg8gmNHsi4DrCKzqYxQaTAf1HgUa2OtXFD2lnZTo5uHeV0EuF1zYLmhxfIBGUpo+0s/VvBWUhpiRB4SE3edb2mDeLZwVpuPtAMRo3hqVab1qRhOTRyXRzD7wiTJG6MTgumyH2tSakzdd8LhjCt2n2NsKtbf6to0/Kc1ZSX2nwuEYNdg6UHzbOGEjLjCgDkNNnRCHEjNgM15fVWgBHDUHkgxt21bQYEGXGiNGWsASTUA3aGciEa3XCRKtIvEEnDmTsNCrB95oAq5pzr2LV1VMh1rDrvIYIB8Dm5p2MLXjscPROs2zziD7toxze0iisrQtAd1SIBGFQogEFrx41VnAeaMxnQcFjcEhs5ycSrOzcQ0CAAL0QOxdItWta3WK+cJjQ0cgI962eBGd1u8SO2E1gLhxdvHz2dD6O81wm7Pm4Jt5p7HHYWkd4QtHjy2BocOwgq0s2OcQTEkCUMCDdPjKZRpeL8dtUPlMHxWor9EXCjhFE8AESAN1rQJ710Joc5rDJLWi686xWVY/ZvnE3RQ9ytGOc3iDdciwlreIrTig4t/5dwLeqSYIiocComzeTIfS92Uw2WjXQdCXXh4lWT3BrbpJumvZVaPBZ4uACZ+u6Y4Ua3MicPNNlkuJ+0dxmcFZtLo1OQ710m0Jvatb/8AKdjcXOMBSRetMDpAbWq6SGn2gq0E1BnSsFCrXA+TmrUOdP8AmQk456klO3TatF4T+DXmrUfaPJmATMD12mlu9uHEXtBmnwYiZc7N13wCOFiDQDQlvkE2jWtEAIZlZ2h9EcXnFYcAuGHvipK+b4nLzRyH3c4CanYR19k14BHEaFDrtycFmNCv8Rmo1R6zTkj1mHYcdlubrTm1o6xCNT1i0HP4pVwWTXPrJaILgAJkhWhAayybeNdZhXTcLsA6KSrMlptLHVuNcPJWryGttBUA5kNpRNENaBAA2Wn2j7MUFm0f4kjCuSGBL2PB/wD2VU1s7140/C3dRgk/GaZuzWYzHNOcB0hgJBLpx/iwXSGzbWEkub+NvFvhmmk/8pandfHySD3BAkF73AugG6boAEK0eGe1iXFxzOgGx9leFK3qmZ7E8FrhqDQqxd7SyOrXRPodnRnA3s7rjHnCtWNfH7wn3OlD/l7Tg74T5bekWdP5R/s2Os3jwKY57f6p9feLvaM5A/RwTQT3K0tT3AD67DYOgfzqya60I47x/wBPvt3ncA418G7bQlg5OL0y1cI5hp2EFWdrI/iA+mw2bo5gSmtufyEt9E0wQXtoe9aMN89zZTxu32w6JjBwX4T9VbWl3eGhb9VYSHAiQQ4io40TWuZZ36Xg9l29hUVT3XgTTdYKuVgXMLmYOp1XTpKwAGZU7tnkD9VZtdcY2jpg3d7KqvuIa6himXNPJFm7JwLpp2AK1m1Nlab10ONS0EappvF9mA2gxBVA5zcS29DyeKspZaA/P1ie2VaOY0d970Qs2k8yJPmsGNGLnaBY2Vg2jQE2eqBdEITdd8pKmA8zZ3h80Amic2WuPs5u4SXH1qrNs3Gm+6SbrRSGgTonEm1tcHxkBTJAw4QQQeIOzpFDBqGDHvwXSA02gFbriM+WQ7VayXOxuziAddTtbghgBsJk7AJJOAAVk4A2rTdc9xmK6UwTm1ccTBIB7Rsz0HNO+EYBaLJoxR/mIWbjj93HVcEMxosjoUf1b8o0RxTsG6bHfrGDLiEcEZhuQnaKknABN61o2AOwHFWG88vA1vGYkVuwmu3oh5ujG6Mymi7ZdHILRzjIJ1C6pPeZ2Wzo6RZfDP8AfLirYB14NDSZ1jbaWvs7Mn5GfkIuue1EXb+Ed+wtHmUMwrUXSRgZ8inm9Y2hwrgeTvNWRJsyGihNZCtT7PpMZSInu8lZRasI/Dj/AEkpzAH/ALwo7xVbK0OTQZr3OPdstj7C2nCDh+eGz2Tj2gT6JoLf5XEe5Yxa2cYyzHwlPaL/AAcKOHfsJNm93CYM9j9jhB5FWNoXN5dU+Q97pTBZv4Sbn0KunyQtHT3DZYWFO2f9y6PY3Tzp6u95uLWbx5JpHspxAaIWhNx3+b0Qad82kxxiFbPLxObYDR5KztqDgS4f6drbQHxeNhaR4Jto8Dz9U4kuqQCTWYBhWLrtmHAOukuMRPAbQXny2DC8AY704ht58ABppTirQh0Bt0B0bx7U43mzoMV8x9FBuzhOUrqltmTAikYJg9ox4hwuGPiHNWbLrgKgjqimquxumd18t705rmMmLz32mJjhKt3X2t/CN0HtTXkvBIBwgGvamgADknEPdGLNI0OvBdIN0WIG8HEVu8k7ft3j5jg3k3Zo8TGdCj8AJu+cp4uuIkkt03iU6jnEST2rpMMHNx3T2GmzoIiyBFCZLbL+reXSJc1zsQ12fN3vNMOtD6I1LjqrSDJaDN43Whs8Va2gsy8kXhJgs3aYqydPSLQfFaRLz2BMAa1oyA2OMnmuKztHYdiOLj6ffMjmCvgfk4KfBEVCOStD/KdjcY2FhHfQ+Cktt3RvTMgE6RCtgQ51mA0y4RKs3h3fLT/lVmbr7uRTmwx0xXnxV5zom8WgnCU9puz8wq3xXRCX72IacRHB3mvmgDwJUbrLSkng6oQtHXhnWF0b7V0UOALrvGZK6tqBk8Y9+KqPJDFxwCzecVZk71mLzbpxbTFdGJs3g4x8Mz3ditBAOjvhPYV0UmxtGnNuA+isLQus5zadO6UResz+JtQrA+ytBnTA9oVjFq3+HHwlPY0uP4o3vFXHXuUJlo8Dwd6+64+26MDocQPzlssftWfw9b+mU0Bj/wB5tD347Ol2YbORMfVo96ytRHIif9KcJHIr2pcGmcIinctQx0eS6RZSx2RFz/4q1LjZueQAd8mJK+cmnYuJu/5oRwuuB8tlq0l7m0LjMRPBH4pp4I/hC1afrKeY9m45BPa0uDzIaY6rRkFavk9u8PNcv7rUiF0iXBv4r0x47IK9s7ybstLd3OABtr57WG9auMwHRPg1WTYL3mSQMyU37OyBEE8fFCpKsiA0uqHSKeScZcTOKtG3SBk0xhxBCtWkODSA4AjGDgsXNEOtHHK9GCNGBom63n5lOIvWb6j2f4YkT3bHbtjZ5ucfoukGSMXEmtxvquij7Ng6rXHqgD81TjJ9yIB4miebwmsBXmkjhflNs3RzIgeK6fbyDqyz3B/U5WLQwhgkC6IgmRVDMwKeKGNQiJ+0EDleEhGoIqCPc6IGm0cMAWVu95VuCLJhqL2bwMuK6TvGcWtJnvOOwYkrAvPVC44Ds2nBjalfLw+7j4TmNjes0YlNo4LI5godV3zDYcXJwQ/Vu1Cwc3miJTS0uJkA3jvGfDgrUl1o8Q8iKgSZmncm3rGgi85zd2najYuDu1zV0t/2cijGtnnMSrQlzGsN8hsXoc5oicdli65aRk7SVattach7bzarP9Za3ReJkneLiE3rW3RaO7bpcD2rpFpdZu3XNacL1ccV7O9WPtJAMO7yul9WTQT/ALXeCaZpnwQG7ZNq49n1RxH+I8eGPdzRq44uceJX/ERdJyFoP7+ezp4uWgyviBPfC6SA0PJIAe2lSOYWBeIv00LBPiulMAvuzccCf4h4r2b73K6VZEfZWxaOt8l8R4o2Ti23aW5jGjiIhPPtPaZOvRhw93obgXRmwmqtGhw7dhcbSyH5/CQsd5wb5qwcXX24Nw76hOMBoa2K/wAKGTGgDwhc48lzP1QxN4/7kwS57w67u45uFFZi4WAy7dMNnSmqc4uMC9E5C9giLuOWiDA2xeGtvWcZAwnuvB1oRDQcgAPVSaSYAUYGSZRwLBhHehUNN70ojWrRIOrSWghHQr4pF5o7nT4JxEWbWFpaOLnoO+19oBDQS35ZlR8RunT4oVs0XbRpDgdwYdy0Ex5rifpCyEGnbKbT2ZN9sDKtWp1j7QAnCW3sV7R8+GwWz/Ju0Mmuu/sAkngFaOLGk8TePorb9ZHw2f8AdMIMyBNcU5lwFrgeubmXNPvOedXXiPRFxIaKYnVHIUMjORWVWloJ8WwrIXrc1DcJ11IQEBkCAOGi6M0uvuIIqQY1+JNEucaAALo8t6I0jIUvxqTh/ZO+y/4fYHBs/wCJGox/IVp9rbHi7Adnuk3rQ4wMEyQy0AIEN1gEBWbCYyBIuD1VraDuaCVYNu9Hun4yAXu7Measjda11b9p1jT4lYAssyw3DArFMTXknA2drYW8BjqzQOzpmsnWe6fDFWT96zdp+E5TirQBw+mxg3W/M44BW59oZxLR1Y/eJomECxs/hMGgriBnx2twYKV5oZDaREjJZvOOzhgsx91bVrhiCvhdk7YOuz5gsxoUKtdoUz+oaoo1s3cE2rXJuIOY2Ayx7es08E4RfdFG6ABOffFgBGNYvTgulAAaAOjyeETNkZAcHfhnFfFbWhE/zHBEltr0twhrW53fzKsWlz3ZvdmeZVo5wswMPtJv9woukUIyAJ9Eagqye15/y/6l0iyDb2pII9QrM3rNxwnMHmuji7a28G+K4FzsxwqrTedaWlS12YArnmm0NQTOkCqFLTpVpiODW6/mi6OBaMfgS5gqaaiU5sPAycKOXR3C0ac4wcrNrbYRlk/u9ERdtBo9tCujb7SMbvxD1UFlqNSKV5hdGY95Lm0JbgztQhptYIF0H4n4QFZtDW8miNuO84N80Jo0F2HIJzSHNayhByMq9es22hLXMB1ohldJdB+WjkRde8NLC3lXBFokvtDjH4Vk8tF8TxhHF2QHBYumpWhEei/DggZB0KbhZB8NjHCYXBdqGMTVcY+q1Bgz3rmNk/FhHYuBI80M5DgVyCiCeC7JQwEKaPZprRDUf+1o5xC1a4IY3Re83NCys7UezbwmaHvVqx1nZ3a3i5pgCFfcYdIEH8WCOAa4HyKNu8NDiBNG67bMNa7g7eMeOz2ThPMQhaPD+ePlCaXQCTi50NrwAXR/1kkkkihbPFy6E37a0c4uvPm7A7aK0dNkw5AUJ7SrrndIdm2ktA2WbS6NTkF0txMn5Z9TsdaezYeANfIIEHpBBiYPUnzXRmh1pZNMjdpjTHALo7DaWg/Djd7mhOPh7jRJPALpDxYWAOTQbziP5Y705gszxfadf1VtFpaUiJFG9i9tLycIBbPgrBwsrEQd4E/klWEFwGZccFMW1BAFMdKID9a03XADj9VYAlzHEOAjL4h4J1obNr4hzmQcY0LaL/iADmB1C1zontB2dGN63IwvDrf7V0Qw9zcDkSOcQO9MENaMABsbjHHbww70RAaMAtSuFAv+m3DtWg+6PwOXbtBkFAAE67D+ss/UIplWn0TaOb6oYHMbGVBGJ4IGNJ9zo8m6MXNOMclZC421rdP4vlqhVrDaQN3Gd5xTRu2dk0+sBMdIA8zqUwQEKsPFWHVnEt/sjZuI5gSPJdGdcLhi2N0HvaFaCe3MdhTusDgZ1XS5f0dxycPh9O5PcXXHulsmuAjxTx7SzGV0Gng7Za/b9HHA4gdnkrVjmNDqmSPlbVWR3GHEsdiIzEq1efZzSDiz+YURoQrR4Dm1j2ZqHzkWhF10Xam6KjeBzzTBBfF0EkySIDsynmGktLnSOdPBZOIr4Eo4tswGeLYWbnkklagNB71oDPkm0BDSUeTUcXTIGwY3TErVxPohnJ2uEFauMrkFyC5BfuhcBC5n6rCb0eakGQZkbSJkCQj8Joe5cz9VwNFo5seSM77ajuK4U8kflcpoXETyWraf5ViGky2UPjaXNce0SFJuj2jnDgZKbJshYODrwipqCgSGtBbQZfCnYe2abtOLX+iBvt6S17YcY810YXnEDrNGNK1UXCwhpmeL8TyVk19oQ3kXC8cymkuvalrZjxXS3ODHZl0lo8ZK6e/2tqf3iAye9dHZ2w0eZTrV7mtcYkcJpGSc661jQHPeeW9RW7gbUDCpDRPbKYA1o0AoFZsc6nASrYl413nXZriYCtTebeMhlZrE1OafBtbTKmQ4BPLmOtCZje8qI4Ee5bkXjo3Mrozfa2g4Nwnnd8UXe2teTZjwB2SH21pE+zHpSp7FaRZ2LMXEgglya32ttFSXEYDyVm4F9nQvgYtu9sq1s3NtLSKMMcl0q0l7jQuuxlzhMFmwEjF0gkhWZHfukf5VbC7ZMFSJpe+i6VXpdoDJaT/hz+EdZON60f8AM76IZIGlnhKOpx70ag7MyVm91GhaYNC0H3bLgU3qHOPcBkaJlDxR/WWeUahFM0zCHWCwcM0dtk0udGMAKzdDmkzQ4FWbSQNTg0d6tXE2AJu3hXAcSm9ZtO/BWYJN6uHBWp3LER1QYM7WkXxkefPBWgqMwc2nkrK1vyTvESBdAzycrUueZyJN2B3bLA+0sSMZGIHMIi7aDR46yaRY2wHymT5SjgVZONnfGO8JHkVeki1fdaQcwZElPpDHG1MaCphWRbdsx13hhvVhPgtD5c+DwMeSPWs2m631Hgv+pafaO8fRaN3fJHFznQuALivwgN8l+JxXGvmuAA+5HEFCgA9wYOgSitJlfibH+VfhdH+ZaxI7xsOJgLUEhaO3ghjO6foqXqbrZ1K+WzMn+kgeKa0B1swguJA+USU6hZbMkA9kp8u9nZuBMnO64tdK6Q0Nbbezo2CJMGNAKJxn2DjdfX8D7pB5FWjS1zodhnErpVreZIglrAL0543fFdGuj2pOZaCXO5LojWmzBEbtnGXFxXSXCWg1ug073KzYA6ML3xHvVgW34477uyF0YXbM4iRuNPbU7HNuD+Mhvqm2bZ5kSfHZaD2bObsfCVb/AGhdg5kjdAPJVMCS5o4jLyXyuodjRJVvSyn4bPXtRPsrKcLoifABOPsrH90R6AKzaS0HN3wjtKti43zSGDec7lPkuhvAsBFYBq4eBKLYtrF3Eb0ahNmbN8SQcgcD2o0DrrwJ1quib9pdwABvwYpMkBWtpIHLdH+ZNDWMbAcGObQEiu9wXSf1bet7Gczq7RW9XE1LQcp1125vOC44IdgCwL3UC0+EIZDbhRfO5ERGXd92ZWNSm9ZuyIMYHnsf12j4Uago9ceaOIPuQJjS8JXTIkNJghxuXXDgVa2jWk8gXei6F0dpaw9W9ArH8Q7la9Xk6oTgQeRTzds20kJjQb3ExTYQb3JE7hPzjGOzFGA8MiHRzGMJgDWtGQG3p32lnPw2hy76dyeNwnJ/wnvXRNx7XY3RQHswV4EWVmL+8MMwE4ybR5IJGpZ9AtBuN8K+KxuNgOPZinOPsn/DdwbBHBHLl+yHYDIJtQwUmck0TcgtFfxRHioIBYQW178FMAXQTGNW08EdW3gTxa8BO6oaTZOnkd1RSxtxfaRoZkeCH+L0V9w/yi9/lVfsulNoOHxeis5u2llD2lpNZYXCtNexNMvbafZlzdGtdmOac26wlroAOdBVWJDrpaQC74Wi9kI2MlsnG88m8fBB4EDdME0vDAq0bJboQYPkukWwnkKf6kNnRB7W3GV44A+CkNvOMCSjUEJ3+IyldS3AodV7QTTzCDwbUCriNBMK1syyxABLgIjALphMWrQQIfUx/DRNaC46uNXeKN60fZtNIA3Z7imXWWgAILwKNF2O9Bt0Xs/mcRxJTa3bMR/lwQMX2tIOGBLBHgmguL33iGgDE0arZ10OOfxO8YVkGtDpgNu7znTwJREOtTUMPpy71bb7DJ3Z11KOAmvuH4RlzWVmOqEKDJTAkROzjs4/cLt/Axdm7jh+hbg4eqbi3XiNhwCNCFaHd7dt4NugxiukFoa4wS1rgXSBrkrjb7rwBLoE7uKfZvAc0giY1XQbc2ZbwJ9qF7Zjp4Frk6yj+lh9FZuuHt3h5JwBHarPL1Vob1odlmL1raD5QfJFlY1z8fdZvWVq6gaeJ0Kst1ji4us92geGNivGVa9eyYGtJAoJc2Rlosnu6x7TLipgW9puWf57UetZdHF2ml/FHG1cbzz2n0TQHifwkHyQJHj95HEbOA+q+V1P0jOvoGjrEpqGF4VHI4oZscXCORPqtU8mQxgeBHzSqktZuPH8JoVZsu2brSskCBehNMF1kTrj8aMg2VuAPOiODrMkfULJtpuu7xIQwB37Mx3hCpdZktIHIypi7ai744eKtTNwGBXrbwkHuTHC8CWzzhgqYOasxDczzViBLSagiaNEZ7LNpc48AJXTLQuE43QTHjKJllyrg8AwQrKBFrgL09WkqPtCzqzwTGkh2IBih71bEizkZZu9Ag5zLNtpLWNAMN6sTTCvFXb24y7ZsuibokzTVMExwmF0lxFmNGA5eXYmi6y2HWaOI9U3FvAp8wSJAJPWVob7xpSIVuWl0HATQeqsQGm6BJc41PaSukE+1tzSQTLmtzXS7QNNq6rjdyGlSFZNDR2DFEyL1Y2ZNGJRy+IrNx+qztHdVccB2IL/AKjvRdw2ZDNZXhE/eW9VybiNeIQq1wxlZoYHTa5om0EVkTLRw1TSW2YFXHM40Vu67ZtkGT/ImSXtOIbnIwcELWyd/NT/AEossrucugUpwTbNrOkDC8RLDH8MINvt5s3vRM3T2YeGy0ILA2sgT3Kau15fN5Lpli72Lj8VDd9VZPewjtveu3KcTyGJX/3Ftus7B/dY+ys9yzEfnRH/AArEXnkj5nfUo/HaVtO6PRDO0O7/ACpgxMNaAETBtms3BxOcbDLTMPe8A49q/eH+1DGyd1uzIoUvNY4eYXyhtVrdd/tQoRccTXsXBrv9qz3Xf7VhVjvoE4xgQQe33RqtTJ8lq0T6rCjYK/dLv8tVxBbTk4LiPps4GfJamgXyME+S/Ebo9F2uK/cCyBbDTzgofKb47PiUwLN+7J0BNFw3vJZtzHNciuAK0z7tugqfBYBxabo7G496toDWnrAAkmeZ94SA8AA1xw2Fsm0ZgDOFGuRMF0XSDqXN9QiKWbyC08sW+SmDbMG73YeKE3rG1bBf/MPJTBfYui9+LdPmiftGWvXj8L2QhX2b95hPZ6gof4lnUx/B9FmHNvAdor4LVpB8tnS3izaM4mT9FZtDRHAQj9nZVwc+hPMNlP37X945dmGxhFp0twyA+H85pgDWtGQFApDgWmCCM1mcXHmcSpDukP0ipHIDxVm0NaOAQBI5wrTI1idUTFqBBaBpRWTS8/wiU5xaG4lgNf7DgukvAj8LTPnC6OLheAIc4VLq6ldHF5zXQYI3iacYG04ErEuOA71qOqO1ZMHVCGGXcv8AqO9Fxw2kUOi1d94PWIyRqE7BN6rgm+PLY0Q1urjQBSSyKB7eHEbBab3aDCcxwFrZHAPJILT2ro02lm+0M2kkzIbJPJAO9gCIJa0OvSV0h1m2zbGIYbzimWbLk5PaB/6ViwNayhv3Rdbeu6cU1pL/AN0CqefsxgfVMa5zgdwkASaHeKNp9s10FoIOEfw5ogXYpTJdDcLRhHyyLw9VaxbMacjO8Oy9CODcXHk0VRx6TbCp/dFfVCrra3O6Ow071FBZiGd+Pgj/AIFnBMYwY9SgN61fF7tcU+YuyWyK9bDZZiG2Rd9nOt1Nwa0QB3fpLKQ4N6wkgtICNDaWRLZ4wGuCBqS00HOAtIH1XxOxa3mTAC/6bMP5j9EMJr/b3TiSxs98L5mEtjswQwbaEXZ1gAIYANAHkuAj3gNy2GPJ2oREG0NpIjhi8IVJd1J/d+qOjAPJHHGewymV9iTP8pd5FNoQ0ESRq26SE7rEC4Y77yFTaWgvGeAwH6PKRUciKomTYvJuzzrPaFnasAz4jd8kawyGWgPFuBQwa4w8Dtr5pri0sfwzGCaCW2YMFxGUrR43e+AR3KZDi1r2uyi9WOxBt6zt7MywwatJaQQhi5nWjsg+C6P1m2hi6+pk9sLVhDh4Lorfa2o44wfDY/csWYlzzw4LpBv27jUyfh7NrRDGnNx6oXSiXlxxuEyO/Ha8S6xgEwKS2q+Y66pwIcDmCvlfgRxLforCLjGYEiugArsdQuAAJjUodZ5wCnHZkwU71k0Yr53rjghl97ikpuIwnaMHZhfA/IhDquGKb8X1UNMZiHAymv8Asuk2bRLW068EF1UR9nb2ZmnFzQfEJ4giAYOorkh1GulpHLRNEAtMgjIGrUSG3RmNCadwVh4g4lERaNHwuzGw4hataJ70aEHRPaekdEaT8u/dHZ5JrRZv/eZu/wB0R+rG84/w/VEOb7e2MvLXEdVscP7o1L7XfcT+FlVh/wAxbQB/C3/2sfZyW2YP55LICAT2CpWB6RbC6wch+eSx9k0ltmD2f2TPigNj+JYSAQwcZ/PNOEvYMAVqafp/xNB8wsbrAGiez7qREgwewhNES4lzjxJP3A4grK0stwzyFEP8O06/ZJ9VyvNjwKJDmuMEOboDBFUd32t3/wBg9hWPsrxLf4mO3h2oTPSbIXmnsw8exYlmDv5TBTQPtC2TXF18TGkKpNleII4EdYDvVtF+3Yb3GZ+sKzF57X7rgOWfYrM3eh2RyjF/5z5e50c3rQjMt65H+UIUA2OJbZgGP4igZfaDAH97NEbLNpIGpyHaV0p3tSCTDWnqgDKm0DrRTZ8rald7lm51Tsya2qBkMbj27MmjEoiYK0WDbPID7u2ocPVDLJ3ELIptQ7Uc1kcwvgtMjw2YJ9SQ0AzzFZUyBjHddRHxtrOlQ5TRzmtJE1xdc8VaOMCziGnGN1ULCcByGxwHt7glrK65J7GkOdiZGcZ+5YmHWTRefaN+WBhmK6q2eXgP68HC7ScOSMxaWpvvcfwtR/x7TruHD+3esXWtrvV4NMrATnGQCND0q1xP7jVm+2N4Twb9VG6DiY0C/wDubejf4WjFfK93s7KeDQiAW3AA0g502NADOjg3WA5kxVMAa0aAUH7YOTh5aJgDWt0ATzBDWhzRHzXqJtS1lC08WnDsKFLJwBvR+IlDC1st0zxGCFAHA+0A75PeUDWDcN4dwPgUQbvSRAcIFA4Zz+SvnYKHm3BM6rrOjmCeU9/es7J9H08+za8mprAdUsdpwK+KycQHjs02SS2Jls8kOsSIJ9Shs6RagkDQbv8AqQoBs4iVkxq0HWK+Y4+4c81k0YlHDUr5nV+8zgEfBDquGIXwvydtyOhRpZ2noUUamcVqUBKGLSniAcbrsndisXFsj5da+CP6zpdsbrQJ+HLzT2H/AJjpZpLiKGCO5dGcTZz8VmdPPt2GgsrLerxKd/hsrauB1OXhyQr7W03nTqMhsJ+xY4blm3IAarIE7x5NxKw/5q2oP4W5+K+a06o4BmiApJjuCwPS7YQ0futzRxtLWoH7rTTZZiGh3VxmrcCh+3oIIYbrTOrRTZZEteyoMjGJxTKljOtGZHJOpfIu2jS04SFW/Y2tXN0ukf22MEljBLjyQrebuvn8TKTzCwbbsqRzP+5Zj4m8HDJAEvvYXYrKFof+VYwY1wFRH/tWg3bQiHkfvfXFH/Dcbr/5T7nQmkDSW7v+Z204nQLNx14bMmipXe47MhmvndieS+Z3uaLXNfO77sTJ2eIXw2mY5pwxHFP6pRRox/y8CjgViCNUeo/JfGzgsxmE9t2/AvAcCr1/olsZArlIwxXSXF1la5MPHyPemG77Nu8XsOt3mjhYs/WOH4v79yA3rQ1cRxcUaC6Js28yrQgtswZuYzXtRws27zz/AAhH/Hthvx+Fv55rE2lrUTwanYsHVYPxkJzd18SGu7Vm60JLRyafVaftj5Q4T3fpLNwc20buukaluPb7oJDDZ1IaPikT4obxLN17TqQKHmEd3/mGCSB+Np9e8rEXCbs6Obi380Vo5oPSWihZn1cdV0PcsmYe0cMSRxzREFhEiixF3eb3GvimiTZbzxEXqg1BjQrXeYP6gUQQH5/wueWjwVs6od1rowntJ975s1kBij8bsUa3nbfFfO6gRqT6bG0MfdpAcw4RsZNNU3Eao4go4tzas6YbDiE7A/KjgQsQdCgIk5o4N57G1vkwW8QUHQ7pfSB1W5BvpnyWdq4V/hGSPVsWbzzOFEer0Zhh7h+P89i0+N5Hi4o0PSbTrOH4B+eaxdbWm84nUThtta2jgILo12ZoYkofCzfP9MoZndHheQqZcf8AYsXAEu8mrEYOI57zvJcAz/asPagQ4cSMD2I4OH7NAm43ed3BHqvfgD+62fNOr7NomP4QWtTy3fdF5riesLsICp1/T2c+ztWEyJ4JpID2Ztymgqrt2W0EchSUcQvwfqzzYrpPtmibJ4FOw1WJ6O8y13Cfr3oUuuo1x4TgeCYN0fM49VvaulTaOcaXWY9k49ysHQ0YG2eM+SaIa0YADbxU7zzQRs0zR+M49i+Z1UcAMSvxdYrIZo/4jkcS7Du2fMaBfI2g2D4RUo/E7H7m7qnTYKluexvVeFkcnI4omSjQQjUFFDX3HdSwZUycL0YIGbPojTAH70f+02ga0QBscADaQL0Dig37NpoCU6osidxg0gUPkhuzW7TJobUrQdbuDyfBDrMf6HNfE9mA5TiuTT/qWl0f7l/1XwY7MEcWWcd0mAO5D4rQhx/qlZC9HotSZ+i+UUHcNpWgMH+6wlw+oXI+izg/Vc59FwE+q0ugeblFJIFfFVwcQY7WrMOAI/mlEAgxQyJp+wmUtCw4u+KSNNFq6vgtBsDmlxxoKO8E8XmGZLprujEoTLiQ08KVUVZaU8cCv325dqPxNIcPD7icCSAgNy0Dmh7f7KxfLLSzhzTOF48NDlgrQ3re0ad4tEQwJghrRkB7gEXckMG/QL53Ynkji520UBRFDosy7BBf9Ry44IZBcMEfhbivmNT9wjtn3PJfC4Yt5rJwwKip4omKI1a4eiPVtPqjgQjiEMB7jcXFAw/pT6GPw/2ryUF1r0h9XRiY0TaO6TaCT2D89ibM2j+sZM+4/dtHNrcBxH7xVoB7V2Z/COHnsJkgGhWpqfFcQjoB+m5BclwAG3Iik80CTYk65s+n34YXaMn976BHAXf95WpDJ/pajiA5wHhCOfP3MjmCm/CcERBY1rQ2OQC1YAB3CiiCQ+DzqCpAc1xBmfgfHgYVoLzT77oizJ6o1dCfVoAIMHDM+S0EO87q1dB8AVpdb6Ao5kCfFZ3XEf5YRxF53q5cx9Fz/suaeCHsJlrgcipl9iatI5Z+aNLjuqT+E+4cX5LEk4Tt1KFb0U2cV/1HUHYuOCbiBlt4U2aNRG7r90OIXxWeR5IdZhxCKyYcu1HEFE4ZtRw9w9Szb1ndiFbHogp/N+ZTaBrRACKGDQIHh7jW7oFSJoXRwFUDuAMvVzJqKrS7BR+Igx4I8wnZH7sypjExn2KyhtqNTHWHP9BTDj9wHxOIA8Uc2CB/VCn4i4kjsuriCfEuRr7NvV/lEBfMfT9K8EGfPsQJdZafiA89op7Nm87uyTjDXGX/ANLQPNYizEXq5bjT4lawyf8AKibxJMlztdvL9EMHjFGjbepcNK5jxThLXCoI2jF7sFnsbgJQwa2p8EfifjHALV2HcsLrUfhGKOJJWaGZXzEUThvRrwQxdifukUnCUcHxTYDiKSo3o12nJDIbXUkVYzWeIQ3nPeZY05Boz/MBNIIeJbaW0aHIIDFxig4lfMSAEPlhZ2jt4+NAj+Imh/eTcGuJc3uw8ET+sbunwonkzfYL0jvWgAC0IkJ8PcB1Q2oHaSsTwnL7tmgwDtmn3UVJKiQ1hvn+mUaB9p6NCxDJoJy4di/CK9rsT9yszeBGJjDtTaOuiQ/iNEetaOMOI4kYDgF89r1J4Wf1Qws2C60DkPuJxCd17M5ceB4rAg0c06Ee4TEL5W4lD4jUrABtfFfI3HtWZz2HE5lfO7BD4RQL5Gao/wAydiTw+6leLUcxsOJGMo4Ha0S5xoAEJFp0gzVvPKdMU7r2jus4/TgrIbvRz+rvfOdUx1y/1XEtxM5DkvmcKeJJXCgXGShgBtJh0iRCsd0tbAaRq0bBUlMLQDrc+7uF1utV0hxd/CN0fdbYkv4hkQPFRv2lpWTndbgB4o5gAfdTitBT7qOvZjAjMRmEz9ZZHEcRqNmDWip5zs0GHev+m31Wuez5RUo5nrLV1Vk0Yo/zEL5jj92ihXw2mRRWLrLI8kOs04hHEFO+Djswa0dZx0aEDLLIUc/jx59ybQNG0ACT1XRhOYPFWZgkRKyJWjRHiYTa3Cd48k2m83dJGpyKOBYZPaFaboAFN7zTxDbIGbvF2Wy3dDoxuAbw8lE8BP3UZlZv/OCxgYu5EpoAaBgAMP0DwYBEye9OJN14kY5RBQ+EOF2eZr4IijYB7d+SiZrAd3tjyRnfDpFNd1NEk5e/ZOLXCuD8D3hRvAmKrmFwr5LAvITSJNQBPNfEND+yGG8QM4qmDfYaXvxN2ZxmFmczsya2pWYHWK+Y1K+UVKPxO6y+Z2OzTPZoPupyKJmCZjY2oIz5oEjSdlpRlkKxOBdHksWWB6jNJAp2JgEhgEA/LJIRwcTAP9KODLSBJ/CcNooHOaCVoBG3AuBLSed0ha33/wC5H4zLnd7p2alWVLzaguOMIAT90aJUw1ozOgQ6tkMB+9+isJIb8wMSB3Jm7vUwpB0K12lpFaZJsT2/+vfcIc01BBWjXGP6pWhqvxGB3NhWYlzgPyUDURUgGRe48FmTiT+yWG8CKEx6qypaNwvD5gNujcT2rU1OxpgnNfM6p2zQNxhZuNShkDAWuf3q1pZWQrjS8fzVWovONoR9mOZzTqe1HVby18k8zfcLxbynPitSZTKuu0/iom7tq0ZOGfb+iZaOgAQA1gJniYWpxPM/dXmY4D+6LZJ0nT9I3PC+0fCeOhWTqO8WlaQicbhHjgtXHDzKtAAcgAP0LQXOJwAFSUDDbV1aaxQBGps5p3CAszmTr+yw4C0mjSDjMI4smu3JoxRyKyGZR+M4oCrtVqVrkso0Q6rjnsFBOJP3h9LKy14ngrVwAvVFjf8AijXyT8fmMU7Frn37TQhOoRkR8pWjACD3kIfFdB76hHAWgujvqPFGocDIPvW4mmIMezd9fu1lV2kN/v8Ae+kTZsHAjePcn7x9P2acQVZ1AGLeXBNH2tnm0/TYBAnYKiVxXzGgC+VtAFwQEAZIZfeWCjc3OOACd/8AT2Z6rW5OA8u9WgLXBPgh2Eg5xkR7pyK4LgsntAa7wx7VMmzd1mzoVEusnUeOz3bz68ICLQfD7nqaI9Z40VsZLuAw+9NEucaAAKxO7IxHxOPOKftBuD20nmminy2g1bsGDW4oCA41Wk0QyC0bVfM6pP3polzjQABdFdAHzn+/khQAIYSYk6IUnIAHLT9FiHtoUMn9aOePmm13uof4jFUcDsZvWTz82nahMOOXCf0+pK4SUcLox7pWZfDT/VXwWhdT1KpN3GO5NEAcB92/G4DzQEneCIkC+36rVNJLRBqG5u4nKU0AA/E7i4/tGzN6wf8AiFbp5nxVkItGOpej4h67RThK+RuC1z7/AL3b71qfw5A8KSVZjedhLs3HmsnHqjWJxXyaegXD9IRVw1Q+DX0X/UdMd0gI8oHiFkAZ/Q8VlARzOP57Fqd0eIX4AXH/AErhA85X4nf7QEMCRePe6VoPuzTBLIuz+8V8zt4+MDwQM3QR/liF1fbAQR/DESnVIDoqdXQZRwN8mO9RhKHwhxae8J+PD9hM67qQe9NEkCMuxNxjMa/p+iwWuw9oBhKsTdt2RFdRsNSQMfvgxJTW3Q8EwBhyWTRRo9yRV2ELP9HrFe9DAD9EaDhxWbXmYHAZoZDdC1ivf96swCWjEyQPVA3bral5FYynyQ+I7zzzcfT9OMSUMsO77z8fELM5nns0GB+iOZxHIpuP6UYjUZhD/wCssRQlubo8+9WgDmnga/fbTAHCG181G9GE/fWiWwYMpvxHqmPIpwlrmmQR7wwBNTyGJWtB4SnGGttBEnmCRtJu3S8TPKVhIMj9MLMu7W7w8ky0eR2hv6YZBE0dEkr5B+aIdv8AdN4TPbihjWq+Gc/vbsSP0wwIQN24wTWMuCtAHNOFD98H6+2ysmHIxmmjE1njP375gKqZuiQO6VMGztLxYR/LA7ESJD3AADOhIR0F4+AcEdGj/Ys7gIPfLVmbR/o2qaIDWNgeiqWvAADj+OCmUaXhrjAymqPwWQAHm0eCMVe6SOxoGKcBLZoJw7f03s3ERwF70TXefvaE1WgH1Wgr5Shl/wC4WYoiJk0oVo2qObhAXzR72oAHvnP9h5tcJBCYLoZMlvP73H2YcQBOtdE03ukdIcKWjzo7yXwuGIKPVeMD+weBhaXiuJn3WgnuVbs5k5pzj3Afprl3+chvqnOJPl7rjEtr3J1S0Eho7RBK1ugnvNVoBA8FhUSpm/ZiO9uCHVDQGwO28vme4k+gTfibUEcQUwQbR4ElfuhaQvlNR9V8zaj9LyXBpXJcSB6ri4ei5n6LXLv+9Gj26hOEg/eyS4gannsOSODtOB/ZAwATgA60bUNe7eukfprQRIxBxB7CiZa4YEYXmniuf9kcJw2WpumPhBTv1lqRVx+nD9N8zaFcaELmfouRXBv91yC5j6L94riT9VxE+a/dC4D3ziCtW4dy1bj3IYg/eHH+U6/fjQgrEtzb/b9kAyAcK4jtVrTdEskmBWc/0/ESsji08CsJvONOV1PEPtSIpo0ZD9ia596+V31COFJB7lq6nhitOqFqJC4wQuMg+q1bB8lxEfpuAXBpQ6jiMOB+/wCLrMZ/u/sfghgxv1QoB+3tCtRTyXOR4rQgj6r8JBXEEe4MXnALMkkeAXGT5r90LgB+w89Hc0KEH9h8EDRmvMrQf9jaxB7wuBnzlD4YjvKGAGH7IHVf6FDMER5rmPquBB9VxB++8F8jcO0oZD/vjiJXAR5LgfrK4gH6LiCPquBHquAnyXEfoTmaDxWgF76LtC/CQVxBHvcF8jcO0oZD/wABfuhcCQuwr8QjyXA/Vc5Pgv5R4LWK9/vakBcCQuMH6LjI+q+RuHaUMh7oIuEGS4RUkRT9GSTXjl/+CF50XcLs7uOcftfL9tf/xABLEQACAQICBQcIBwUHAwQDAAABAgMAERIhBDFBUWEQEyIyQnGBI1JicpGhscEUIDAzU4LRQEOS4fAFJDRQY3OiYLLCFdLi8TVEg//aAAgBAgEBPwD/AKrLqGVCek98I34df1S7jSlS/QaFjb0lIz9//Q0kWJ43vYxMT3gixH1Mb/SObt0BFiJ9Imw+FTZT6O3pOntW/wAuSWXAYxa/OyYO7Im/urTs9HYecyKe4sAayUbgB4AUCCLjMGiyhgpIDNqF8zb/AKB0zF9GlKkqwTECMj0c6Q3VTruoPLpHWgP+v8VamkVWRD1pCQvgLmmRWKlhfA2Je/V8607/AA0h82zfwkGpwWhkA2xsPdUAtFGNVo1+FML6bGfNhc+8CndURnbJUBY9wpSGAYamFx4/50GfnWUr0AisrcTe4p5sM0cWHKUNZr7VztanmKzxxW6MqtnxXZQjYTtJfotGqW4gk/Ooy30uZSTYxxsovkNYNqdQysp7QI9taLKBoiO5wiNLMfUyPwplP0qJ1uVaJ1J2awRQH99bjo6/9xrSshEd08fvNvnU0bGWB1F+bdsXAMpFNIisqE2aS+Eb7a60z/DTf7TfCkN1U71FNKBNHHhvjVmDbsNf/uDho597Vpv+Fm9Q0osABsHKJidIaIDopGrFuLHVU0wiUEgtidUAGu7ck8vNpiAuxZUUb2Y2H+Yc6w0nmiBhaLGp23BsRyPOElijtcTYhi2AgX9/LpgIjEo1wOJPAdb3VpeUaTDPmXWTvXU3uNAggEZg0/R0uJvxI3j/AIbMPnyRoZNG0iEa+cmRfE3HxpAQqg6wBejlpanzoGH8LD9a0z7of7sX/eORowzo51x4rfmyrSv8PN/tP8Kj6i+qKsL3tnqvVi2kyhThYQIqm17XLZ1pSn6JKCcRETXY7bDXT3bSdFsTbBI5GzUAPjyIhV3YuzYyCFOpQBqFLGqu8g60mG/5dVTdLSIE83HKfAYR/wB3I8avhxC+Bg47xyLKrO6C94rYt3SzrRLsjSsT5Zy4vsTUo9nIjM8rEG0UfR9Z9vgvxqR1jRnY2VBc0jYlDEFcQBsdY4f5QGBJUEXXWN19VaUCoScDOBsR9Q5P7s6yZd6sPaDTxPFoi36TaM/OLbaqn/20CCARmCLigQRcEEcKYBgVOYIsfGtFs2jmJ+lzZaBuOHL4UTHDHn0UjW2+wFaSR5BxsmSx4P0fnyKqrfCAtzc22k7a0lnVoCpsGmCtxBBqU20nR+IkX3A/KtMygJ81429jjkjMhL4wAMfQt5u81JKJNGnIBGFZUz9EEGovu09RfhWiOzxF2JOKSQi+xcRAFQ56TpJ3c2vsW/zrSmU6LKykMDE1iMxqpBfSwPw9GA8Wb+VCZTKYc8QTGd1r2rnW+kcyAMIixk7bk2A5C4E80p6sEIX/AMz8qxs0WNAcRTEqnXe2QqFXWNBIcThRiPHbTuqIztkqAsfCkikOjPbKXSTib0Q+X/Faa6paNcRUWVb299MCVIBwki191YiJU0eKwVFxSHXZdg7zU15ZlhB6C2ll7geiPE/D6pIBAJ16v20OpYqCCy2uL5i+r6jNIsy36UTjDkM1bjwPJO7pEzoMTIMVjtA1+6kVGbnlP3iKOBGsfGgyPiUENhOFhx3Go5TDDKpBc6Lla+tNan2fCgQ6g7GX3GtDJERiOZgYxHuXq+61aIvNmWDVgfGnqPmPYbikhCSSODnKVJHqi1R9DSpk/FRJR4dFvlUmGRXhuMTRnLg2V6L4tBjc605q/ejAH4VJIka4nOEXA8TqrSJTFEZAuOxUYb21m1aXlGH/AA5Y3/5C9aTlJo7bpsP8SkVOJGj0sP1Al48ty3PvrSHbmomUlS0sWrLInk0hVXRpgosObkPiQSaQ4YVO6MH3VoQtosXFAfbnUaskWkO4ws7yPnuAsvuFOuH+zlQbUjH8RF/jUWelTtY9FY0Bt3k0EUMXA6TAAngNVaP0ptIk9MRDuQfqeTRkWSFmcXGkO0hB3E9H3AVLII0LEX1AAayTkBWmTOrRwR3DzsBjHZAOdaX0+bg/GcYvUTpN+nJIzKBhXGWYL3X1k8BUkixoztqQXNaMjKhd/vJTjfhfUv5RlUMJjxsxxvI+Jm+A7gKSTFNNKWwxQrzY824zdvDVQIIBG3OtJdujDGbSTGwPmqOs1AWAF72G2ovLy8/+7jukXE9p/kP22dGWRZ41xMvQdRrZDu4ihmL/AFEZjixLhwsQOI2Hkg8m76OdQ8pH6h1j8p+VCNQzOBZntiO+2qpQFnRj1J1MD9+tfmKiKJ5BSTzSLr3HVn4V93pfDSI/+cf8qn6EkU2y/NP6r9X2NSR4WdrsecYNY6hlbKprrpMD7Hxwt4jEvwqXo6ZA34iSRnw6QrSkA0WYKAOgzZb9daUMeiyEfh4x4dKhhkQXAIYBrH21pgvo0vCNj7M60o9CJv8AXiPtNTC8Ug3xt8KSMS6PDckWEcmW9bGtLd0RShsTKi6r5MbVpP8Ah5v9p/hSn+7g/wCl8qilMWiaOwGLFzSfxZVpRto8x/0n+FaQP7tEvnPCvvFTSrFGZGvYWGWvM2om2dRTczofPlSxdjJhGsmRsq0tyujyMMiVwjvbIfGiVggv2YY/+0UsBZIMbG8bCRr7Wt8iafpaVEv4SPIfzdFfnRlAkn0hrlYF5lQNp1t77CnlCR844IyHR1m52VzpMxiUXCLic7ieqKby8uH91AbtuaTYv5dZ40SBrNq0qVkjsn3kpwR952+FGCNIBGxskdmY78PSN+/bSzKYhMbopXH0tYFaMrMW0hxZpclB7MY1Dx1mp3Ln6PGekw6bDsIfmdlIiooRRZVFgOA/b4pVkDEAjAxRg2RBFLMeeaFlw2UOhv1l2+zkjkWRA6G6tQiUSNILguAGGw21HvrSUOESp14TjXiO0viKmlbmVmiOQwyHLrJ2h7K0lDJA2DrWxoR5y9Jai5uTDOo6TxgX4HO1TqZMDREM8Mw2+Dg+BqWMSRvGe2pH860eTnIlY9a2Fx6S5NUkDRaIQDjOjyc6m/Cpvb2XrTGHNxTjVHLG9/RbI/GpVxRuvnIw9oqDp6LH6UKj2itCbFo0ROsJhPeuXypZOfhlFsOckVu7KpWxf2eH3Rxt4raiLgjeK0QMmjRiQYCq4SG4ZCtM+7Q7poj/AMhWlf4eb/af4UseSy3yGjYMPvvQH9xg4GE/8hWmkDRpv9s++tIHS0VP9YH+FTWmi4gTz9IS/cM60psOjytujb4U62XRIPSUn/8Amt/jWl5iJPPnT2L0j8K0zNEj/FlRPC+I+4ckUgCz6S2oscPqR5D2m9aPCBAgkGJvvGv55OL3UfLaRh/d6ObnjIdQ/L8anbm1JjA52Zgi8WOVz3CmTCiaLESGYXd9oXtN3tRiLSoCLRQAFfSfZ/DUXltIabWkQ5qPcW7bD4VpHlXXRx1T05fUGpfzGnjVwFYXAINtmWqmBIIU2NsjrsahhWJLA4iek7nWzbSailWVSyXw4ityNdto4ftLSIpUMQMZwrfad3JHNikkjYYHjNwPOQ6mHJHI4meKS3nxnVddo7weSTyUom7D2SXh5r+Go1pEbMA6feRHEnHevc1RyrIgkXUd+zeD3Uo5mcr+70g4l4Sdofm10JvLNCwwnCHQ+cNvsNCR10ho3PRdccXh1l+daOAjS6OdSnGg9B9ngbitGODFAf3XU4xnq+zV4VovQMkH4T4l9R8x7MxUUbxzy5eTltIDubUw8ddXF7XzqLyekyR7JRzyd+px8DQkV3khI6ire+oh60VLwGCVbiNjH0hkyjqn2VbK1aCf7uinXHijP5TatFjaOLA4sQ727ixIrRMjpC7tIY/xAGtFVX0doXFwjSRMO4/pTOyTQQr1GR73zPQAtnWlqzQsqgliVtb1hWm/4dzuwn2MK0o20eb/AGn+FHLRs8rQ/wDjTf8A46PgkR9hFadbmGG2RkT2sKn/AMTow4yH/jUqltKgyOGNXe9sr6hWnH+7OPOwp/EQKeENLHJiPksVl2HELU6M2kRNboRq7X9Jsh7r1NnpOjr5vOP7rD41pEnNwu+0L0fWOQ99PA3NwQAXQEc6eCi+fea0mUxwu69a1l9Y5CoIhFGqazrY72Os1H5WdpexDeOPi3bb5Usah2cDpPbEfV1UyhlKnUwtlxomPR4dVkjWwHwHea0eNlUu/wB5Kcb8Ny9y6qSaNy4Vr82bNuB76jkWRA69U3tfhlenY6QTHGbRDKSQbd6J8zSYAMKWsnRsuy2yopOcBbCVAYqMW0Db+0SxrIhRtR3awRqI48ksRZ45EsGjaxvtRusPmOSaPGt0I5yI4kPpbjwO2lfnoiUJjYgrxRtoPdSqWjCy4WJXC9tR31GJBKsZfOG/W/eRNqPrKcv/ALqRRG5JF4dI6Eg2BzkG7m1Gjo4MHMlmNuq56wI6p8KdnkiWVR5fRnOJd5HXX8w1VIOeiWSI9IWkjPHce/UajCTGPSFLKwUrbv1q3ca0jybxz7FPNv6j7fA2qfyc0U+wnmZPVbqnwPJMMOlQSecGhPj0l+FabdBHOBcwyAm3mt0Wrm15wy9opg4WGfJEWXSpY2YsGVZUB2bGArRjZp0PZmLeDgNXOsZUCgNFIjNjGfSGrOoTbStJXfzbjxFj8K0h+ZieSNVviBI1XLEAk0VBIJAuNR3X5NO/wsvq06rIhUgMGGo6jUiycxKCQ7GN8IVbbMhtqbLQFuMPQhFj3rTrHIcDWYoVe18weyaaENKkpJ8mrBV2dLWaZ1XDiNsbYV4k7K0zMQr52kR+7pfLlZFEnPsbYYypvqC3uTUzCSSCMG6seeNtqp1fabVcar6ql8rpCRDqw+Vk9bsL860mUpH0PvHISMek36a6JXRtHAXpFQFUbWc6vaajvDDime5zd24nYPgKBuL1JKruZHPkNGP8Uv8A8fjU0jNhhjuryi7HaibT37BT6Oph5hSY0yHR122jxqU4yNGiOEADnGXsJ5o4mnjIiMcJEZw4VPm1FEsaBF1D2k7SeJqacoVjjXnJX1Je1l2sx2D9njlDl1sVMbYSD7j3HkniMiWVsLKQ6NuYavCoZecW5GF1OF181h/WVRsWUMVKE61OymXmZudGUctll4N2X+RqUGFzOoJU/fKN3njiNvCj0kOBrYl6LDPXqNFneNZLeX0ZrOo2+cB6wzFeTmi85JV9xqETCJka2NMSxs2eIdljb30kpBTSCMOM8zpC+a4yDe33GkR4pSqi8Ml29Rtvg1DyU+HsaRdhwkGv+IZ0YCZZb5xTRgML9oZZeFIDPorxMemuKJj6S6m+Bp0lMBQPaXm7Yxl0t9aUrHRycscQEo3YkzqNxIiuup1DDxonkMIMqy3IZVKW2EGly0uVbG0sSPw6JKmtAy0dV/DZ0P5WNN0dNjP4sLJ4ob/Og3OyTQyAFU5sjjfPPxHJetIjMkLxg2LqQL08jRtBGLdMlW/Kt8q54c9zNs+b5y/ja1MqsLMAw3HVlQRQ5cDpMACd4GrknGLSNHXzS8h/KLD3mp89I0ZfSdz+VbfOneT6RGi3wYHZzbLcovWksxeCNSVLyYiRl0EzapI1kRka+FhY23UsJE7Sm1hGI0A2DWajjCYrXONy5J13NJGiYsIC42xNxJ200as6yHMoCF3DFrNDyukk9jRshxkbWfyipYjJJESRgjJYrvbs+ytJkYBYozaSY4QfNXtN4VIkMUKgrdYiCi7S/Z7yagiKgs+cknSc/BRwFTSsCI485ZNV9SjazcBUUSxJYG/aZm1sdpNRTLKCyg4QbBtjcRwqaZg3NRANKc8+qg85v0qOIQqzdKRz0nbtMR/WQqHnSuKWwLG4Qdkbr7T+y3HIl0keGQ40lLNGWzyPWTw2cKgjeImLXEM42vqHmHu2VOpjb6QgzUWkUdpP1XZUiCaIhWIxAMrrv1qaQM0QEqjEy2ddY4+2omKNzEhv+Gx7S7j6Q/nUETxF0y5q9494vrXuGypfJSrN2HtHJ/4N4ajRC6PE5jQkLd8IPibUrBlDDMMLjuNSQEyEgYo51KTL4ZMPhWjSNhaJ85IDgPpDst4isQ0qAlQY3VsgdaSIcr0dJP0bnwuIr113WNn9lFhFpCsD5PSrDukA6J/MORpEVlRjYyXC8bUAALAWA3UeRmVRdjYXA9uQ5AoF7AC5ubbztqWHHJFIGw8yzG28MLUuWmyDfAh95om1bRyNGrMrEXZLlTuvWBcRewxEWJ22FHkFGIc6Jb5hClu83oHFpp/0YQPFzf4DkHT0s7oIgPzSG/wFTy81E0lsWEZDedg8aklkXmgqXaRwG9EWu2dPGWkja/RjxG3pEWHzoiTnQQQIwpuNpb9BU0oijZ9eEZDeTqHia6WjwIos0sr2z1c4+bE8BTMFBYmwUXJrRlLk6Q46UvUB7MfZHjrNR+Wk50/dxkrFxOpn+QpZSA+kvisx5uGPaRsy85jUEbLd5M5ZM24blHAUcEgZMmGaMPkaeQg8xAAXAHqRjYW+Qq66OAigzTSm/pMdrMdgFB1LFbjEBcjbY1POIwABjkfJEGtj+m80mPCMdsVulh1X4UjFlDFSl+y2vx/Y54ecUWOF0OKNtzfpvqGXnFzGF1OF13MP6ypsGkIyg2KNa/aR11GoZC4IcYZIzhccd44HZTTosqxMCDIDhPZJHZ76j8hJzJ+7kuYjuOsp8xyKfpCNHIOalibZ2T2XU1BKXujjDLHk4+DDgakL88Y5OnDOuEZdVrZg99aM5s0Lm7w5XPaXst41BFzSlL3XESo81T2fCllbnmiZbDCHRhtGo34g1pClHTSEBYg4JAouSh4eiaWHDM0itYSKMSekO17KJEOkYT93pXuk/wDkKVFVVUDooAF22tqqUYJ4pBqe8L9xzX3/ABrTEJixr1oWEq/l1jxFSOTJo0iEmNyQbemvRJ7qNGpo+djZL4cW2r1ccnMjnueub83zdtlr3vR18hrOr7+TbQ5BGgdnAsz2xHfh1VGjK8rsb42GHgoFq0Q3SSc/vnZ/yDJfcK0LGYecc9KZjLbcG1AeFGr8kqNJNEtvJxnnWO9h1V+ddG4BtfWBt76n8rINHHV68x9HYv5vhU7FiNHjNmcXdh2E2+J1CuhEmxEjXwAFRK0jc/ILfhIeyp7R9I+6p5WLCCE+VbW2yNfOPHcKPklGjwZyHMsc8N9bvxolNGjCIC8jnojtO+1mPxNRQFAzsQ0zjpPb3AeaN1XXRlCi800pv6TtvO5R7qjjEQaaZlMhHSfUqjzV4fGlYMoZTcMLg8DRlQSCO/TYXsM7Ded37HHIkih0OJTf3VMrI3Pxi5AtIo7Sf+5dnspMDDnEsecAOIbd1TIwYTRi7oLMvnp5vfuoc1PGrWDqbMOBHwIqaISoUOW1WGtWGoitHlZ1KvlJEcDjjsI4Gpoi1pI+jKnVOwjarcDTEyqJ4hhmhuCh1+lG3ypZ0MaSG6CSwAbI3OytJBTDpCC7RdcedGesPDWKmUyRBoz0ltJGRqvs8DX0hBCJzfDYE2FyN9+7bWvMU/O448GHBnzl9fAj6ujw8zHgxFwCSt9gOzwqGXnExWwm5VlOwqbEVwq+VW5NX1DrrOtlCtpoauUi4tvqWBhopgh14ObGI7NR91O/NtBDHa7G3ciDOjqoj30zKilmOFRt5EhCyvKTiZ7KPRUdkeNLGYlkZRzkjkvnlc9le4aq0eIopLnFJIcUjcdw4DZUsPOOmI+TQ4innN2b8BU85S0cYxzSdRdg9JuArA8EYSMGWaY9JyMsW1m4DYK6GjR3zkkkP55H/r2VBCVJllOKV9Z2KPNXhU8zJhVFMkkmSjZ3sdgpVTR1MkrY5JOs1s2OxUHwFLE8rCScWAzSLWB6Tb2+FMCQQDhuLXGzjSsqYotFXnHv5SVs1B3u3aPCoYyiWZ2ka9yzbzu3CldWvhN8JKnvGujKgkWInpuCQvAbTu+1lcohcKXw9YDXh2kb6VgyhlNwwuCN1JMeeaJ1wHrRnY6/qN1KqrfCALnEbbztozMkwRwBHJ92487arfKj5CS+qGVv4HPyb407NDJjYloZDnf922/1T7qf+7uZR9zIfKDzW88cD2vbWkSukYkQBlVgX29DaRUkZLxzwkEmyvnk0Z/TZRIAJJsBrJqYFG+kRjFYeUUdpN44rsp1jniIviSRciPcRSqQoVjiIUAnfxrRo3jDxkdBW8kb9k528KsIpih+60km24SbR+b41GgRFRb2QYRfPVy66sK1auSOJkkla4wyMrKNxtZvbR30OS3JqocnHlGur5kDkzNZUDyFVLBiBiXUbZi9HVyTI8kkSW8kDzjneV6q+3PklMgwiMAksASdSrtPJGJBiMjA3Y4QNSrsH61NIY0LBWcjUq6yTqqCEpeSQ4ppOsdgHmrwFESBnlN2wqQkaHZx9I+6oI2vz033rDVsRfNHzqGZ5WLKuGG3RZusx3gebUswjsAMcjdVF1n9BxpIiCZpvKSAGwXUo3IPntrEVPP6Q/NjqpEDqvvt1mrDLP18UMXmdt/WPZHCmkWK0MKBnt0UXIKPObcKiEgUCRg7bSosO6pmlFkiXpPfpnqoN53ncKhgWIG12ds3dusx+2ih5tnwt5NjiCW6p22O47qmi5xbA4XU4kbzW/rXUUpkUqehInRcead44HZSI8kRj0gZg4cQ7VtTjdUbXvo8/SbCbE6pE39++ozgP0aXpBgebZu2u1T6QqImNjo0nSUgmJm7S7UPFfhUZMEgga5jf7lt3+mflS/3eTmzlDKfJ+g/mdx2UJA8jwSJbK63zDpt/mKhiES4AxZQeji7I83wqGAxM4VvJMcSpbqk67HdUUciSSZ4on6a3OasdY7uSWJZUwPe1wcsiCNx5Nta8uTOhW3kPINXKfd9S9Fhe1FtgpRYUatV6GqhyHVyGr1nV+Q1e/IZk5wRA4nOZAzwje26pZipEcYxytqGxR5zcPjUUIjuxOORuu51n9BwpJUcsqsGKZNbYaYJk7hehchm7O/PZWOSbKK8ce2UjNvUB+JqOJIxZBa+ZOsk7ydtcyxl5x3ZgOog6Kjv848kZlYlnAReyutu9jq8PtiQBckAbzTEhSQMRAyG/hRfnEXSYgRJHcOm0gdZDxGykdXUOpurC4NTRCRbXwsvSRhrVt9Lh0hMMq2eJhiUHNWGog7jsqeLnEsDhdekjeaw1GoJudXMYXQ4XXzW/TdT83NzkDA5KL3HnaitKuBBcl8C2xHNjakkWVA6G6uMjUalUVWYuVFix1njQZsbKVsoAKtvvrHhy7aG2tVBgcqyq9uFA3N+TOgaL55Vs10jZ2o0BRq18jto3A8aGulW2fId1a61UNVDbV+TdTHLKsZ250HG0UOUckiMgP0dFxyv0mOoX1sd/dUMKxA2JZmzd26zHjUiY1K3ZcW1cjTNFo6Kqi2xETWTw/WhC0hx6RawzWIdReLecfdUczzNeKwhU9cjN+C8OPJLCZWAZrRAZouRY+kd3Ck0hCwjhUuq9EsvUW2y+3w+2mjd1sjmNlOJSNV9zDaKjJkjKyphPVdT1T3bwaiYxtzDm41xMdq+aeK/CpVMTmdBcH71BtA7Q4j3ig4gcMvS0ecjV2Hbb6rU8N5UlDFWXoncy7jU0bX56L7yPIr56+aflUTtJIJUbFC8dip1q6nd8aMI54TAlWw4WA1MNl+6nkKvGMBYSNhLDs7r8kUTRSuqjyL9NfRftL3HXVzV6FAVtrbRoqb5VjaszSi1CjR1clzSnOrDkIvlTZAV1hxHINVGrcnhV630OFba+Va6wisAoCiaFMbUGvyRNIwJkUJ0uit7nDx41KQs11xSzMtkjv0UG1juHH2VFDhJkc85KwsW3DzVGwchnZyY9GAa2TSH7te7zjQ5vR73LSyym52u5G4bAPZXNyzffeTj/CQ5n12+QqwRbKtgoyVR8BUbOy3dcBPZvfLjx+zuL2uL2vbbapjIsZMSh3GYVtu+opVlQOuo+0HaD3U5YIxQYmA6Kk2ue+lKaTFmCpBzGpkcfMVFI2IxS/eKLg7HXzh8xQ/u72P3EjdH/TY7PVPuNTSyRyR2XFG7YGI6yseqe6lhCyPIpPlLYl7Nx2u+lgZJi6EBJM5EPnbGX50jSFnDrZVIwMO0CKJAoEHkO+rclwD38h5NlYRQUWoa6+XI2qsG81ZBWJaBy5NtN1aW4oqDmMr0OQVto8vGhRrUeS9bKtV6OZpdfI87MxigszDrv2E/U8KihWMGxLM2bO3WY8and1YMXWKFbFjrZj5o/q9YXmF5bxQ68GpmHpnYOHtoSM4waKqqgy5wjoD1B2vhV4dHPSYvLJr7UjeA2e6nkSMYnYKOPyqOR3uxTm07OLrHiRso6SXJXR050jIuThjH5tvhQvbP7KeHHZ0OGWPNG/8AE8DS3sLixtmONSK0LmaMEq33qD/vXjv31IZHRJNHZTniseq67r7KliYNz0VuctZl2ONx47jT20iMNGcEsZuuLIq41q3ftoqHTDIAcS2YbONQyGN/o8pz/dOe2u71hWkxOSs0X3sXZvk67VpSSoJGEkXsdnDka96UGtVHlfXatVGttDkvasQvW2hqpzYfUjOVuS9bqsKyrfQoUOU0oN8+TXR5bUd3IQDQHIFVBhUBRuAsOSURpJzszYzqhjAvb1V2sd9Mpcc5pREcY1RXy/P5x4UDLKLKDBHvP3hHAdnxz4UVaIiPR4+k+bSvmBxY62PCugklgH0rSBrJ1LfjqT419HaTPSGxf6SZR+O1vH2UGQMIwQGtcKN3dSMWF8LLn2tff9mXUOEuMRGILtsKDNjK4bKACGvr3jwojmSZYunESecjXOx2svzFSzOESWECVL3e3WK714ipEYETwjpW6S6ucXd6w2VIBpMOKJyrA4kOqzjY3zFBcQUuqllsd9m225bimGYoVwoGhyAYmvsFa+ThRoua6RoIRmeVhesNYaC0FHJattHVybaOqr8SKxHhWLhyHZW08go/U4141socsolwgRYQxNizdkbTbbShI3Kxg6RpB6zMdXrN2RwFJB0uclPOSbPNX1R89dNIwdUVGa+bNqVR8zwpmCqWY2Ci5PCkZWUOoIDZ5ixqVXYBUfmyTmbXNvR41FCkYsozPWY5sx3k/ZkgazapXijCvLYYTYMRqLZeFSSLGhduqoubC/wojmj9Ig6Ub9KRF2+mvHfvr7ry8PThk6Touy/bT5ijJIWR47SwuLHDrB87iN9cwRMJY2wBvvFtk+48Dy2rKrV30TbgK2ZcmzKgAK21x5Qo3Vu+rYGgBW/6m2jQ1Vt5LCinGsJGzk3UzAUCdvJnWytlF91K16vwofUkSaRsOMRxeh128ezTQsqrHAVhXtEC7eHE7zSrhULcmwtdjc+Jq9SypGuJza+QAzJO4DbSszJiwlCb2D+69qxJG562k6QR2dY4bkH9Z1FzuG8uEMTkF2Ddfafs5I0kUo4xKdhrm1wc2emuHD0s7jjSEwMIpDeJso3Oz0G+RqJUjJhRWUKMXo9InIGj/dmxfuHPSH4bHb6p27qjSKIYUCoGJa287eTXWvk21cVsp+rSBvChur51srZyDVybaPIOXfQOVfrys1qx0pJzrFasQJ5BqoaqPJtoWJJ3U51UDcV8a2UaawGW2r2q530uujWQq45EhIcyO7SPna+SqNyryCpZYkK4s37CgYn8BWGeXrHmE81c5D3tqXw9tWh0dOzGg95+ZpWDKGF7HPMW9x+zlEuTRm+HWh1OO/Yd1XTSImVWZNht0XRhvqNiwOj6QAz28JE84fOo2aJxDISyt91IdvoNx3b6ZnjlKynnIdIOFb9hj2TwOynisV0dyVsb6NNtB808R7xVjbOs6HIxyoZirVwPKa21wocptRcAXJt30dIiUXLi3DP4U+loFJU4idQHzrn5jmZUS+wj+VDSZcdmk6N8yoBqTSrC0QZ234cqV9IJsS6jeV/lX0h1QDosQNdmHutR0mbETe19myo9Ie1y2frBfdavptjZlvxDA/Cvpqk/dkjfUemBiQEPtHztRmG3EnevzpXVtTqfGh30NVDVR5NnfQFr8asCBerChV8uQm55FGdBQDR20OQGjyFlBAJALahtNLEiszhek/WbWfbQ0hWfBGDJY2Zl6q/m2ngKmaBGV5SuMdS+bflWsU8vVH0dPOYXkPcupfH2UoKqASWsLXOs/Yy85gPNYcfZxaqjYsgZlKE61OsGhEgkaQCzuArHfbVUxDyiFwYyRihlHnDWP5baBEobR5xZwLm2phsdD/VqikIf6PNm46SMe2o2+sNtNzc/OQMCClr3FteplqNSqKrMXKi2I6zyGsdqYnbSGxo8orhW6pJo0tibDTabENWJu4frR0xyLrgT1mufYKadz1pj+Rf/AKoup1tK/ebfrXOLYDCSBsZifhamIJyUL3fzoSOBZWKjhlRZmN2Jbvz5DI51sx8axNvNB3Gpm9tCaQHN3t307s3WJNtV6UqD0hccDaseE+TLoDx/Si9/3iP66/Oxoh73Sy/7bfK9c5iFpCzey/vpZMPVlYcHGXuvSzztbAqOL6wf11Ut7DEAp4Z8l/rWoi+VFSORRYd9DfXzru5dfJGLOwhQzSgkPPNkL7QDr8BUkyR2Dm7tqRRiY9wpUmYBQBosY1Kti/6L76jhjjzVczrY5se9jnUmkRo2DpO5zCIMTfy8aRmZbspjPmk3Pu+zkidJGniuxa3ORk9a21dxpWjmVXFmANxfWrD4EVNGJR0WCyRm6sOy248DtFIV0lQJFwyQSDEB2XXcdxqw11ejW2nFjWy9AXoDLkNXrv5NOQYQ9s8QF+H2ojc6lY+FGGUDEUa3d9bR4w75jEFBJFRphUDK/DUOA+pbkFuTZy66wi/Id1NcAmlvvrbXGpDKMIjVTc9IsbWHzp3VFLOQqjWTRkklHk/JR7ZXGZHoqfifZUZXMaMnOE9aaQ9En1tbeGVNOsYCseckI6sY6R/LsHfUTSNcugjHZGLE3jbKucjx4MS4yL4dtuP2glQu0YPTQAkcDt4062dpICrOtudjv1u/c241EInbn4yQWXA66sx5w84Vlfvo1ajRvTi9Bd9WAGVDVye/6lqeCJ+sor6FDuPtNHRIT2feaGhwjs+81NoVzeOy8N9HRZh2PeKj0KQ5v0RuGZpdFXaqAeLH25V9HTUVS3q02hQnUCvca+gJfrNQ0TCegVX0iMTe+ho47UkjfmsPdSwxqbgEH1jUmixPmRY7xlQ0GO97sRuNfQI97e3+VNoGeT2HEUugC/SckcBao4UjFlFvq25MuTOiaPIOQUy3tSi1cKtSzh5Ciq5C3Be3RuNl9tTxgssgj551yUFrIvpEH9KcpitO5nfWIIhdR3rt72rDPL1j9GTzUzkP5tS+FRQxxiyLa+s6ye87aOeVRxRxDDGoUbd57zt+zmWUOssRLYRZor5MOHpUsquhkQFrA9HtXHZsdtYedA0mAc3LqZWyxW1q/HcaRVkcTRsYXBwzJbX6LDfuNY0L4MS47Xw3zt3VnWdFs6BvyH/I7Ufqd/IeTO+2/JtpsWE4LYtmLVQ0gr5O/wBIm2rGLAd52eJrmZZM5nwj8OLIeLaz7qSNEGFFCjhRR7GTSZQirngjOFB3trNc9NL9zHhU/vZch3hdZ91Z21+NRRiNcIJa5uWY5knafs7S87fEDGVsVIzB3g7akhbFzsJCybQeq43N8jUUwkBFisidaNtY/lxpTz15IxzOkRHA6tqPotvB2GlEU7q5BjmhPSGphwO9TUs5jkCsuGNxZZdYD7mFaPMxJhmsJk9jrsZfnWEX+vb/ACO3LegKvvq9CpExoVxMuLapsfCo4kjUIihVG6pFa7NNKI4l1KnRy9JtfgK5+STKBMvxZMl8Bral0ZcWOUmZ9YL9UequoVJpEaHCTifzEGJvYKViVBIKXF7HWKjgjJEhYztrDscQBHmgZD7KWdIsOO4DnDit0R6x2UqqhJxHyjXszZX9G9SQukhnhN2a3ORk5OBu3GiE0hQ8bFJEyDamU+aw3bxRmMMSvPruA5jBKjieFTxYwJIiBKgujbD6J3qaKc5HhlUdNbOuzjTQxsULLcxG6HdyW5PdQZTqIq4osoFybDjR0qHz1+NHS4R279ymn089hfFv0FJpzg9MAjhrpdIVs16Q4EX9htQkG4/H4VjXj/CaxCsQ13oG+Y/YZtJSLI5k7BT6c56qhe/OoHxRqxNyRnVx9jbkvyuZixLskEKHXrZhxJyUVz8kmUCdH8WTJfyjW1Joy4g8pMzja+oequoUUmLli9kXNUj1tbzmPwoieQEysNGj81D0rek+zw9tROoGHQ4gRtkbop7dbf1nUiItm0qUyYtUY6KnuQZt43oPMwCwxiFNjSjZ6KD52qOMpcs7yM2stq8FGQ+xZQwKsAwORBqWAouEqZoAbhf3kfFDtt7ax85FigZSSvRY5i/GjE7gSgcxOMj2la2xrax76inWTFE64JFHTjbPLeN6mgAAABYDIActuXSjM3RRThOsj4UkM6nonCx2YhemZ8RxE4hvNEk6zf64dxqZvbRJOukdkN1NjWjyrIlxlvG4/allGsgU2kRLrYfGpNPN7RjLe1HTJjtA7h+tO7O2Jjc8iyyKMKsQKE0oN8be2otLkDDGcS7aBuL8jOqi7EAcabSYgpIYG2wHOv8A1A36nvptMub2f+Ow91HS5OzZfeffR0mY9s1z8vnt7aTSZVbFivwO2oNJWTLU248ksUaNzsuOclrRpbEF4KvzPJI8iviZoooVtm2bN8AK595MoEJ/1JOins1n+s6JjL2OLTZV7K/dqf8AtHjc1BNjxK2HGhs2C+EejiOsjbU08UVsZu3ZVRdz3CpDOxURYEVhcu+bDgFqNCihWdpD5za/d9iRcEXtfdQaXR8pGM0X4nbT19440kSB2lQ/eAXAPQPpW30kyM7xg2ePWpyy3jeKsL3tnv5Xmjj6zWNPpqdkt4D9ak0yRur0B76h0qUuqkh7nblQNxepFU2JUMwzXwpmLMWOsm5+z0BTZm2E29n2c06RC7eA31LprtkgwfGmZmN2Jbv+xg0wxrhYFgNVqk02Rur0B7TTMzG7EseP2AJBuK0bTL9CTXsbkxNJGcGKEm4BZcxxwml5oN5NW0yYa3bMA+scl7hXMSSffvl+HH0V8TrajPDH5KJcbL+7iF7d+weNPz2EtK6aLHtC5t4sch4Co1Yk/R05sN1p5rlm7gcz41HoyKwkYtLINTudV9w1D7I1Ek0ZwFudj2M3XHA+d300TwkvB0k1tD803HhqpRHMI5cJuOkpPRYX2clwOSWFJBZhehoiDVhXwufa1/hTaOjABhjttP8AK1DQog2LPuvlQAGqtNkYAAHDiJvvsPsgCTYbajgAbCAJHGu/UXv30i4Ra9z7Pd9RwSuRIzGqn0uSNirp3WOyvp0t9S2qCeSQg6hiCkbPqaRAJQL5W1EUugi+Zf3fqa5lIhclVUa8rk95NPhxHBfDfK/7JoulYbI5y2Nu76ZVdSrZqwsdlHyaARx4rZBVsK5mWX758K/hxGw/M2s+6o3dhh0WNYowSMbiwy81BmfGk0ZQQ8hMzjUz6h6q6hSSyu2URRL5tIbHwUX99Ezl7ARrGDrJJYjgNnt+1JqbTES6qMTD2VJK8huxvw2VomkZYGzI6vEbqDA/zy+o2QvTMWJJNyfstFTFJe18HS8dlKoUWHjxO/60kSuMwDuuL0NGTaE/g/nSRhdpPuHsH1dJn5pLjMnICpJ5JMmOW4fs2i6TYhGaw1Z+7PZQN6UTc4xZl5vUqqM+8mpo3cnFLzcIGYTosd+Jtg7q+k4gF0dDNbLFeyD851+F6EWkjpmQPJsTqxLfhra1RhwgDtjba1re6o0KLZnaQ3Ju3H7M0SACTqFaZhLKwFmYG428L8gJGYyokk3Jz31HpUqHXiG5v1qLTI31nAdx/WgwOog0xABJNqlTC5FwdoI3fXAJ1Z0sErakb2WoaI4ze/cq4j+laNGQeoY1XVi6zHeftSbVpU3OSZdVch+v7RomklWCMbqchfYakiSVcL3w3vYEi/fatITRlwNNqXoomZBPBNpoNpDi0aLAuxpM2twQavE1FGUuWd5GbWW1eC6h9rLpccdwOm24avbTaZKw1gZ7BWbHM69p+skkYtdWBHaRrGn0tmywqVGxrn20Tc3sBwH1Iog2ZOs2Cr1jSaKfMRPXOM+zIUkCLrCn8oFBQNQ+0m0lI8tbbhR06QnIKBuqTTZGth6Hvr6TN55ptJlZMDG49/1ArEEgEgaz9qhRBiNnY6lOocTQeN1KuFjPZZV+NSQumfWXzlzH19FnDrhY9Ncu8b6sDYkatX20ysyFVtdss922mFmI3Hu+2BsQd1RaZGR0iVPHP30ssbamU+NXosBrIHeaM8Q/eJ7aOkQjtr7aEynDbtmy8eP2GkkmZ7/1lyrG7dVWPhX0abzPeKaJ0tiUi+quaktfA1u6lkiUC0eI7cRy9lfSJL3BwjYo6vsozIxu8SnLsnDTlCegCo3E3+1ucxfX9cVoukc4MLddR7eP2+kaMsouOi+w/rToyMVYWI/YAzDUSO7lGvOtGcy6QXOQVbAbvsJ9HlMjEC4Jve4p0ZDZhatF0XEMbjLsj51YclhWEXvYXoIo1Kvsp4Y3HSUHjtqTQNsbeDfrX0aa9sBoaJOex7xQ0KfcB40NAl3oPH+VD+z32utD+z98n/H+dD+z02u3soaBFtLnxFNoEZHRLKeOdSaJKmdsQ3rn9orFWDKbEVBMJUuMiOsPt5oUlWza9jbRUsLxNhYdx2H9jikdG6Gs7Nd6haVlvIoXx+X1yAcjnXNpYDCMtX7HJBHJ1lF94yNDQIwblmI3UsEQFgi+IvTaJA3Zt6ptTf2evZcjvzptAlGoq3uptHmXWjeGfwogjI5ckWjSSC4Fhvah/Z77WWh/Z++T/j/OodEETYg7fI/sEkayLhYXFTaLIjWALqdRAv7a5mXzH/hNEEawR3/bAEmwFzUWgu2bnAN22ooUjFlGfnbf8jKg6wD30dGhJvgX+uH7Pa+ujDEdaKfCjokB7Nu4mjoEexmHvo/2eey/tFHQZhqwt3Gjosw1ofDP4UmiTN2cPrZUn9njtv4L+tfQoLWse+9N/Z6dl2HfnTaA/ZZT35U2iTjs37jUWgu2chwDdtqOGOMdBbcdv/Xv/8QAPxEAAgEBBQUEBwcDBAIDAAAAAQIRABIhMUFRAyIyYXEQMJGhIEJSgbHB0QQTQGJyguEzUPAjQ2CykvEUg6L/2gAIAQMBAT8A/wCVwYJyHogD7snMMPD/AIMGgMPaHoQLE52o91LwOOh8+xVmfyia2XGDoCfKseyDExcP+A7KLazffFG4kdqYP+j5igpIJ9nGgSJjO6tlxjwpLmU/mFNxN1NA/wCk3NhQBJAGdEQY/vUCyDN84UFlWaeGLutBZRm9kjzq0LAXRiaMfdqYzIoGCDpW0X/UIGZu99Aj7tgcbQr/AGh+s/Ctn636DSkBXBzAjxoKSCRgMa2fGv6hRxNBd0tOBF1f7X7/AJVsv6i9aOPaV3A04mIpVLE8hPh2Itox7z7v7hZFi1mGg9gWVYzwxd27Lis+2LP0rZ4lT6ws+/LsF+zYeywPjd2MYdGPsqaOJof0zyceYrZ8X7W+HYGIBHtfKtnxr+oUcT17MEWbxbPyrZn/AFFOG9QuTadQOwkEAREedFiQBkvzpbkc6wvYCRMZiOwqQAfawraQCFHqiPfn2EAKPaa/oKAJIAzoiCRj/aIONbO+U9sR78qvB6UGDbQ5DaCD1P8ANG67sEgg1tLntD1oce+r2bUsaT1x+U+XYSTjSAENI9WRS8D/ALTWy4+ob4dhs3Rpf1pVsuk5lT403EetbQANAyUfCm4EH6jWzB+8UH2hR/p/qf4VZNm1lMVZFi1najsjcVfbaflUANBN0waYgsSBAm6gCSAM6LKNoPZS4e76mhebzjiaGOtRKl2zMDrS7qlszur8/wCxQYmLj6AClT7Qv6jsQAsATANEkCycjUEQcJvFFbTKcPvPjnWB6VteK0PXFqtobVl9RB6iixIUezTX7NT7JK/OlkENkD8KiNqRra8xSqWMCkW00TFbPiI1Vh5UmDj8s+BpLIbZkYzveNIBaYHJW7EJLrPtLRvY9a2v9RutEgsgF8BRQM7Ynm3lTf00HNjUmIyFPcqLytePY5KsAPUAH1pVLGK2SghnbBBhrWzutP7Iu6nDsUA4mLqUEkAZ05BMDBbh9aZpgAQFECisKqgbzb307NmBezcK+ZyHY26tjNr2+Q/GoQQUJibxyPokARBm7sfeAf3N1/mpJAGQwpb0IzQ2h86aTvn1iax2f6D5GkvVl/cOootIAjhpb0caQ1Lfs3GhDfKtmf8AUWdQK2d20HWKvB6VsuNetbPFh+VqXiHUUTZdv3CtmASZ9kmtnxr+oUeP91MtraODdFo+FbPjX9QpONjoGNKpYwOxltbWxMQI8BWzEuBznwq93/UfjRcAvA4t0dKF2zY+0QPC+rMqiDF94/KgstA8as7to5m750NxZ9Z8OQ/ns2agmTwreatsXtDFrvGipDWcTMXU5AhBguPM0gA32wGA1NEkkk5/j2UrE5iaK7oYGcjy7GUqYNFiVC6YVsyJsnBrvoaVRaKtzX30hhxPQ9DjTSJQ5GkNmQ1wZf8A1SmywOhpxZYgdR0pXDbTS2LJrZg2mTVSPeKUwwOhp7toeTVtRDt1nxoiwy3zg1KI2xHMjs2kF2IvkzWzxP6W+FbPjX9QoteVj15r/df93wrZDfXrSYbQ/l+JrZXWjohrZiXUfmFA37RuR/8A0a2frHRD9K2eJb2VJ7GF6bMf4Wp23yRhwjpXCk5v/wBf5pBaN/Cok9KBknaNlgOeQoNCn2nx5D+abdQLm283yFJugv7l6/xQYgyKGN9MxYz4DSmUqYP4kAmYGF57GWAGBkH46dhUWQw6N17F3ls5i9fmKRgDB4WuP1plIMGjvLOaXHpRXdDDWDyoqCgYYgw3yp7wr63HqKe+H9rHrnW0vsv7Qv6imIKL7S7vuy7GvRW9ndPyqyQA2p+FbQ7wdTxC1dkc+za8ZOt/jW0ILSNB8K2mCH8g8q2hIcMMwGFAAq7HEEedbMgMCefwrZcY99bPjX9Qr/c/d86H9Y9WrZcYOgJ8qXgf9vxpSBs31JArZcY5SfCg0KyxxR5UCAjDNiPCluRzrZFItpgKDi075+r76RbTAHDPpTtaYnw6U26gXNt49MhRJIAyFAwZ0q925sadgTA4VuH1oqREjiwplKmDQFiGbi9VfmaM4nO+mWLpm78QrFTI7FaAVOB+OXYpg38LY0RZa+8fEUSA0rdfdTWSpaOLyYfI0pLC7iS9elW9+1HUfGgArFTwbQXH4H3Uu6xDYcLUZW0hvvn+RSXhk1vHUUm8rJ+4e7HsUzs3XSGrZXkp7Q8xhVo2bOUz2MAdmpAiCVNPeEOqx4XVZFkzcwOFN/TQ/qFILTAEmp7Nl/UXrQJBkXRSkW1i68TJpf6xzvagWW8XTIoNCldT8KAJmMr62frHRD2gkiyM2mlFlXY4jd95x7F3ULZtur86RQWvwF56UJd77pvPIUd5oUch2KpAgce08l/mlUCWbBfM0HNq0d486W7fa/2eZoNvS29rTMWMmlSQSTZUZ/hysQfaE9iNZN4kG49KZbJ1BvB5URBiZoG0tnNeHpmKXeFg4+qflQuN4wN4qAGK+ptBcfh4VeraFTTWSwIwN5GmtFbimMbyHUUSGWSd5buoriSc0/6/xVvdWOJWu6UdzaBhgYYdDQK25jdnDlWzItxk277jTAgkHLttGyV1M0b9mp9liPG+ttxk6gHyoX7I/lYHxqLKqwN5ntRrLAnI0ACHOl48as7tr81mgSLxdUmAMh2IYRzrC+NJwOeg8TQAsMTjIApAIZiJhfM0pIIIyotuBedo0WJjkIokmJOFwoMQCNca4U5v/wBaVoDatdPLOkAvY8K+egoFmY38WJ5U7TcOFcKVQZZuEefKmYsfICmUrAOOmlKoi01y/HkKLWiBwjADIU1mYXLPX8Ob1DLcVuaPI05DQ3resPnSGRYOfCdDSmy14wuINGA26c7qYSLY/cNDTsGg+t631pd5SuY3l+YqS7CTjdNEQYOVK27jDIZX6VtAJDDB7/qKj7t77wR4g1Y37E44H4VFpCPW2fw/jsCkgkerj6ABNw7JJxpWhWWOKPKj/SH6z6AJAIBxxqTEZeha3bPOaw2Q/M3w7MNmPzN5ClW0wGtBQbUnhF3PSgYVh7UVu2TdvT5Uq2mAq53JNyqPIUBJgU5AhBguPNqbdWz6zcX0orgg6sf80p2BuHCuH1q8EHDMUF9dzd5mr3kndVfAcqgxOVItrkBiaMTdhzoiDEz+DRrJ1BuIplg6g3g8qEoQTmPEGmWDIwN4oISpYerjrTb62vWHF9ewiwQy7yt56g06xBF6nD6UIs2hcyGeopwLmGDeRzFM1oznF/Oioshgc4NIQQUJ5rOtFpUKRgbjy0riSfW2f/X+KJJJOuNLejLpvD51szvQcG3T76AEOp4h8se1WssDp2R2Wt2z+a13B7CxIAyGFEghQMhfW0xCD1QB762sWrI9UWfQUgKxzO6PnV9JugvngvXX3UgjfOWHM1ex1JpiFFgfuOtIoi23CPM6VxG2/DpryFCXMm4DHQCmeYAEKMBV7mTuqvgKJtQqi7Ia8zREGDlVkxay/BlSDBpSCLB9x0NGRccsqUiLLYHPQ1vISMDhSsVM/wCRTqAZHC14pWAuPCcfrQ3TYa9Wz+BFFDaIF8aVs75Q4Nh+qlNlobDBqsG1Zz7BZgzjl6LtaMxFMtkxjnPd5+mrD7y22s3UBIdjl8TQ7ACTA7C0qFwA8zRa0VB3QLqdpMC5VuFK1kGMTdOgpEm83KMT8hUhzJ3VXActBV7nRR4KKdgd1blHnzNIoMkmAMf4okuQqiFGXzNFgohPe30oY60QTDbQwMlHyFMZMgBeQogjHrQUwWyHeqJMTFEEGDlRXdDAzryNEk40FBWRiMRy1rjH5l8x/FABhA4h5j60N8WfWHDzGlIoJsm6cOtAgBkb3cm7Fgiw13snQ0CUbQqaJvkXU5DQ2ZG91riWRxbP4fxRMkk5+hPazAqozW7vbBs2jcMufpSYjtUgBj62A9+J7Fs32jl59jFboGV/WlEkCY5mnaYUXKuH1rdICi7VjTsOFeEeZ1plCiCd7QYClWb8AMTRYHdW5eefWoB3EFrVv8wFSqYbza5D60AW3mMDX6U0TuiBShbyxuGWZpnLcgMAMB3zNaAkXjPWlaydQbiOVMtkgi9TeKJCsGQ8+nKiMHS4f9TRvFtbvaAyOtNeLYuPrddabfFscQ4h86O+s+suPMa1ZhQ4P8Gma0Zi/Oma0BI3hidaYgqMmF3UdisVMj08vQPcLsHZbQFxpNiBL7SQq6iJraOXacBkNB+BsmLWVKs3tco/yBTNN2AGAoqQASInCgTgJvyqFXi3m9n60zFsatCzAHU59jBRcDJ1y76JwoY6VEE7Njc2B+BoggkHEUrWTqDiNRRlDKm5hdzFI1k6g3EcqdbJuvBvB5ULS2XFEydJOFEFTBxFEgkkCOVQIBm/T0m2TqoYjdImewKWMAFjyraqERUI3uI+/wBDZ/ZpQs1xxHZ9o2QUKy3SPQVrJBAmMedbNdmzgi9bEwb75pyQhIyE1ttudoAIiMevdbEKXFqLPOj9m2TXiR0NN9kb1WB63UdPRBBO+TCi4fKmYsdAMBpQMGYnrQDOSSepNWgLk/8ALP3aUyhRvcRy069isFFw3tdKKGLTGCb4OJ75SAbxaFHdMqZzBphaFsfuHPWlNoWD+0/KotiDxp5gfMUGhSpEg+RpSOFuE+R1pgAtkjeBx1FWjZs43yOVASCZiL417GYMoJ4hd1HcbHbpYCvlu9RQ+zbIHD3TQsruiByrbbQs0GN0kXdovIFQIjKl2aLgoFfaVnZn8t/arFTaGVbAh9qZAAZTcMKH+htYPC2dESCNRR7sMReDHSvv9pINo3egi2jHvJ5UyFccxPj2MFEQZ1pb1v3VBvOZ5UzTcLlGX17LAF7/APjmfpRl8AFVfAVaVeHePtH5CrybzjrTAA3Ge7jOlCkgMYFMpUkGhEiTAogo2vwIplEWl4T5HSuMT66jxH1pVBDXwwvFFiVAPq4GiwKwcVwPLSiBAg9aVWYwoJ6UylTBEdfRXZsyFhfZxGfX0fvHkGTK4U+0ZjJN/oIQGBOAM0ftZNyL41a+0tgCPKjsducfjTAgkHLt+zkDagkxW2CM6gkQQwnTSk252YKNvWeEijeZ7D3yEqMJDXnoDW0JKg+1f78+wIALT3DIZmmYtyAwAypQCCACzHDQc6kLcu82uQ6VZAv2hk+zn763nwEKPcooKSYAmmAEAGTnGFWIvc2eWfh3aNFxvU4ijyoEMLJxHCflS2QSHB+YpWEWWwyOlDcaGvVseY1FTBkZYUwtC2v7hofpSML1bhbyOtHHXs+zsq7KQLyY5k5Vt9oDcd5vJeQ9H7KoCFqN9D0IJr7trJbIZ9v2dA7wcMaCqtwAHTt+1JDz7Q73askKqRcLzESe5VyuHgbxTGeySTJM9iyVsqI9o0DG7s95va+lbq477eX81Ibedrhgo/y6ryL42afH61bC8Aj8xx/ioJFrzoiM56d3BiYuqBEz7qG9utc3qk/A0qiSrbpy686UjgbDI6Gl3GhhIwPSpiYJg/D0EcqDGeenT0D2W/u9gF9Z/gc/R2f2YEBi2Im4UfuExhjz3jW2+0bNkKLJnw7dltChkY0v2gRLKRzF4o/aFyDN7oHiaf7S5wNnp9abaO1zEmPTFFeEFdntLWEXHyr7pbwV2gIMGLxTbNReHnkVI/ALZnemOVGWEtuJkB8taL3QosjzPWgBBJMcs6AkwM6IIMHKlIBki1TMWx8Mu8UMZC+FAEmBiam1uPcwuUn4GuLca5hcCfgasiCG3WGvwq3u2SJjhOnpojOSFvIE0QQYNx7BE30zFjJ9H7x4C2jAy9IMwwJHSmZjiSevdByMgeopdvGRWfZb5GabbhlItm+6GX5ijEkDIns2WxZ5IuGpp1AYgG0Bn6KfZyQCxCWsNTW22J2Z1BwPpKVAmLTc8B9aDAkl5Y5aUTJnsVSxgUQAYmelQWGSJ/njTWZ3Z9/dhipkGDVozIuON1EWxaHEOIfMUxJ3iZm7ndXGI9ceY+tEs15vjuPsxjajmCK+0tsohhL5RiPwey2RcmIu1Nf/ABG1B8q2qhTZAI1kzQSb5UUdmRz8fTZmVFUEhWEmthsy9qIujE69KdSrEHI+h9lUs1o3hMOpplVhDAHrR2ezUFrK3X4V9oAbZK9myfrQ9AtIgCyP8z7VVjMYZ6VKLhvnnh4VvOc2NEQY7tbODZ56VejAkA+YNMIh0uHwNEBhaXEcQ+YoAFZW5kvPMa0Gxcf/AGL8/RRSzBRnTqVMHp2AkEEGCO7JGtFhGtWjqBVozeaLaX0C3OrRAq01BjrX3kUNu4Mgt40dqXMm81s9rs1EOrH30dtsCICfKmgm4R6W02luzdZsiLqV2WbJiakn0NklhAuefXs+0vZSzm3wpto7CGMj04ONFiQATcMqsECW3dBmaUOQQsxn/JqEXHfPLDxomTOHcrEi1hyphBIBnnVowFyFKIUsL8mXlRFmHQ3f5caZQRbXDMaH6ULSWXGf+QaYySQI5dgvupfs7sA0izrNfZ1QA2ZnU/KvtOzkWxlj9fTJAxouKtdBVo6+FSOZqeVGpPbJ1qak61aOpokmhU6SKnmD1q/LyqdaB0PjUtlHdK1khtDWz26PdwnQ0TF5ra7S25OWA7g3gWjZXJFoKThhqaJUGT/qNqcP5pmY4n3ZUqEicBqcKIANxnuwwICNdGDadaIZSRh9KViuI3WxGooyh3TKuPL0fszWlZff440oYPYUWVBnlEVtdoqKSc8Br6bi6e9g6VZOnpKJNDvPvXs2bV3bsVDbRQRIrawSWs2OXaLN9onlFAEmAJqyq8W83sj5mmn1zZ0UfTKghaSBC6nCmCjA2tbqsmJi7vCpADZGgZAV8PVbT+Ka0BYOV4/j0vs+0CPJwIitp9pBIsDhOJ+lMzMZYz3BAOIqyulWV0qyulFJwqw2lBDnVkaCoGgqytWBqasxhUczUUVBqwKsLzqxoasamgAMPwGy2thWAxaL+VbRgYvtc+0rAkkX5Z0jXEWrION1/ShMbgsD22+v0qUXDfOpwpmLYnsZixkme7UrBVrpwbT+KKkGDdzy61Nnce9ciMuYokqLLC0PVP0qDExdr2XVsvs+4wYXt76dbLEaHL+2DsKowm0o2YA687te0RN+FWJv4F/NVpV4RPNvpRYkyTNSMEWSczefCrKrxGT7K/XsZixnyHdytmI3pxpWEWWvHmOlMscwcCKO7uneRrwR8RRLKCOJW8P/AHSoCJBkjFeXKnUcS8J8jpX320s2bVw/uEdgMGYB60zFjJM0CLgqy3O/wFWFXjP7Vxo7QxCiyOXzNKjG/Aam4URBjGmduGLHIfPulQtMZUSTF3CKVgVsNgMG0oShgiVOWR5igoZiE91rGlaDZbhOI+dTDSpwNxoMwmDxY9xaXWrS0X0FB9RUzhf+GJAxouchQMgd+LMCAXY+AqwF4z+0Y0doYhRZHL5mgVAgC84k5dBUoLlFs6nDwpgcdo37Rj/FKSbtmsc8/GiFF7NaOi/WmacAF6dyCQZFK8mZsvr6rdaiGhx1q0Bu8afCmQrDAyuR9NpwFAMMKM59xJ17ASMKBkT30jWi+gq2aJJx7ASMDUnWg5m/t61I1q3yq2NPOrZq02tWjrQY0GB7FYkWVhLrz/PYApEAMzHwFWAOM+4XmhIF0bJdfWNOsQRgcJxpUZsMMzlS2BJaTyGHjTGTIAXkO6IV+EWW0yPSixgKfV8elFSAGyPoEgY0WFFzldQYzGPae7TA92SBjRfS6p7lXgQaLnK7ulabj2QFYTDdDRtReRs10/jOrYXgH7mxqwzbzGBq1CzMKDtG54eFMR65mPUXCjtCRAAVdB3bFSJAstoMKDBrmxyb60Syys8jp6BAONWRUA431ZHY5gd2BB1Pl6RYg3irZ0FAk3+gRNWBzoADkKMTd+EVsjQJBkZVxHePvNWlXhEn2m+lEAX7RrR0HzNFzEDdGg+ZoqoHFJ0X61uRiSfLviwHOiScaVsvwCC/p6ZvqBoPSJgTRYn8MrZE9hs2QADazNKwAuW03P6VYi9zZ5Z+FWkwswNcWpiCbhApjJuEdO9fL0AxHOgwPLtIg9xZOlWNaHSB3zGT0/EK2RpWKmRSFzIXO8n+ahBxEufy4eNM04AKBp3pYCrZ9MEaeFFuXogUF5D331HelgKtnSi5yuq02tFiRHoR3ogX41INxuoqR6atIjvzhR78MOlSNey6rS61I1qR3DcR7QCcqsNpRBGNQdKkDKrRq0MxRjL8KrT179lmiI/CAy09wymTRBGNKuZ9GKIBoppVk6VZOlWDVg1YOtWOdWBrVgVYFFSO9Bkd+RNER+DBIwoTmPxZANWBUDSrIqxzqwasnTtCk1YNWOdBYz/AETRUioOn4AIc6AA/slkafiIGlWRVgVY51YNWTpQU0E1NWRVgVYNWTQQ50AB/z3//2Q==" />
+ <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 %>)
+ <strong>Ruby version:</strong> <%= RUBY_DESCRIPTION %>
</p>
</section>
</div>
diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb
index 5cc1b5b219..4e3ec184be 100644
--- a/railties/lib/rails/test_help.rb
+++ b/railties/lib/rails/test_help.rb
@@ -1,40 +1,48 @@
+# 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
+# :enddoc:
+
+ActiveSupport.on_load(:action_controller_test_case) do
def before_setup # :nodoc:
@routes = Rails.application.routes
super
end
end
-class ActionDispatch::IntegrationTest
+ActiveSupport.on_load(:action_dispatch_integration_test) do
def before_setup # :nodoc:
@routes = Rails.application.routes
super
diff --git a/railties/lib/rails/test_unit/line_filtering.rb b/railties/lib/rails/test_unit/line_filtering.rb
index dd9732bb12..f8ca77fe4a 100644
--- a/railties/lib/rails/test_unit/line_filtering.rb
+++ b/railties/lib/rails/test_unit/line_filtering.rb
@@ -1,78 +1,13 @@
-require 'method_source'
+# frozen_string_literal: true
+
+require "rails/test_unit/runner"
module Rails
module LineFiltering # :nodoc:
def run(reporter, options = {})
- if options[:patterns] && options[:patterns].any? { |p| p =~ /:\d+/ }
- options[:filter] = \
- CompositeFilter.new(self, options[:filter], options[:patterns])
- end
+ options[:filter] = Rails::TestUnit::Runner.compose_filter(self, options[:filter])
super
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_and_line|
- file, *lines = file_and_line.split(':')
-
- 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
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 e9195d5b4e..0000000000
--- a/railties/lib/rails/test_unit/minitest_plugin.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-require "active_support/core_ext/module/attribute_accessors"
-require "rails/test_unit/reporter"
-require "rails/test_unit/test_requirer"
-require 'shellwords'
-
-module Minitest
- class SuppressedSummaryReporter < SummaryReporter
- # 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)
- executable = ::Rails::TestUnitReporter.executable
- opts.separator ""
- opts.separator "Usage: #{executable} [options] [files or directories]"
- opts.separator "You can run a single test by appending a line number to a filename:"
- opts.separator ""
- opts.separator " #{executable} 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 " #{executable} test/controllers test/integration/login_test.rb"
- opts.separator ""
- opts.separator "By default test failures and errors are reported inline during a run."
- 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
-
- 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
- options[:patterns] = opts.order!
- end
-
- # Running several Rake tasks in a single command would trip up the runner,
- # as the patterns would also contain the other Rake tasks.
- def self.rake_run(patterns) # :nodoc:
- @rake_patterns = patterns
- passed = run(Shellwords.split(ENV['TESTOPTS'] || ''))
- exit passed unless passed
- passed
- end
-
- # Owes great inspiration to test runner trailblazers like RSpec,
- # minitest-reporters, maxitest and others.
- def self.plugin_rails_init(options)
- self.run_with_rails_extension = true
-
- ENV["RAILS_ENV"] = options[:environment] || "test"
-
- unless run_with_autorun
- patterns = defined?(@rake_patterns) ? @rake_patterns : options[:patterns]
- ::Rails::TestRequirer.require_files(patterns)
- end
-
- 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
-
- # Replace progress reporter for colors.
- reporter.reporters.delete_if { |reporter| reporter.kind_of?(SummaryReporter) || reporter.kind_of?(ProgressReporter) }
- reporter << SuppressedSummaryReporter.new(options[:io], options)
- reporter << ::Rails::TestUnitReporter.new(options[:io], options)
- end
-
- mattr_accessor(:run_with_autorun) { false }
- mattr_accessor(:run_with_rails_extension) { false }
-end
-
-# Put Rails as the first plugin minitest initializes so other plugins
-# can override or replace our default reporter setup.
-# Since minitest only loads plugins if its extensions are empty we have
-# to call `load_plugins` first.
-Minitest.load_plugins
-Minitest.extensions.unshift 'rails'
diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb
index 511cee33bd..42b6daa3d1 100644
--- a/railties/lib/rails/test_unit/railtie.rb
+++ b/railties/lib/rails/test_unit/railtie.rb
@@ -1,7 +1,9 @@
-require 'rails/test_unit/line_filtering'
+# 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
@@ -11,10 +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::TestCase.extend Rails::LineFiltering
+ 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 4086d5b731..417933836b 100644
--- a/railties/lib/rails/test_unit/reporter.rb
+++ b/railties/lib/rails/test_unit/reporter.rb
@@ -1,10 +1,11 @@
+# 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
@@ -50,7 +51,7 @@ module Rails
end
def relative_path_for(file)
- file.sub(/^#{app_root}\/?/, '')
+ file.sub(/^#{app_root}\/?/, "")
end
private
@@ -63,16 +64,27 @@ module Rails
end
def format_line(result)
- "%s#%s = %.2f s = %s" % [result.class, result.name, result.time, result.result_code]
+ klass = result.respond_to?(:klass) ? result.klass : result.class
+ "%s#%s = %.2f s = %s" % [klass, result.name, result.time, result.result_code]
end
def format_rerun_snippet(result)
- location, line = result.method(result.name).source_location
- "#{self.executable} #{relative_path_for(location)}:#{line}"
+ 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 ||= defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root
+ @app_root ||=
+ if defined?(ENGINE_ROOT)
+ ENGINE_ROOT
+ elsif Rails.respond_to?(:root)
+ Rails.root
+ end
end
def colored_output?
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 8b211ce130..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"
- else
- 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 41921e43f3..3a1b62d9d1 100644
--- a/railties/lib/rails/test_unit/testing.rake
+++ b/railties/lib/rails/test_unit/testing.rake
@@ -1,18 +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"
- pattern = if ENV.key?('TEST')
- ENV['TEST']
- else
- "test"
- end
- Minitest.rake_run([pattern])
+
+ if ENV.key?("TEST")
+ Rails::TestUnit::Runner.rake_run([ENV["TEST"]])
+ else
+ Rails::TestUnit::Runner.rake_run
+ end
end
namespace :test do
@@ -21,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|
+ ["models", "helpers", "channels", "controllers", "mailers", "integration", "jobs", "mailboxes"].each do |name|
task name => "test:prepare" do
$: << "test"
- Minitest.rake_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.rake_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.rake_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.rake_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 fa417816eb..519b08746a 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.5.0"
+
+ s.license = "MIT"
- s.required_ruby_version = '>= 2.2.2'
+ s.author = "David Heinemeier Hansson"
+ s.email = "david@loudthinking.com"
+ s.homepage = "https://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://rubyonrails.org'
+ s.bindir = "exe"
+ s.executables = ["rails"]
- s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', '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.20.3", "< 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 2a5a731fe2..9c866263f0 100644
--- a/railties/test/abstract_unit.rb
+++ b/railties/test/abstract_unit.rb
@@ -1,31 +1,34 @@
+# frozen_string_literal: true
+
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 bcb6aff0d7..7623e8e352 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
@@ -7,10 +9,7 @@ module ApplicationTests
include Rack::Test::Methods
def setup
- # FIXME: shush Sass warning spam, not relevant to testing Railties
- Kernel.silence_warnings do
- build_app(initializers: true)
- end
+ build_app(initializers: true)
app_file "app/assets/javascripts/application.js", "//= require_tree ."
app_file "app/assets/javascripts/xmlhr.js", "function f1() { alert(); }"
@@ -28,24 +27,16 @@ module ApplicationTests
RUBY
ENV["RAILS_ENV"] = "production"
-
- boot_rails
end
def teardown
teardown_app
end
- # FIXME: shush Sass warning spam, not relevant to testing Railties
- def get(*)
- Kernel.silence_warnings { super }
- end
-
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/rails assets:precompile --trace 2>&1` }
- assert $?.success?, output
+ rails "assets:precompile", "--trace"
# Load app env
app "production"
@@ -53,7 +44,7 @@ module ApplicationTests
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
@@ -66,9 +57,107 @@ module ApplicationTests
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 11e19eec80..a80581211b 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
@@ -36,7 +37,7 @@ module ApplicationTests
def clean_assets!
quietly do
- assert Dir.chdir(app_path) { system('bin/rails assets:clobber') }
+ assert Dir.chdir(app_path) { system("bin/rails assets:clobber") }
end
end
@@ -46,14 +47,14 @@ module ApplicationTests
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
@@ -61,29 +62,12 @@ module ApplicationTests
add_to_env_config "development", "config.assets.digest = false"
- # FIXME: shush Sass warning spam, not relevant to testing Railties
- Kernel.silence_warnings do
- require "#{app_path}/config/environment"
- end
+ require "#{app_path}/config/environment"
get "/assets/demo.js"
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"
- add_to_env_config "production", "config.assets.precompile = []"
-
- # Load app env
- app "production"
-
- 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();"
@@ -111,8 +95,8 @@ module ApplicationTests
eoruby
precompile! \
- RAILS_ENV: 'production',
- DATABASE_URL: 'postgresql://baduser:badpass@127.0.0.1/dbname'
+ 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
@@ -177,19 +161,19 @@ module ApplicationTests
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();'
+ 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'
+ precompile! RAILS_ENV: "production"
assert_file_exists("#{app_path}/public/assets/something-*.js")
end
- test 'sprockets cache is not shared between environments' do
+ test "sprockets cache is not shared between environments" do
app_file "app/assets/images/rails.png", "notactuallyapng"
- app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>"
- add_to_env_config 'production', 'config.assets.prefix = "production_assets"'
+ app_file "app/assets/stylesheets/application.css.erb", "body { background: '<%= asset_path('rails.png') %>'; }"
+ add_to_env_config "production", 'config.assets.prefix = "production_assets"'
precompile!
@@ -198,7 +182,7 @@ module ApplicationTests
file = Dir["#{app_path}/public/assets/application-*.css"].first
assert_match(/assets\/rails-([0-z]+)\.png/, File.read(file))
- precompile! RAILS_ENV: 'production'
+ precompile! RAILS_ENV: "production"
assert_file_exists("#{app_path}/public/production_assets/application-*.css")
@@ -206,17 +190,17 @@ module ApplicationTests
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
+ 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" ]'
+ 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/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")
@@ -262,7 +246,7 @@ module ApplicationTests
app_file "app/assets/javascripts/application.js", "alert();"
add_to_env_config "production", "config.public_file_server.enabled = true"
- precompile! RAILS_ENV: 'production'
+ precompile! RAILS_ENV: "production"
manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first
assets = ActiveSupport::JSON.decode(File.read(manifest))
@@ -272,10 +256,10 @@ module ApplicationTests
app "production"
# Checking if Uglifier is defined we can know if Sprockets was reached or not
- assert !defined?(Uglifier)
+ assert_not defined?(Uglifier)
get "/assets/#{asset_path}"
assert_match "alert()", last_response.body
- assert !defined?(Uglifier)
+ assert_not defined?(Uglifier)
end
test "precompile properly refers files referenced with asset_path" do
@@ -293,7 +277,7 @@ module ApplicationTests
app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }"
- precompile! RAILS_ENV: 'production'
+ precompile! RAILS_ENV: "production"
manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first
assets = ActiveSupport::JSON.decode(File.read(manifest))
@@ -307,11 +291,11 @@ module ApplicationTests
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", "p { background-image: url(<%= asset_path('rails.png') %>) }"
- precompile! RAILS_ENV: 'production'
+ precompile! RAILS_ENV: "production"
file = Dir["#{app_path}/public/assets/application-*.css"].first
assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file))
@@ -344,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
@@ -377,16 +360,16 @@ module ApplicationTests
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
@@ -410,7 +393,7 @@ module ApplicationTests
app_with_assets_in_view
# config.assets.debug and config.assets.compile are false for production environment
- precompile! RAILS_ENV: 'production'
+ precompile! RAILS_ENV: "production"
# Load app env
app "production"
@@ -418,7 +401,7 @@ module ApplicationTests
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
@@ -446,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
@@ -467,7 +450,7 @@ module ApplicationTests
assert_equal 0, files.length, "Expected application.js asset to be removed, but still exists"
end
- test "asset urls should use the request's protocol by default" do
+ test "asset URLs should use the request's protocol by default" do
app_with_assets_in_view
add_to_config "config.asset_host = 'example.com'"
add_to_env_config "development", "config.assets.digest = false"
@@ -477,13 +460,13 @@ module ApplicationTests
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
- test "asset urls should be protocol-relative if no request is in scope" do
+ test "asset URLs should be protocol-relative if no request is in scope" do
app_file "app/assets/images/rails.png", "notreallyapng"
app_file "app/assets/javascripts/image_loader.js.erb", "var src='<%= image_path('rails.png') %>';"
add_to_config "config.assets.precompile = %w{rails.png image_loader.js}"
@@ -509,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 a07c51a60f..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,16 +16,16 @@ 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 }
+ list_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip }
File.write("log/test.log", "zomg!")
- assert_equal '[]', list_tables.call
+ assert_equal "[]", list_tables.call
assert_equal 5, File.size("log/test.log")
assert_not File.exist?("tmp/restart.txt")
`bin/setup 2>&1`
@@ -35,18 +37,28 @@ module ApplicationTests
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 decc4d138d..b8e167b488 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,25 +37,21 @@ module ApplicationTests
FileUtils.cp_r(app_path, new_app)
end
- def app(env = 'development')
+ def app(env = "development")
@app ||= begin
- ENV['RAILS_ENV'] = env
+ ENV["RAILS_ENV"] = env
- # FIXME: shush Sass warning spam, not relevant to testing Railties
- Kernel.silence_warnings do
- require "#{app_path}/config/environment"
- end
+ require "#{app_path}/config/environment"
Rails.application
ensure
- ENV.delete 'RAILS_ENV'
+ ENV.delete "RAILS_ENV"
end
end
def setup
build_app
- boot_rails
- supress_default_config
+ suppress_default_config
end
def teardown
@@ -60,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
@@ -75,7 +74,19 @@ 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
@@ -83,8 +94,8 @@ module ApplicationTests
restore_default_config
with_rails_env "development" do
- app 'development'
- assert Rails.application.config.log_tags.blank?
+ app "development"
+ assert_predicate Rails.application.config.log_tags, :blank?
end
end
@@ -92,13 +103,13 @@ module ApplicationTests
restore_default_config
with_rails_env "production" do
- app 'production'
+ 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
@@ -108,9 +119,21 @@ module ApplicationTests
config.logger = MyLogger.new STDOUT
RUBY
- app 'development'
+ app "development"
- assert_equal 'MyLogger', Rails.application.config.logger.class.name
+ 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
@@ -120,7 +143,7 @@ module ApplicationTests
config.action_dispatch.show_exceptions = true
RUBY
- app_file 'db/migrate/20140708012246_create_user.rb', <<-RUBY
+ app_file "db/migrate/20140708012246_create_user.rb", <<-RUBY
class CreateUser < ActiveRecord::Migration::Current
def change
create_table :users
@@ -128,7 +151,7 @@ module ApplicationTests
end
RUBY
- app 'development'
+ app "development"
ActiveRecord::Migrator.migrations_paths = ["#{app_path}/db/migrate"]
@@ -157,31 +180,29 @@ module ApplicationTests
end
test "Rails.application is nil until app is initialized" do
- require 'rails'
+ require "rails"
assert_nil Rails.application
- app 'development'
+ app "development"
assert_equal AppTemplate::Application.instance, Rails.application
end
test "Rails.application responds to all instance methods" do
- app 'development'
- 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
- app 'development'
- 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
- app 'development'
+ app "development"
assert_equal Pathname.new(app_path), Rails.application.root
end
test "the application root can be seen from the application singleton" do
- app 'development'
+ app "development"
assert_equal Pathname.new(app_path), AppTemplate::Application.root
end
@@ -193,7 +214,7 @@ module ApplicationTests
use_frameworks []
- app 'development'
+ app "development"
assert_equal Pathname.new(new_app), Rails.application.root
end
@@ -204,7 +225,7 @@ module ApplicationTests
use_frameworks []
Dir.chdir("#{app_path}") do
- app 'development'
+ app "development"
assert_equal Pathname.new("#{app_path}"), Rails.application.root
end
end
@@ -214,7 +235,7 @@ module ApplicationTests
config.root = "#{app_path}"
RUBY
- app 'development'
+ app "development"
assert_instance_of Pathname, Rails.root
end
@@ -224,28 +245,198 @@ module ApplicationTests
config.paths["public"] = "somewhere"
RUBY
- app 'development'
+ 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 "does not eager load attribute methods in production when the schema cache is empty" 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_not_includes Post.instance_methods, :title
+ end
+
+ test "eager loads attribute methods in production when the schema cache is populated" 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_file "config/initializers/schema_cache.rb", <<-RUBY
+ ActiveRecord::Base.connection.schema_cache.add("posts")
+ RUBY
+
+ app "production"
+
+ assert_includes Post.instance_methods, :title
+ end
+
+ test "does not attempt to eager load attribute methods for models that aren't connected" 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_file "app/models/comment.rb", <<-RUBY
+ class Comment < ActiveRecord::Base
+ establish_connection(adapter: "mysql2", database: "does_not_exist")
+ end
+ RUBY
+
+ assert_nothing_raised do
+ app "production"
+ end
+ end
+
test "initialize an eager loaded, cache classes app" do
add_to_config <<-RUBY
config.eager_load = true
config.cache_classes = true
RUBY
- app 'development'
+ app "development"
assert_equal :require, ActiveSupport::Dependencies.mechanism
end
test "application is always added to eager_load namespaces" do
- app 'development'
+ 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")
@@ -257,7 +448,7 @@ module ApplicationTests
use_frameworks []
assert_nothing_raised do
- app 'development'
+ app "development"
end
end
@@ -269,18 +460,18 @@ module ApplicationTests
RUBY
assert_nothing_raised do
- app 'development'
+ 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
- app 'development'
+ 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
@@ -292,9 +483,9 @@ module ApplicationTests
end
RUBY
- assert !$prepared
+ assert_not $prepared
- app 'development'
+ app "development"
get "/"
assert $prepared
@@ -306,7 +497,7 @@ module ApplicationTests
end
test "skipping config.encoding still results in 'utf-8' as the default" do
- app 'development'
+ app "development"
assert_utf8
end
@@ -315,7 +506,7 @@ module ApplicationTests
config.encoding = "utf-8"
RUBY
- app 'development'
+ app "development"
assert_utf8
end
@@ -324,7 +515,7 @@ module ApplicationTests
config.paths["public"] = "somewhere"
RUBY
- app 'development'
+ app "development"
assert_equal Pathname.new(app_path).join("somewhere"), Rails.public_path
end
@@ -332,7 +523,7 @@ module ApplicationTests
restore_default_config
with_rails_env "production" do
- app 'production'
+ app "production"
assert_not app.config.public_file_server.enabled
end
end
@@ -342,7 +533,7 @@ module ApplicationTests
with_rails_env "production" do
switch_env "RAILS_SERVE_STATIC_FILES", "1" do
- app 'production'
+ app "production"
assert app.config.public_file_server.enabled
end
end
@@ -353,7 +544,7 @@ module ApplicationTests
with_rails_env "production" do
switch_env "RAILS_LOG_TO_STDOUT", "1" do
- app 'production'
+ app "production"
assert ActiveSupport::Logger.logger_outputs_to?(app.config.logger, STDOUT)
end
end
@@ -364,139 +555,103 @@ module ApplicationTests
with_rails_env "production" do
switch_env "RAILS_SERVE_STATIC_FILES", " " do
- app 'production'
+ app "production"
assert_not app.config.public_file_server.enabled
end
end
end
- test "config.serve_static_files is deprecated" do
- make_basic_app do |application|
- assert_deprecated do
- application.config.serve_static_files = true
- end
-
- assert application.config.public_file_server.enabled
- end
- end
-
- test "config.static_cache_control is deprecated" do
- make_basic_app do |application|
- assert_deprecated do
- application.config.static_cache_control = "public, max-age=60"
- end
-
- assert_equal application.config.static_cache_control, "public, max-age=60"
- end
- end
-
test "Use key_generator when secret_key_base is set" do
make_basic_app do |application|
- application.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
+ 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
- Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
+ test "application will generate secret_key_base in tmp file if blank in development" do
+ app_file "config/initializers/secret_token.rb", <<-RUBY
+ Rails.application.credentials.secret_key_base = nil
RUBY
- app_file 'config/secrets.yml', <<-YAML
- development:
- secret_key_base:
- YAML
- app 'development'
+ # For test that works even if tmp dir does not exist.
+ Dir.chdir(app_path) { FileUtils.remove_dir("tmp") }
- 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
- message = app.message_verifier(:sensitive_value).generate("some_value")
- assert_equal 'some_value', Rails.application.message_verifier(:sensitive_value).verify(message)
+ app "development"
+
+ assert_not_nil app.secrets.secret_key_base
+ assert File.exist?(app_path("tmp/development_secret.txt"))
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
- Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
+ test "application will not generate secret_key_base in tmp file if blank in production" do
+ app_file "config/initializers/secret_token.rb", <<-RUBY
+ Rails.application.credentials.secret_key_base = nil
RUBY
- app_file 'config/secrets.yml', <<-YAML
- development:
- secret_key_base:
- YAML
- app 'development'
-
- assert_deprecated(/You didn't set `secret_key_base`./) do
- app.env_config
+ assert_raises ArgumentError do
+ app "production"
end
end
- test "raise when secrets.secret_key_base is not a type of string" do
- app_file 'config/secrets.yml', <<-YAML
- development:
- secret_key_base: 123
- YAML
-
- app 'development'
+ 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
- assert_raise(ArgumentError) do
- app.key_generator
+ error = assert_raise(ArgumentError) do
+ app "production"
end
+ assert_match(/Missing `secret_key_base`./, error.message)
end
- test "prefer secrets.secret_token over config.secret_token" do
- app_file 'config/initializers/secret_token.rb', <<-RUBY
- Rails.application.config.secret_token = ""
+ 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
- app_file 'config/secrets.yml', <<-YAML
- development:
- secret_token: 3b7cd727ee24e8444053437c36cc66c3
- YAML
-
- app 'development'
- assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_token
+ assert_raise(ArgumentError) do
+ app "production"
+ end
end
test "application verifier can build different verifiers" do
make_basic_app do |application|
- application.secrets.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
@@ -506,110 +661,122 @@ 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
- app 'development'
- 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
- app 'development'
- 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
- Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
- RUBY
- app_file 'config/secrets.yml', <<-YAML
+ test "custom secrets saved in config/secrets.yml are loaded in app secrets" do
+ app_file "config/secrets.yml", <<-YAML
development:
- secret_key_base:
- secret_token:
+ secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
+ aws_access_key_id: myamazonaccesskeyid
+ aws_secret_access_key: myamazonsecretaccesskey
YAML
- app 'development'
+ app "development"
- assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.secrets.secret_token
- assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token
+ assert_equal "myamazonaccesskeyid", app.secrets.aws_access_key_id
+ assert_equal "myamazonsecretaccesskey", app.secrets.aws_secret_access_key
end
- test "custom secrets saved in config/secrets.yml are loaded in app secrets" do
- app_file 'config/secrets.yml', <<-YAML
+ 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:
- secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
- aws_access_key_id: myamazonaccesskeyid
- aws_secret_access_key: myamazonsecretaccesskey
+ api_key: abc12345
YAML
- app 'development'
+ app "development"
- assert_equal 'myamazonaccesskeyid', app.secrets.aws_access_key_id
- assert_equal 'myamazonsecretaccesskey', app.secrets.aws_secret_access_key
+ 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
- app 'development'
+ 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
- app 'development'
+ 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
- Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
- 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
- app 'development'
+ app "development"
- assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token
- assert_equal nil, app.secrets.secret_key_base
- assert_equal app.key_generator.class, ActiveSupport::LegacyKeyGenerator
+ assert_equal "697361616320736c6f616e2028656c6f7265737429", app.secrets.smtp_settings[:password]
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
- development:
- secret_key_base:
- YAML
+ test "require_master_key aborts app boot when missing key" do
+ skip "can't run without fork" unless Process.respond_to?(:fork)
- app 'development'
+ remove_file "config/master.key"
+ add_to_config "config.require_master_key = true"
- 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
+ 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
@@ -626,7 +793,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)
@@ -635,15 +802,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 %>"
@@ -657,14 +823,136 @@ module ApplicationTests
end
RUBY
- app 'development'
+ 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
@@ -674,19 +962,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(*args); token; end # stub the authenticy token
+ def form_authenticity_token(*args); token; end # stub the authenticity token
end
RUBY
@@ -696,7 +984,7 @@ module ApplicationTests
end
RUBY
- app 'development'
+ app "development"
params = { authenticity_token: token }
@@ -718,7 +1006,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
@@ -749,12 +1037,12 @@ module ApplicationTests
config.action_mailer.interceptors = MyMailInterceptor
RUBY
- app 'development'
+ 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
@@ -762,12 +1050,12 @@ module ApplicationTests
config.action_mailer.interceptors = [MyMailInterceptor, "MyOtherMailInterceptor"]
RUBY
- app 'development'
+ 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
@@ -775,7 +1063,7 @@ module ApplicationTests
config.action_mailer.preview_interceptors = MyPreviewMailInterceptor
RUBY
- app 'development'
+ app "development"
require "mail"
_ = ActionMailer::Base
@@ -788,7 +1076,7 @@ module ApplicationTests
config.action_mailer.preview_interceptors = [MyPreviewMailInterceptor, "MyOtherPreviewMailInterceptor"]
RUBY
- app 'development'
+ app "development"
require "mail"
_ = ActionMailer::Base
@@ -797,11 +1085,11 @@ module ApplicationTests
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
- app 'development'
+ app "development"
require "mail"
_ = ActionMailer::Base
@@ -814,12 +1102,12 @@ module ApplicationTests
config.action_mailer.observers = MyMailObserver
RUBY
- app 'development'
+ 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
@@ -827,12 +1115,12 @@ module ApplicationTests
config.action_mailer.observers = [MyMailObserver, "MyOtherMailObserver"]
RUBY
- app 'development'
+ 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
@@ -840,12 +1128,12 @@ module ApplicationTests
config.action_mailer.deliver_later_queue_name = 'test_default'
RUBY
- app 'development'
+ 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
@@ -854,7 +1142,7 @@ module ApplicationTests
config.time_zone = "Wellington"
RUBY
- app 'development'
+ app "development"
assert_equal "Wellington", Rails.application.config.time_zone
end
@@ -866,7 +1154,7 @@ module ApplicationTests
RUBY
assert_raise(ArgumentError) do
- app 'development'
+ app "development"
end
end
@@ -876,7 +1164,7 @@ module ApplicationTests
config.beginning_of_week = :wednesday
RUBY
- app 'development'
+ app "development"
assert_equal :wednesday, Rails.application.config.beginning_of_week
end
@@ -888,15 +1176,47 @@ module ApplicationTests
RUBY
assert_raise(ArgumentError) do
- app 'development'
+ app "development"
end
end
+ test "autoloaders" do
+ app "development"
+
+ config = Rails.application.config
+ assert Rails.autoloaders.zeitwerk_enabled?
+ assert_instance_of Zeitwerk::Loader, Rails.autoloaders.main
+ assert_equal "rails.main", Rails.autoloaders.main.tag
+ assert_instance_of Zeitwerk::Loader, Rails.autoloaders.once
+ assert_equal "rails.once", Rails.autoloaders.once.tag
+ assert_equal [Rails.autoloaders.main, Rails.autoloaders.once], Rails.autoloaders.to_a
+ assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.main.inflector
+ assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.once.inflector
+
+ config.autoloader = :classic
+ assert_not Rails.autoloaders.zeitwerk_enabled?
+ assert_nil Rails.autoloaders.main
+ assert_nil Rails.autoloaders.once
+ assert_equal 0, Rails.autoloaders.count
+
+ config.autoloader = :zeitwerk
+ assert Rails.autoloaders.zeitwerk_enabled?
+ assert_instance_of Zeitwerk::Loader, Rails.autoloaders.main
+ assert_equal "rails.main", Rails.autoloaders.main.tag
+ assert_instance_of Zeitwerk::Loader, Rails.autoloaders.once
+ assert_equal "rails.once", Rails.autoloaders.once.tag
+ assert_equal [Rails.autoloaders.main, Rails.autoloaders.once], Rails.autoloaders.to_a
+ assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.main.inflector
+ assert_equal ActiveSupport::Dependencies::ZeitwerkIntegration::Inflector, Rails.autoloaders.once.inflector
+
+ assert_raises(ArgumentError) { config.autoloader = :unknown }
+ end
+
test "config.action_view.cache_template_loading with cache_classes default" do
add_to_config "config.cache_classes = true"
- app 'development'
- require 'action_view/base'
+ app "development"
+ require "action_view/base"
assert_equal true, ActionView::Resolver.caching?
end
@@ -904,8 +1224,8 @@ module ApplicationTests
test "config.action_view.cache_template_loading without cache_classes default" do
add_to_config "config.cache_classes = false"
- app 'development'
- require 'action_view/base'
+ app "development"
+ require "action_view/base"
assert_equal false, ActionView::Resolver.caching?
end
@@ -916,8 +1236,8 @@ module ApplicationTests
config.action_view.cache_template_loading = false
RUBY
- app 'development'
- require 'action_view/base'
+ app "development"
+ require "action_view/base"
assert_equal false, ActionView::Resolver.caching?
end
@@ -928,8 +1248,8 @@ module ApplicationTests
config.action_view.cache_template_loading = true
RUBY
- app 'development'
- require 'action_view/base'
+ app "development"
+ require "action_view/base"
assert_equal true, ActionView::Resolver.caching?
end
@@ -939,11 +1259,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"
- app 'development'
+ app "development"
assert_equal false, ActionView::Resolver.caching?
end
@@ -955,20 +1275,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)
@@ -976,16 +1296,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
@@ -996,17 +1316,17 @@ module ApplicationTests
end
RUBY
- app 'development'
+ app "development"
post "/posts.json", '{ "title": "foo", "name": "bar" }', "CONTENT_TYPE" => "application/json"
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
@@ -1018,17 +1338,17 @@ module ApplicationTests
config.action_controller.permit_all_parameters = true
RUBY
- app 'development'
+ 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
@@ -1040,16 +1360,23 @@ module ApplicationTests
config.action_controller.action_on_unpermitted_parameters = :raise
RUBY
- app 'development'
+ 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
- app 'development'
+ 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
@@ -1058,16 +1385,19 @@ module ApplicationTests
config.action_controller.always_permitted_parameters = %w( controller action format )
RUBY
- app 'development'
+ 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
@@ -1080,32 +1410,87 @@ module ApplicationTests
config.action_controller.action_on_unpermitted_parameters = :raise
RUBY
- app 'development'
+ 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
- app 'development'
+ test "config.action_controller.action_on_unpermitted_parameters is :log by default in development" do
+ app "development"
+
+ 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
- app 'test'
+ test "config.action_controller.action_on_unpermitted_parameters is :log by default in test" do
+ app "test"
+
+ 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
- app 'production'
+ test "config.action_controller.action_on_unpermitted_parameters is false by default in production" do
+ app "production"
+
+ 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
@@ -1114,28 +1499,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
@@ -1144,22 +1528,37 @@ module ApplicationTests
end
test "config.session_store with :active_record_store with activerecord-session_store gem" do
- begin
- make_basic_app do |application|
- ActionDispatch::Session::ActiveRecordStore = Class.new(ActionDispatch::Session::CookieStore)
- application.config.session_store :active_record_store
- end
- ensure
- ActionDispatch::Session.send :remove_const, :ActiveRecordStore
+ make_basic_app do |application|
+ ActionDispatch::Session::ActiveRecordStore = Class.new(ActionDispatch::Session::CookieStore)
+ application.config.session_store :active_record_store
end
+ ensure
+ ActionDispatch::Session.send :remove_const, :ActiveRecordStore
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
@@ -1173,24 +1572,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
- app 'production'
+ 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
- app 'development'
+ 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|
@@ -1198,7 +1603,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
@@ -1212,12 +1617,12 @@ module ApplicationTests
end
RUBY
- app 'development'
+ 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
@@ -1234,7 +1639,7 @@ module ApplicationTests
end
RUBY
- app 'development'
+ app "development"
assert_not Rails.configuration.ran_block
Rails.application.load_generators
@@ -1252,7 +1657,7 @@ module ApplicationTests
end
RUBY
- app 'development'
+ app "development"
assert_not Rails.configuration.ran_block
Rails.application.load_console
@@ -1270,7 +1675,7 @@ module ApplicationTests
end
RUBY
- app 'development'
+ app "development"
assert_not Rails.configuration.ran_block
Rails.application.load_runner
@@ -1278,7 +1683,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'
@@ -1286,59 +1691,265 @@ module ApplicationTests
end
RUBY
- app 'development'
+ 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")
- app 'development'
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
- app 'development'
+ 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
- app 'production'
+ 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
+ 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
- app 'production'
+ 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
+ test "config_for loads custom configuration from yaml accessible as symbol or string" do
+ 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
+
+ app "development"
+
+ assert_equal "bar", Rails.application.config.my_custom_config[:foo]
+ assert_equal "bar", Rails.application.config.my_custom_config["foo"]
+ end
+
+ test "config_for loads nested custom configuration from yaml as symbol keys" do
+ 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 loads nested custom configuration from yaml with deprecated non-symbol access" do
+ app_file "config/custom.yml", <<-RUBY
+ development:
+ foo:
+ bar:
+ baz: 1
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ app "development"
+
+ assert_deprecated do
+ assert_equal 1, Rails.application.config.my_custom_config["foo"]["bar"]["baz"]
+ end
+ end
+
+ test "config_for loads nested custom configuration inside array from yaml with deprecated non-symbol access" do
+ app_file "config/custom.yml", <<-RUBY
+ development:
+ foo:
+ bar:
+ - baz: 1
RUBY
add_to_config <<-RUBY
config.my_custom_config = config_for('custom')
RUBY
- app 'development'
+ app "development"
- assert_equal 'custom key', Rails.application.config.my_custom_config['key']
+ config = Rails.application.config.my_custom_config
+ assert_instance_of Rails::Application::NonSymbolAccessDeprecatedHash, config[:foo][:bar].first
+
+ assert_deprecated do
+ assert_equal 1, config[:foo][:bar].first["baz"]
+ end
+ end
+
+ test "config_for makes all hash methods available" do
+ app_file "config/custom.yml", <<-RUBY
+ development:
+ foo: 0
+ bar:
+ baz: 1
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ app "development"
+
+ actual = Rails.application.config.my_custom_config
+
+ assert_equal({ foo: 0, bar: { baz: 1 } }, actual)
+ assert_equal([ :foo, :bar ], actual.keys)
+ assert_equal([ 0, baz: 1], actual.values)
+ assert_equal({ foo: 0, bar: { baz: 1 } }, actual.to_h)
+ assert_equal(0, actual[:foo])
+ assert_equal({ baz: 1 }, actual[:bar])
+ end
+
+ test "config_for generates deprecation notice when nested hash methods are called with non-symbols" do
+ app_file "config/custom.yml", <<-RUBY
+ development:
+ foo:
+ bar: 1
+ baz: 2
+ qux:
+ boo: 3
+ RUBY
+
+ app "development"
+
+ actual = Rails.application.config_for("custom")[:foo]
+
+ # slice
+ assert_deprecated do
+ assert_equal({ bar: 1, baz: 2 }, actual.slice("bar", "baz"))
+ end
+
+ # except
+ assert_deprecated do
+ assert_equal({ qux: { boo: 3 } }, actual.except("bar", "baz"))
+ end
+
+ # dig
+ assert_deprecated do
+ assert_equal(3, actual.dig("qux", "boo"))
+ end
+
+ # fetch - hit
+ assert_deprecated do
+ assert_equal(1, actual.fetch("bar", 0))
+ end
+
+ # fetch - miss
+ assert_deprecated do
+ assert_equal(0, actual.fetch("does-not-exist", 0))
+ end
+
+ # fetch_values
+ assert_deprecated do
+ assert_equal([1, 2], actual.fetch_values("bar", "baz"))
+ end
+
+ # key? - hit
+ assert_deprecated do
+ assert(actual.key?("bar"))
+ end
+
+ # key? - miss
+ assert_deprecated do
+ assert_not(actual.key?("does-not-exist"))
+ end
+
+ # slice!
+ actual = Rails.application.config_for("custom")[:foo]
+
+ assert_deprecated do
+ slice = actual.slice!("bar", "baz")
+ assert_equal({ bar: 1, baz: 2 }, actual)
+ assert_equal({ qux: { boo: 3 } }, slice)
+ end
+
+ # extract!
+ actual = Rails.application.config_for("custom")[:foo]
+
+ assert_deprecated do
+ extracted = actual.extract!("bar", "baz")
+ assert_equal({ bar: 1, baz: 2 }, extracted)
+ assert_equal({ qux: { boo: 3 } }, actual)
+ end
+
+ # except!
+ actual = Rails.application.config_for("custom")[:foo]
+
+ assert_deprecated do
+ actual.except!("bar", "baz")
+ assert_equal({ qux: { boo: 3 } }, actual)
+ end
end
test "config_for uses the Pathname object if it is provided" do
- app_file 'config/custom.yml', <<-RUBY
+ app_file "config/custom.yml", <<-RUBY
development:
key: 'custom key'
RUBY
@@ -1347,9 +1958,9 @@ module ApplicationTests
config.my_custom_config = config_for(Pathname.new(Rails.root.join("config/custom.yml")))
RUBY
- app 'development'
+ 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
@@ -1358,14 +1969,14 @@ module ApplicationTests
RUBY
exception = assert_raises(RuntimeError) do
- app 'development'
+ 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
@@ -1374,26 +1985,96 @@ module ApplicationTests
config.my_custom_config = config_for('custom')
RUBY
- app 'development'
+ app "development"
assert_equal({}, Rails.application.config.my_custom_config)
end
+ test "config_for implements shared configuration as secrets case found" do
+ app_file "config/custom.yml", <<-RUBY
+ shared:
+ foo: :bar
+ test:
+ foo: :baz
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ app "test"
+
+ assert_equal(:baz, Rails.application.config.my_custom_config[:foo])
+ end
+
+ test "config_for implements shared configuration as secrets case not found" do
+ app_file "config/custom.yml", <<-RUBY
+ shared:
+ foo: :bar
+ test:
+ foo: :baz
+ 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 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
- app 'development'
+ app "development"
assert_equal({}, Rails.application.config.my_custom_config)
end
+ test "represent_boolean_as_integer is deprecated" 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"
+ assert_deprecated do
+ force_lazy_load_hooks { Post }
+ end
+ end
+
+ test "represent_boolean_as_integer raises when the value is false" 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 = false
+ RUBY
+
+ app_file "app/models/post.rb", <<-RUBY
+ class Post < ActiveRecord::Base
+ end
+ RUBY
+
+ app "development"
+ assert_raises(RuntimeError) do
+ force_lazy_load_hooks { Post }
+ end
+ 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
@@ -1402,13 +2083,13 @@ module ApplicationTests
config.my_custom_config = config_for('custom')
RUBY
- app 'development'
+ 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 with syntax error show a more descriptive exception" do
- app_file 'config/custom.yml', <<-RUBY
+ app_file "config/custom.yml", <<-RUBY
development:
key: foo:
RUBY
@@ -1418,14 +2099,14 @@ module ApplicationTests
RUBY
exception = assert_raises(RuntimeError) do
- app 'development'
+ app "development"
end
- assert_match 'YAML syntax error occurred while parsing', exception.message
+ 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
+ app_file "config/custom.yml", <<-RUBY
test:
key: 'walrus'
production:
@@ -1437,19 +2118,19 @@ module ApplicationTests
RUBY
require "#{app_path}/config/environment"
- assert_equal 'unicorn', Rails.application.config.my_custom_config['key']
+ assert_equal "unicorn", Rails.application.config.my_custom_config[:key]
end
test "api_only is false by default" do
- app 'development'
- refute Rails.application.config.api_only
+ 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'
+ app "development"
Rails.application.load_generators
assert Rails.configuration.api_only
@@ -1459,25 +2140,361 @@ module ApplicationTests
add_to_config <<-RUBY
config.api_only = true
RUBY
- app 'development'
+ app "development"
assert_equal :api, Rails.configuration.debug_exception_response_format
end
- test "debug_exception_response_format can be overriden" do
+ 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
+ app_file "config/environments/development.rb", <<-RUBY
Rails.application.configure do
config.debug_exception_response_format = :default
end
RUBY
- app 'development'
+ 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
+
+ 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_deprecated do
+ ActionView::Template.finalize_compiled_template_methods
+ end
+ end
+
+ test "ActionView::Template.finalize_compiled_template_methods can be configured via config.action_view.finalize_compiled_template_methods" do
+ app_file "config/environments/test.rb", <<-RUBY
+ Rails.application.configure do
+ config.action_view.finalize_compiled_template_methods = false
+ end
+ RUBY
+
+ app "test"
+
+ assert_deprecated do
+ ActionView::Template.finalize_compiled_template_methods
+ end
+ end
+
+ test "ActiveJob::Base.return_false_on_aborted_enqueue is true by default" do
+ app "development"
+
+ assert_equal true, ActiveJob::Base.return_false_on_aborted_enqueue
+ end
+
+ test "ActiveJob::Base.return_false_on_aborted_enqueue is false in the 5.x defaults" do
+ remove_from_config '.*config\.load_defaults.*\n'
+ add_to_config 'config.load_defaults "5.2"'
+
+ app "development"
+
+ assert_equal false, ActiveJob::Base.return_false_on_aborted_enqueue
+ end
+
+ test "ActiveJob::Base.return_false_on_aborted_enqueue can be configured in the new framework defaults" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
+ Rails.application.config.active_job.return_false_on_aborted_enqueue = true
+ RUBY
+
+ app "development"
+
+ assert_equal true, ActiveJob::Base.return_false_on_aborted_enqueue
+ end
+
+ test "ActiveStorage.queues[:analysis] is :active_storage_analysis by default" do
+ app "development"
+
+ assert_equal :active_storage_analysis, ActiveStorage.queues[:analysis]
+ end
+
+ test "ActiveStorage.queues[:analysis] is nil without Rails 6 defaults" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app "development"
+
+ assert_nil ActiveStorage.queues[:analysis]
+ end
+
+ test "ActiveStorage.queues[:purge] is :active_storage_purge by default" do
+ app "development"
+
+ assert_equal :active_storage_purge, ActiveStorage.queues[:purge]
+ end
+
+ test "ActiveStorage.queues[:purge] is nil without Rails 6 defaults" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app "development"
+
+ assert_nil ActiveStorage.queues[:purge]
+ end
+
+ test "ActionMailbox.logger is Rails.logger by default" do
+ app "development"
+
+ assert_equal Rails.logger, ActionMailbox.logger
+ end
+
+ test "ActionMailbox.logger can be configured" do
+ app_file "lib/my_logger.rb", <<-RUBY
+ require "logger"
+ class MyLogger < ::Logger
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ require "my_logger"
+ config.action_mailbox.logger = MyLogger.new(STDOUT)
+ RUBY
+
+ app "development"
+
+ assert_equal "MyLogger", ActionMailbox.logger.class.name
+ end
+
+ test "ActionMailbox.incinerate_after is 30.days by default" do
+ app "development"
+
+ assert_equal 30.days, ActionMailbox.incinerate_after
+ end
+
+ test "ActionMailbox.incinerate_after can be configured" do
+ add_to_config <<-RUBY
+ config.action_mailbox.incinerate_after = 14.days
+ RUBY
+
+ app "development"
+
+ assert_equal 14.days, ActionMailbox.incinerate_after
+ end
+
+ test "ActionMailbox.queues[:incineration] is :action_mailbox_incineration by default" do
+ app "development"
+
+ assert_equal :action_mailbox_incineration, ActionMailbox.queues[:incineration]
+ end
+
+ test "ActionMailbox.queues[:incineration] can be configured" do
+ add_to_config <<-RUBY
+ config.action_mailbox.queues.incineration = :another_queue
+ RUBY
+
+ app "development"
+
+ assert_equal :another_queue, ActionMailbox.queues[:incineration]
+ end
+
+ test "ActionMailbox.queues[:routing] is :action_mailbox_routing by default" do
+ app "development"
+
+ assert_equal :action_mailbox_routing, ActionMailbox.queues[:routing]
+ end
+
+ test "ActionMailbox.queues[:routing] can be configured" do
+ add_to_config <<-RUBY
+ config.action_mailbox.queues.routing = :another_queue
+ RUBY
+
+ app "development"
+
+ assert_equal :another_queue, ActionMailbox.queues[:routing]
+ end
+
+ test "ActionMailer::Base.delivery_job is ActionMailer::MailDeliveryJob by default" do
+ app "development"
+
+ assert_equal ActionMailer::MailDeliveryJob, ActionMailer::Base.delivery_job
+ end
+
+ test "ActionMailer::Base.delivery_job is ActionMailer::DeliveryJob in the 5.x defaults" do
+ remove_from_config '.*config\.load_defaults.*\n'
+ add_to_config 'config.load_defaults "5.2"'
+
+ app "development"
+
+ assert_equal ActionMailer::DeliveryJob, ActionMailer::Base.delivery_job
+ end
+
+ test "ActionMailer::Base.delivery_job can be configured in the new framework defaults" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
+ Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"
+ RUBY
+
+ app "development"
+
+ assert_equal ActionMailer::MailDeliveryJob, ActionMailer::Base.delivery_job
+ 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
+
+ test "hosts include .localhost in development" do
+ app "development"
+ assert_includes Rails.application.config.hosts, ".localhost"
+ end
+
+ test "disable_sandbox is false by default" do
+ app "development"
+
+ assert_equal false, Rails.configuration.disable_sandbox
+ end
+
+ test "disable_sandbox can be overridden" do
+ add_to_config <<-RUBY
+ config.disable_sandbox = true
+ RUBY
+
+ app "development"
+
+ assert Rails.configuration.disable_sandbox
+ 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 ea68e63f8f..db16f4cc56 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
@@ -23,14 +25,14 @@ class ConsoleTest < ActiveSupport::TestCase
end
def test_app_method_should_return_integration_session
- TestHelpers::Rack.send :remove_method, :app
+ TestHelpers::Rack.remove_method :app
load_environment
console_session = irb_context.app
assert_instance_of ActionDispatch::Integration::Session, console_session
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
@@ -71,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
@@ -79,9 +81,9 @@ class ConsoleTest < ActiveSupport::TestCase
end
MODEL
- assert !User.new.respond_to?(:age)
+ assert_not_respond_to User.new, :age
irb_context.reload!(false)
- assert User.new.respond_to?(:age)
+ assert_respond_to User.new, :age
end
def test_access_to_helpers
@@ -89,76 +91,83 @@ 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
- Process.spawn(
- "#{app_path}/bin/rails console --sandbox",
- in: @slave, out: @slave, err: @slave
+ def spawn_console(options, wait_for_prompt: true)
+ pid = Process.spawn(
+ "#{app_path}/bin/rails console #{options}",
+ in: @replica, out: @replica, err: @replica
)
- assert_output "> ", 30
+ if wait_for_prompt
+ assert_output "> ", @primary, 30
+ end
+
+ pid
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_sandbox_when_sandbox_is_disabled
+ add_to_config <<-RUBY
+ config.disable_sandbox = true
+ RUBY
+
+ output = `#{app_path}/bin/rails console --sandbox`
+
+ assert_includes output, "sandbox mode is disabled"
+ assert_equal 1, $?.exitstatus
+ end
+
+ def test_environment_option_and_irb_option
+ spawn_console("-e 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/credentials_test.rb b/railties/test/application/credentials_test.rb
new file mode 100644
index 0000000000..2f6b109b50
--- /dev/null
+++ b/railties/test/application/credentials_test.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "env_helpers"
+
+class Rails::CredentialsTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation, EnvHelpers
+
+ setup :build_app
+ teardown :teardown_app
+
+ test "reads credentials from environment specific path" do
+ write_credentials_override(:production)
+
+ app("production")
+
+ assert_equal "revealed", Rails.application.credentials.mystery
+ end
+
+ test "reads credentials from customized path and key" do
+ write_credentials_override(:staging)
+ 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
+
+ test "reads credentials using environment variable key" do
+ write_credentials_override(:production, with_key: false)
+
+ switch_env("RAILS_MASTER_KEY", credentials_key) do
+ app("production")
+
+ assert_equal "revealed", Rails.application.credentials.mystery
+ end
+ end
+
+ private
+ def write_credentials_override(name, with_key: true)
+ Dir.chdir(app_path) do
+ Dir.mkdir "config/credentials"
+ File.write "config/credentials/#{name}.key", credentials_key if with_key
+
+ # secret_key_base: secret
+ # mystery: revealed
+ File.write "config/credentials/#{name}.yml.enc",
+ "vgvKu4MBepIgZ5VHQMMPwnQNsLlWD9LKmJHu3UA/8yj6x+3fNhz3DwL9brX7UA==--qLdxHP6e34xeTAiI--nrcAsleXuo9NqiEuhntAhw=="
+ end
+ end
+
+ def credentials_key
+ "2117e775dc2024d4f49ddf3aeb585919"
+ 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 644af0e737..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]
@@ -166,9 +167,36 @@ module ApplicationTests
config.api_only = true
RUBY
- FileUtils.cd(rails_root){ `bin/rails generate mailer notifier foo` }
+ 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 44209a52f7..a35247fc43 100644
--- a/railties/test/application/initializers/frameworks_test.rb
+++ b/railties/test/application/initializers/frameworks_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
@@ -38,7 +39,7 @@ module ApplicationTests
assert_equal expanded_path, ActionMailer::Base.view_paths[0].to_s
end
- test "allows me to configure default url options for ActionMailer" do
+ test "allows me to configure default URL options for ActionMailer" do
app_file "config/environments/development.rb", <<-RUBY
Rails.application.configure do
config.action_mailer.default_url_options = { :host => "test.rails" }
@@ -60,7 +61,7 @@ module ApplicationTests
assert_equal "https", ActionMailer::Base.default_url_options[:protocol]
end
- test "includes url helpers as action methods" do
+ test "includes URL helpers as action methods" do
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get "/foo", :to => lambda { |env| [200, {}, []] }, :as => :foo
@@ -129,7 +130,7 @@ module ApplicationTests
end
RUBY
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
get "/foo/included_helpers"
@@ -161,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
@@ -176,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
@@ -186,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
@@ -204,66 +205,64 @@ 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/rails 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/rails db:migrate db:schema:cache:dump db:rollback`
- end
- silence_warnings {
- require "#{app_path}/config/environment"
- assert !ActiveRecord::Base.connection.schema_cache.tables("posts")
- }
+ 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'
- ActiveRecord::Base.establish_connection
- assert ActiveRecord::Base.connection
- assert_match(/#{ActiveRecord::Base.configurations[Rails.env]['database']}/, ActiveRecord::Base.connection_config[:database])
- ensure
- ActiveRecord::Base.remove_connection
- ENV["DATABASE_URL"] = orig_database_url if orig_database_url
- Rails.env = orig_rails_env if orig_rails_env
- end
+ require "#{app_path}/config/environment"
+ orig_database_url = ENV.delete("DATABASE_URL")
+ 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])
+ ensure
+ ActiveRecord::Base.remove_connection
+ ENV["DATABASE_URL"] = orig_database_url if orig_database_url
+ Rails.env = orig_rails_env if orig_rails_env
end
test "active record establish_connection uses DATABASE_URL even if Rails.env is set" do
- begin
- require "#{app_path}/config/environment"
- orig_database_url = ENV.delete("DATABASE_URL")
- 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
- assert ActiveRecord::Base.connection
- assert_match(/#{database_url_db_name}/, ActiveRecord::Base.connection_config[:database])
- ensure
- ActiveRecord::Base.remove_connection
- ENV["DATABASE_URL"] = orig_database_url if orig_database_url
- Rails.env = orig_rails_env if orig_rails_env
- end
+ require "#{app_path}/config/environment"
+ orig_database_url = ENV.delete("DATABASE_URL")
+ 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
+ assert ActiveRecord::Base.connection
+ assert_match(/#{database_url_db_name}/, ActiveRecord::Base.connection_config[:database])
+ ensure
+ ActiveRecord::Base.remove_connection
+ ENV["DATABASE_URL"] = orig_database_url if orig_database_url
+ Rails.env = orig_rails_env if orig_rails_env
+ 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 0f9bb41053..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
@@ -142,13 +143,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
@@ -178,13 +179,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.load_path.inspect]] }
end
RUBY
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
load_app
@@ -215,7 +216,7 @@ fr:
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
@@ -223,7 +224,7 @@ fr:
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
@@ -234,7 +235,7 @@ fr:
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
@@ -246,13 +247,13 @@ fr:
end
test "[shortcut] config.i18n.fallbacks = [{ :ca => :'es-ES' }] initializes fallbacks with a mapping ca => es-ES" do
- I18n::Railtie.config.i18n.fallbacks.map = { :ca => :'es-ES' }
+ 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
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
index d106d5159a..c08761092b 100644
--- a/railties/test/application/integration_test_case_test.rb
+++ b/railties/test/application/integration_test_case_test.rb
@@ -1,12 +1,14 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "env_helpers"
module ApplicationTests
class IntegrationTestCaseTest < ActiveSupport::TestCase
- include ActiveSupport::Testing::Isolation
+ include ActiveSupport::Testing::Isolation, EnvHelpers
setup do
build_app
- boot_rails
end
teardown do
@@ -14,9 +16,9 @@ module ApplicationTests
end
test "resets Action Mailer test deliveries" do
- script('generate mailer BaseMailer welcome')
+ rails "generate", "mailer", "BaseMailer", "welcome"
- app_file 'test/integration/mailer_integration_test.rb', <<-RUBY
+ app_file "test/integration/mailer_integration_test.rb", <<-RUBY
require 'test_helper'
class MailerIntegrationTest < ActionDispatch::IntegrationTest
@@ -38,8 +40,36 @@ module ApplicationTests
end
RUBY
- output = Dir.chdir(app_path) { `bin/rails test 2>&1` }
- assert_equal 0, $?.to_i, output
+ 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
diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb
index efb21ae473..9c98489590 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,27 @@ 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 "constants without a matching file raise NameError" do
+ app_file "app/models/post.rb", <<-RUBY
+ class Post
+ NON_EXISTING_CONSTANT
+ end
+ RUBY
+
+ boot_app
+
+ e = assert_raise(NameError) { User }
+ assert_equal "uninitialized constant #{self.class}::User", e.message
+
+ e = assert_raise(NameError) { Post }
+ assert_equal "uninitialized constant Post::NON_EXISTING_CONSTANT", e.message
end
test "concerns in app are autoloaded" do
@@ -103,24 +120,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::InternalMetadata], 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, ActiveRecord::InternalMetadata, 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::InternalMetadata], 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 +150,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 +162,7 @@ class LoadingTest < ActiveSupport::TestCase
end
MODEL
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
require "#{rails_root}/config/environment"
@@ -174,7 +191,7 @@ class LoadingTest < ActiveSupport::TestCase
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
@@ -186,7 +203,7 @@ class LoadingTest < ActiveSupport::TestCase
end
MODEL
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
require "#{rails_root}/config/environment"
@@ -209,7 +226,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]] }
@@ -222,7 +239,7 @@ class LoadingTest < ActiveSupport::TestCase
end
MODEL
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
require "#{rails_root}/config/environment"
@@ -241,7 +258,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
@@ -255,7 +272,7 @@ class LoadingTest < ActiveSupport::TestCase
end
MODEL
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
require "#{rails_root}/config/environment"
@@ -274,7 +291,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]] }
@@ -286,7 +303,7 @@ 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
@@ -299,7 +316,7 @@ class LoadingTest < ActiveSupport::TestCase
end
MIGRATION
- Dir.chdir(app_path) { `rake db:migrate`}
+ rails("db:migrate")
require "#{rails_root}/config/environment"
get "/title"
@@ -313,7 +330,7 @@ class LoadingTest < ActiveSupport::TestCase
end
MIGRATION
- Dir.chdir(app_path) { `rake db:migrate` }
+ rails("db:migrate")
get "/body"
assert_equal "BODY", last_response.body
@@ -341,32 +358,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 643d876a26..fb84276b8a 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", {}, {"REMOTE_ADDR" => "4.2.42.42"}
+ 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
@@ -84,9 +85,10 @@ module ApplicationTests
end
test "mailer previews are loaded from a custom preview_path" do
+ app_dir "lib/mailer_previews"
add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'"
- mailer 'notifier', <<-RUBY
+ mailer "notifier", <<-RUBY
class Notifier < ActionMailer::Base
default from: "from@example.com"
@@ -96,11 +98,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 +110,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 +118,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 +133,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 +148,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 +156,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 +166,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 +178,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 +199,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 +226,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 +236,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
@@ -253,14 +255,15 @@ module ApplicationTests
end
test "mailer previews are reloaded from a custom preview_path" do
+ app_dir "lib/mailer_previews"
add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'"
- app('development')
+ 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 +273,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 +288,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 +296,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 +313,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 +325,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 +343,7 @@ module ApplicationTests
end
RUBY
- mailer_preview 'notifier', <<-RUBY
+ mailer_preview "notifier", <<-RUBY
class NotifierPreview < ActionMailer::Preview
def foo
Notifier.foo
@@ -348,7 +351,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 +359,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 +369,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 +381,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 +400,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 +412,7 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/foo"
assert_equal 200, last_response.status
@@ -419,7 +422,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 +476,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 +492,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 +526,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 +538,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 +610,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 +622,7 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/foo"
assert_equal 200, last_response.status
@@ -526,7 +636,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 +647,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 +663,7 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/foo"
assert_equal 200, last_response.status
@@ -571,7 +681,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 +692,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 +709,7 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/foo"
assert_equal 200, last_response.status
@@ -616,7 +726,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 +751,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 +767,7 @@ module ApplicationTests
end
RUBY
- app('development')
+ app("development")
get "/rails/mailers/notifier/foo"
assert_equal 200, last_response.status
@@ -672,10 +782,45 @@ 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
app_file "config/routes.rb", "Rails.application.routes.draw do; end"
+ app_dir "test/mailers/previews"
end
def mailer(name, contents)
@@ -695,7 +840,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 639b01b562..17df78ed4e 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
@@ -59,7 +60,7 @@ module ApplicationTests
assert_equal "YOU FAILED", last_response.body
end
- test "url generation error when action_dispatch.show_exceptions is set raises an exception" do
+ test "URL generation error when action_dispatch.show_exceptions is set raises an exception" do
controller :foo, <<-RUBY
class FooController < ActionController::Base
def index
@@ -70,7 +71,7 @@ module ApplicationTests
app.config.action_dispatch.show_exceptions = true
- get '/foo'
+ get "/foo"
assert_equal 500, last_response.status
end
@@ -78,7 +79,7 @@ module ApplicationTests
app.config.action_dispatch.show_exceptions = false
assert_raise(ActionController::RoutingError) do
- get '/foo'
+ get "/foo"
end
end
@@ -86,7 +87,7 @@ module ApplicationTests
app.config.action_dispatch.show_exceptions = true
assert_nothing_raised do
- get '/foo'
+ get "/foo"
assert_match "The page you were looking for doesn't exist.", last_response.body
end
end
@@ -96,11 +97,25 @@ module ApplicationTests
app.config.consider_all_requests_local = true
assert_nothing_raised do
- get '/foo'
+ 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 37bd8a25c1..515b32080e 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,10 @@ 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::CachingKeyGenerator.new(
+ ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33", iterations: 1000)
+ )
)
endpoint = Proc.new do |e|
@@ -69,7 +73,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 be86f1a3b8..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,7 +57,7 @@ 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.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 85e7761727..479615c133 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
@@ -30,7 +31,7 @@ module ApplicationTests
add_to_config "config.force_ssl = true"
add_to_config "config.ssl_options = { secure_cookies: false }"
require "#{app_path}/config/environment"
- assert !app.config.session_options[:secure]
+ assert_not app.config.session_options[:secure]
end
test "session is not loaded if it's not used" do
@@ -50,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)'
@@ -71,7 +72,7 @@ module ApplicationTests
end
def read_session
- render text: session[:foo].inspect
+ render plain: session[:foo].inspect
end
end
RUBY
@@ -82,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)'
@@ -112,7 +113,7 @@ module ApplicationTests
end
def read_cookie
- render text: cookies[:foo].inspect
+ render plain: cookies[:foo].inspect
end
end
RUBY
@@ -123,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
@@ -150,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
@@ -195,42 +201,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
- 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
+ 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
@@ -239,8 +246,9 @@ module ApplicationTests
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] = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTE5NjVkOTU3MjBmZmZjMTIzOTQxYmRmYjdkMmU2ODcwBjsAVEkiCGZvbwY7AEZpBg==--315fb9931921a87ae7421aec96382f0294119749"
+ cookies[:_myapp_session] = "TlgrdS85aUpDd1R2cDlPWlR6K0FJeGExckwySjZ2Z0pkR3d2QnRObGxZT25aalJWYWVvbFVLcHF4d0VQVDdSaFF2QjFPbG9MVjJzeWp3YjcyRUlKUUU2ZlR4bXlSNG9ZUkJPRUtld0E3dVU9LS0xNDZXbGpRZ3NjdW43N2haUEZJSUNRPT0=--3639b5ce54c09495cfeaae928cd5634e0c4b2e96"
head :ok
end
@@ -250,104 +258,56 @@ 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"
- RUBY
-
- 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
+ # Use a static key
+ Rails.application.credentials.secret_key_base = "known key base"
- 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)
-
- get '/foo/read_raw_cookie'
- assert_equal 2, encryptor.decrypt_and_verify(last_response.body)['foo']
- end
-
- test "session upgrading legacy signed cookies to new signed cookies" do
- app_file 'config/routes.rb', <<-RUBY
- Rails.application.routes.draw do
- get ':controller(/:action)'
- end
+ # Enable AEAD cookies
+ config.action_dispatch.use_authenticated_cookie_encryption = true
RUBY
- controller :foo, <<-RUBY
- class FooController < ActionController::Base
- def write_raw_session
- # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1}
- cookies[:_myapp_session] = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTE5NjVkOTU3MjBmZmZjMTIzOTQxYmRmYjdkMmU2ODcwBjsAVEkiCGZvbwY7AEZpBg==--315fb9931921a87ae7421aec96382f0294119749"
- head :ok
- end
+ begin
+ old_rails_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "production"
- def write_session
- session[:foo] = session[:foo] + 1
- head :ok
- end
-
- def read_session
- render text: session[:foo]
- end
-
- def read_signed_cookie
- render text: cookies.signed[:_myapp_session]['foo']
- end
-
- def read_raw_cookie
- render text: cookies[:_myapp_session]
- end
- end
- RUBY
-
- add_to_config <<-RUBY
- secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
- secrets.secret_key_base = nil
- RUBY
+ require "#{app_path}/config/environment"
- 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_encrypted_cookie"
+ assert_equal "2", last_response.body
- get '/foo/read_signed_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)
- verifier = ActiveSupport::MessageVerifier.new(app.secrets.secret_token)
-
- get '/foo/read_raw_cookie'
- assert_equal 2, verifier.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 'calling reset_session on request does not trigger an error for API apps' do
- add_to_config 'config.api_only = true'
+ 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
@@ -358,7 +318,7 @@ module ApplicationTests
end
RUBY
- app_file 'config/routes.rb', <<-RUBY
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get '/dump_flash' => "test#dump_flash"
end
@@ -366,12 +326,18 @@ module ApplicationTests
require "#{app_path}/config/environment"
- get '/dump_flash'
+ get "/dump_flash"
assert_equal 200, last_response.status
- assert_equal 'It worked!', last_response.body
+ assert_equal "It worked!", last_response.body
- refute Rails.application.middleware.include?(ActionDispatch::Flash)
+ 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 1246e20d94..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,17 +20,17 @@ 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'
+ get "foo"
- assert_not last_response.headers.has_key?('Cache-Control'), "Cache-Control should not be set"
+ 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'
+ app_file "public/about.html", "static"
add_to_config <<-CONFIG
config.public_file_server.headers = {
"Access-Control-Allow-Origin" => "http://rubyonrails.org",
@@ -38,19 +40,19 @@ module ApplicationTests
require "#{app_path}/config/environment"
- get '/about.html'
+ get "/about.html"
- assert_equal 'http://rubyonrails.org', last_response.headers["Access-Control-Allow-Origin"]
- assert_equal 'public, max-age=60', last_response.headers["Cache-Control"]
+ 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 "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
@@ -60,7 +62,7 @@ module ApplicationTests
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 5869ff64bc..4242daf39a 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,6 +25,8 @@ module ApplicationTests
boot!
assert_equal [
+ "Webpacker::DevServerProxy",
+ "ActionDispatch::HostAuthorization",
"Rack::Sendfile",
"ActionDispatch::Static",
"ActionDispatch::Executor",
@@ -31,19 +34,21 @@ module ApplicationTests
"Rack::Runtime",
"Rack::MethodOverride",
"ActionDispatch::RequestId",
- "Rails::Rack::Logger", # must come after Rack::MethodOverride to properly log overridden methods
+ "ActionDispatch::RemoteIp",
+ "Rails::Rack::Logger",
"ActionDispatch::ShowExceptions",
"ActionDispatch::DebugExceptions",
- "ActionDispatch::RemoteIp",
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
"ActiveRecord::Migration::CheckPending",
"ActionDispatch::Cookies",
"ActionDispatch::Session::CookieStore",
"ActionDispatch::Flash",
+ "ActionDispatch::ContentSecurityPolicy::Middleware",
"Rack::Head",
"Rack::ConditionalGet",
- "Rack::ETag"
+ "Rack::ETag",
+ "Rack::TempfileReaper"
], middleware
end
@@ -53,16 +58,18 @@ module ApplicationTests
boot!
assert_equal [
+ "Webpacker::DevServerProxy",
+ "ActionDispatch::HostAuthorization",
"Rack::Sendfile",
"ActionDispatch::Static",
"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",
"Rack::Head",
@@ -71,10 +78,41 @@ module ApplicationTests
], middleware
end
+ test "middleware dependencies" do
+ boot!
+
+ # The following array-of-arrays describes dependencies between
+ # middlewares: the first item in each list depends on the
+ # remaining items (and therefore must occur later in the
+ # middleware stack).
+
+ dependencies = [
+ # Logger needs a fully "corrected" request environment
+ %w(Rails::Rack::Logger Rack::MethodOverride ActionDispatch::RequestId ActionDispatch::RemoteIp),
+
+ # Serving public/ doesn't invoke user code, so it should skip
+ # locks etc
+ %w(ActionDispatch::Executor ActionDispatch::Static),
+
+ # Errors during reload must be reported
+ %w(ActionDispatch::Reloader ActionDispatch::ShowExceptions ActionDispatch::DebugExceptions),
+
+ # Outright dependencies
+ %w(ActionDispatch::Static Rack::Sendfile),
+ %w(ActionDispatch::Flash ActionDispatch::Session::CookieStore),
+ %w(ActionDispatch::Session::CookieStore ActionDispatch::Cookies),
+ ]
+
+ require "tsort"
+ sorted = TSort.tsort((middleware | dependencies.flatten).method(:each),
+ lambda { |n, &b| dependencies.each { |m, *ds| ds.each(&b) if m == n } })
+ assert_equal sorted, middleware
+ end
+
test "Rack::Cache is not included by default" do
boot!
- 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
@@ -82,7 +120,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
@@ -90,27 +128,27 @@ 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[2].args
end
test "removing Active Record omits its middleware" do
use_frameworks []
boot!
- assert !middleware.include?("ActiveRecord::Migration::CheckPending")
+ assert_not_includes middleware, "ActiveRecord::Migration::CheckPending"
end
test "includes executor" do
@@ -140,20 +178,20 @@ module ApplicationTests
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
@@ -161,21 +199,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
@@ -188,35 +226,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.fourth
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[5]
end
test "Rails.cache does respond to middleware" do
boot!
- assert_equal "Rack::Runtime", middleware.fifth
+ assert_equal "ActiveSupport::Cache::Strategy::LocalCache", middleware[5]
+ assert_equal "Rack::Runtime", middleware[6]
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.third
end
test "can't change middleware after it's built" do
boot!
- assert_raise RuntimeError do
+ assert_raise FrozenError do
app.config.middleware.use Rack::Config
end
end
@@ -228,35 +267,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..f0f1112f6b 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
@@ -99,30 +100,6 @@ module ApplicationTests
assert_nothing_raised { AppTemplate::Application.new }
end
- def test_initializers_run_on_different_applications_go_to_the_same_class
- application1 = AppTemplate::Application.new
- run_count = 0
-
- AppTemplate::Application.initializer :init0 do
- run_count += 1
- end
-
- application1.initializer :init1 do
- run_count += 1
- end
-
- AppTemplate::Application.new.initializer :init2 do
- run_count += 1
- end
-
- assert_equal 0, run_count, "Without loading the initializers, the count should be 0"
-
- # Set config.eager_load to false so that an eager_load warning doesn't pop up
- AppTemplate::Application.create { config.eager_load = false }.initialize!
-
- assert_equal 3, run_count, "There should have been three initializers that incremented the count"
- end
-
def test_consoles_run_on_different_applications_go_to_the_same_class
run_count = 0
AppTemplate::Application.console { run_count += 1 }
@@ -164,12 +141,12 @@ module ApplicationTests
app.config.some_setting = "a_different_setting"
assert_equal "a_different_setting", app.config.some_setting, "The configuration's some_setting should be set."
- new_config = Rails::Application::Configuration.new("root_of_application")
+ new_config = Rails::Application::Configuration.new(Pathname.new("root_of_application"))
new_config.some_setting = "some_setting_dude"
app.config = new_config
assert_equal "some_setting_dude", app.config.some_setting, "The configuration's some_setting should have changed."
- assert_equal "root_of_application", app.config.root, "The root should have changed to the new config's root."
+ assert_equal "root_of_application", app.config.root.to_s, "The root should have changed to the new config's root."
assert_equal new_config, app.config, "The application's config should have changed to the new config."
end
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
index dfe3fc9354..ab055c7648 100644
--- a/railties/test/application/per_request_digest_cache_test.rb
+++ b/railties/test/application/per_request_digest_cache_test.rb
@@ -1,33 +1,37 @@
-require 'isolation/abstract_unit'
-require 'rack/test'
-require 'minitest/mock'
+# frozen_string_literal: true
-require 'action_view'
-require 'active_support/testing/method_call_assertions'
+require "isolation/abstract_unit"
+require "rack/test"
+require "minitest/mock"
+
+require "action_view"
class PerRequestDigestCacheTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
- include ActiveSupport::Testing::MethodCallAssertions
include Rack::Test::Methods
setup do
build_app
- add_to_config 'config.consider_all_requests_local = true'
+ add_to_config "config.consider_all_requests_local = true"
- app_file 'app/models/customer.rb', <<-RUBY
+ 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
+ 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
+ app_file "app/controllers/customers_controller.rb", <<-RUBY
class CustomersController < ApplicationController
self.perform_caching = true
@@ -37,7 +41,7 @@ class PerRequestDigestCacheTest < ActiveSupport::TestCase
end
RUBY
- app_file 'app/views/customers/_customer.html.erb', <<-RUBY
+ app_file "app/views/customers/_customer.html.erb", <<-RUBY
<% cache customer do %>
<%= customer.name %>
<% end %>
@@ -49,17 +53,17 @@ class PerRequestDigestCacheTest < ActiveSupport::TestCase
teardown :teardown_app
test "digests are reused when rendering the same template twice" do
- get '/customers'
+ get "/customers"
assert_equal 200, last_response.status
values = ActionView::LookupContext::DetailsKey.digest_caches.first.values
- assert_equal [ '8ba099b7749542fe765ff34a6824d548' ], values
+ assert_equal [ "effc8928d0b33535c8a21d24ec617161" ], values
assert_equal %w(david dingus), last_response.body.split.map(&:strip)
end
test "template digests are cleared before a request" do
assert_called(ActionView::LookupContext::DetailsKey, :clear) do
- get '/customers'
+ get "/customers"
assert_equal 200, last_response.status
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 a229609e84..258066a7e6 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -1,14 +1,15 @@
+# frozen_string_literal: true
+
require "isolation/abstract_unit"
-require "active_support/core_ext/string/strip"
+require "env_helpers"
module ApplicationTests
module RakeTests
class RakeDbsTest < ActiveSupport::TestCase
- include ActiveSupport::Testing::Isolation
+ include ActiveSupport::Testing::Isolation, EnvHelpers
def setup
build_app
- boot_rails
FileUtils.rm_rf("#{app_path}/config/environments")
end
@@ -21,53 +22,126 @@ 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/rails 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/rails 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
+ test "db:create and db:drop respect environment setting" do
+ app_file "config/database.yml", <<-YAML
+ <% 1 %>
+ development:
+ database: <%= Rails.application.config.database %>
+ adapter: sqlite3
+ YAML
+
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.database = "db/development.sqlite3"
+ end
+ RUBY
+
+ db_create_and_drop("db/development.sqlite3", environment_loaded: false)
+ end
+
+ test "db:create and db:drop don't raise errors when loading YAML with multiline ERB" do
+ app_file "config/database.yml", <<-YAML
+ development:
+ database: <%=
+ Rails.application.config.database
+ %>
+ adapter: sqlite3
+ YAML
+
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.database = "db/development.sqlite3"
+ end
+ RUBY
+
+ db_create_and_drop("db/development.sqlite3", environment_loaded: false)
+ end
+
+ test "db:create and db:drop don't raise errors when loading YAML containing conditional statements in ERB" do
+ app_file "config/database.yml", <<-YAML
+ development:
+ <% if Rails.application.config.database %>
+ database: <%= Rails.application.config.database %>
+ <% else %>
+ database: db/default.sqlite3
+ <% end %>
+ adapter: sqlite3
+ YAML
+
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.database = "db/development.sqlite3"
+ end
+ RUBY
+
+ db_create_and_drop("db/development.sqlite3", environment_loaded: false)
+ end
+
+ test "db:create and db:drop don't raise errors when loading YAML containing multiple ERB statements on the same line" do
+ app_file "config/database.yml", <<-YAML
+ development:
+ database: <% if Rails.application.config.database %><%= Rails.application.config.database %><% else %>db/default.sqlite3<% end %>
+ adapter: sqlite3
+ YAML
+
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.database = "db/development.sqlite3"
+ end
+ RUBY
+
+ db_create_and_drop("db/development.sqlite3", environment_loaded: false)
+ end
+
def with_database_existing
Dir.chdir(app_path) do
set_database_url
- `bin/rails db:create`
+ rails "db:create"
yield
- `bin/rails db:drop`
+ rails "db:drop"
end
end
- test 'db:create failure because database exists' do
+ test "db:create failure because database exists" do
with_database_existing do
- output = `bin/rails db:create 2>&1`
+ output = rails("db:create")
assert_match(/already exists/, output)
- assert_equal 0, $?.exitstatus
end
end
def with_bad_permissions
Dir.chdir(app_path) do
+ skip "Can't avoid permissions as root" if Process.uid.zero?
+
set_database_url
FileUtils.chmod("-w", "db")
yield
@@ -75,48 +149,110 @@ module ApplicationTests
end
end
- test 'db:create failure because bad permissions' do
+ test "db:create failure because bad permissions" do
with_bad_permissions do
- output = `bin/rails db:create 2>&1`
- assert_match(/Couldn't create database/, output)
+ 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:drop failure because database does not exist' do
- Dir.chdir(app_path) do
- output = `bin/rails db:drop:_unsafe --trace 2>&1`
- assert_match(/does not exist/, output)
+ test "db:create works when schema cache exists and database does not exist" do
+ use_postgresql
+
+ begin
+ rails %w(db:create db:migrate db:schema:cache:dump)
+
+ rails "db:drop"
+ rails "db:create"
assert_equal 0, $?.exitstatus
+ ensure
+ rails "db:drop" rescue nil
end
end
- test 'db:drop failure because bad permissions' do
+ 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 = `bin/rails db:drop 2>&1`
+ 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)
+ test "db:truncate_all truncates all non-internal tables" do
Dir.chdir(app_path) do
- `bin/rails generate model book title:string;
- bin/rails db:migrate`
- output = `bin/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)
+ rails "generate", "model", "book", "title:string"
+ rails "db:migrate"
+ require "#{app_path}/config/environment"
+ Book.create!(title: "Remote")
+ assert_equal 1, Book.count
+ schema_migrations = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"")
+ internal_metadata = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"")
+
+ rails "db:truncate_all"
+
+ assert_equal(
+ schema_migrations,
+ ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"")
+ )
+ assert_equal(
+ internal_metadata,
+ ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"")
+ )
+ assert_equal 0, Book.count
end
end
- test 'db:migrate and db:migrate:status without database_url' do
+ test "db:truncate_all does not truncate any tables when environment is protected" do
+ with_rails_env "production" do
+ Dir.chdir(app_path) do
+ rails "generate", "model", "book", "title:string"
+ rails "db:migrate"
+ require "#{app_path}/config/environment"
+ Book.create!(title: "Remote")
+ assert_equal 1, Book.count
+ schema_migrations = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"")
+ internal_metadata = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"")
+ books = ActiveRecord::Base.connection.execute("SELECT * from \"books\"")
+
+ output = rails("db:truncate_all", allow_failure: true)
+ assert_match(/ActiveRecord::ProtectedEnvironmentError/, output)
+
+ assert_equal(
+ schema_migrations,
+ ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"")
+ )
+ assert_equal(
+ internal_metadata,
+ ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"")
+ )
+ assert_equal 1, Book.count
+ assert_equal(books, ActiveRecord::Base.connection.execute("SELECT * from \"books\""))
+ 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
@@ -124,170 +260,177 @@ module ApplicationTests
def db_schema_dump
Dir.chdir(app_path) do
- `bin/rails generate model book title:string;
- bin/rails 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/rails db:migrate db:fixtures:load`
+ rails "generate", "model", "book", "title:string"
+ reload
+ 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/rails db:migrate db:fixtures:load`
- require "#{app_path}/app/models/admin/book"
- assert_equal 2, Admin::Book.count
- end
+
+ rails "generate", "model", "admin::book", "title:string"
+ reload
+ rails "db:migrate", "db:fixtures:load"
+
+ 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/rails 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/rails 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/rails 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/rails db:schema:load`
- assert_equal '["posts", "comments", "schema_migrations", "ar_internal_metadata"]', 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/rails db:structure:load`
- assert_equal '["posts", "comments", "schema_migrations", "ar_internal_metadata", "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/rails 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/rails 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
- begin
- @old_rails_env = ENV["RAILS_ENV"]
- @old_rack_env = ENV["RACK_ENV"]
- ENV.delete "RAILS_ENV"
- ENV.delete "RACK_ENV"
+ test "db:setup loads schema and seeds database" do
+ @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
@@ -295,17 +438,135 @@ module ApplicationTests
end
RUBY
- app_file 'db/seeds.rb', <<-RUBY
- puts ActiveRecord::Base.connection_config[:database]
+ app_file "db/seeds.rb", <<-RUBY
+ puts ActiveRecord::Base.connection_config[:database]
+ RUBY
+
+ 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
+
+ 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
+
+ test "db:seed:replant truncates all non-internal tables and loads the seeds" do
+ Dir.chdir(app_path) do
+ rails "generate", "model", "book", "title:string"
+ rails "db:migrate"
+ require "#{app_path}/config/environment"
+ Book.create!(title: "Remote")
+ assert_equal 1, Book.count
+ schema_migrations = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"")
+ internal_metadata = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"")
+
+ app_file "db/seeds.rb", <<-RUBY
+ Book.create!(title: "Rework")
+ Book.create!(title: "Ruby Under a Microscope")
RUBY
+ rails "db:seed:replant"
+
+ assert_equal(
+ schema_migrations,
+ ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"")
+ )
+ assert_equal(
+ internal_metadata,
+ ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"")
+ )
+ assert_equal 2, Book.count
+ assert_not_predicate Book.where(title: "Remote"), :exists?
+ assert_predicate Book.where(title: "Rework"), :exists?
+ assert_predicate Book.where(title: "Ruby Under a Microscope"), :exists?
+ end
+ end
+
+ test "db:seed:replant does not truncate any tables and does not load the seeds when environment is protected" do
+ with_rails_env "production" do
Dir.chdir(app_path) do
- database_path = `bin/rails db:setup`
- assert_equal "development.sqlite3", File.basename(database_path.strip)
+ rails "generate", "model", "book", "title:string"
+ rails "db:migrate"
+ require "#{app_path}/config/environment"
+ Book.create!(title: "Remote")
+ assert_equal 1, Book.count
+ schema_migrations = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"")
+ internal_metadata = ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"")
+ books = ActiveRecord::Base.connection.execute("SELECT * from \"books\"")
+
+ app_file "db/seeds.rb", <<-RUBY
+ Book.create!(title: "Rework")
+ RUBY
+
+ output = rails("db:seed:replant", allow_failure: true)
+ assert_match(/ActiveRecord::ProtectedEnvironmentError/, output)
+
+ assert_equal(
+ schema_migrations,
+ ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.schema_migrations_table_name}\"")
+ )
+ assert_equal(
+ internal_metadata,
+ ActiveRecord::Base.connection.execute("SELECT * from \"#{ActiveRecord::Base.internal_metadata_table_name}\"")
+ )
+ assert_equal 1, Book.count
+ assert_equal(books, ActiveRecord::Base.connection.execute("SELECT * from \"books\""))
+ assert_not_predicate Book.where(title: "Rework"), :exists?
end
- ensure
- ENV["RAILS_ENV"] = @old_rails_env
- ENV["RACK_ENV"] = @old_rack_env
+ end
+ end
+
+ test "db:prepare setup the database" do
+ Dir.chdir(app_path) do
+ rails "generate", "model", "book", "title:string"
+ output = rails("db:prepare")
+ assert_match(/CreateBooks: migrated/, output)
+
+ output = rails("db:drop")
+ assert_match(/Dropped database/, output)
+
+ rails "generate", "model", "recipe", "title:string"
+ output = rails("db:prepare")
+ assert_match(/CreateBooks: migrated/, output)
+ assert_match(/CreateRecipes: migrated/, output)
end
end
end
diff --git a/railties/test/application/rake/dev_test.rb b/railties/test/application/rake/dev_test.rb
index deb9bc8dee..a87f453075 100644
--- a/railties/test/application/rake/dev_test.rb
+++ b/railties/test/application/rake/dev_test.rb
@@ -1,4 +1,6 @@
-require 'isolation/abstract_unit'
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
module ApplicationTests
module RakeTests
@@ -7,37 +9,55 @@ module ApplicationTests
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
+ test "dev:cache creates file and outputs message" do
Dir.chdir(app_path) do
- output = `rails dev:cache`
- assert File.exist?('tmp/caching-dev.txt')
- assert_match(/Development mode is now being cached/, output)
+ 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
+ test "dev:cache deletes file and outputs message" do
Dir.chdir(app_path) do
- `rails dev:cache` # Create caching file.
- output = `rails dev:cache` # Delete caching file.
- assert_not File.exist?('tmp/caching-dev.txt')
- assert_match(/Development mode is no longer being cached/, output)
+ 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 removes server.pid also' do
+ test "dev:cache touches tmp/restart.txt" do
Dir.chdir(app_path) do
- FileUtils.mkdir_p("tmp/pids")
- FileUtils.touch("tmp/pids/server.pid")
- `rake dev:cache`
- assert_not File.exist?("tmp/pids/server.pid")
+ 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 7e2519ae5a..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,211 +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::Current
- end
- MIGRATION
+ app_file "db/migrate/01_a_migration.bukkits.rb", <<-MIGRATION
+ class AMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
- output = `bin/rails db:migrate SCOPE=bukkits`
- assert_no_match(/create_table\(:users\)/, output)
- assert_no_match(/CreateUsers/, output)
- assert_no_match(/add_column\(:users, :email, :string\)/, output)
+ output = rails("db:migrate", "SCOPE=bukkits")
+ assert_no_match(/create_table\(:users\)/, output)
+ assert_no_match(/CreateUsers/, output)
+ assert_no_match(/add_column\(:users, :email, :string\)/, output)
- assert_match(/AMigration: migrated/, output)
+ assert_match(/AMigration: migrated/, output)
- output = `bin/rails db:migrate SCOPE=bukkits VERSION=0`
- assert_no_match(/drop_table\(:users\)/, output)
- assert_no_match(/CreateUsers/, output)
- assert_no_match(/remove_column\(:users, :email\)/, output)
+ # run all the migrations to test scope for down
+ output = rails("db:migrate")
+ assert_match(/CreateUsers: migrated/, output)
- assert_match(/AMigration: reverted/, output)
- end
+ output = rails("db:migrate", "SCOPE=bukkits", "VERSION=0")
+ assert_no_match(/drop_table\(:users\)/, output)
+ assert_no_match(/CreateUsers/, output)
+ assert_no_match(/remove_column\(:users, :email\)/, output)
+
+ assert_match(/AMigration: reverted/, output)
+
+ output = rails("db:migrate", "VERSION=0")
+
+ assert_match(/CreateUsers: reverted/, output)
end
- test '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/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 = `bin/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 "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/rails 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/rails 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 = 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 = `bin/rails db:migrate:status`
+ 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/rails db:rollback STEP=1`
- output = `bin/rails 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/rails db:migrate`
+ app_file "db/migrate/02_two_migration.rb", <<-MIGRATION
+ class TwoMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
- output = `bin/rails 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/rails db:rollback STEP=1`
- output = `bin/rails 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/rails 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/rails 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/rails db:rollback STEP=2`
- output = `bin/rails 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/rails db:migrate:redo`
- output = `bin/rails 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/rails db:migrate`
+ rails "generate", "model", "user", "username:string", "password:string"
+ rails "generate", "migration", "add_email_to_users", "email:string"
+ rails "db:migrate"
- output = `bin/rails db:migrate:status`
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{3,}\s+Create users/, output)
- assert_match(/up\s+\d{3,}\s+Add email to users/, output)
+ assert_match(/up\s+\d{3,}\s+Create users/, output)
+ assert_match(/up\s+\d{3,}\s+Add email to users/, output)
- `bin/rails db:rollback STEP=2`
- output = `bin/rails db:migrate:status`
+ rails "db:rollback", "STEP=2"
+ output = rails("db:migrate:status")
- assert_match(/down\s+\d{3,}\s+Create users/, output)
- assert_match(/down\s+\d{3,}\s+Add email to users/, output)
+ assert_match(/down\s+\d{3,}\s+Create users/, output)
+ assert_match(/down\s+\d{3,}\s+Add email to users/, output)
- `bin/rails db:migrate:redo`
- output = `bin/rails db:migrate:status`
+ rails "db:migrate:redo"
+ output = rails("db:migrate:status")
- assert_match(/up\s+\d{3,}\s+Create users/, output)
- assert_match(/up\s+\d{3,}\s+Add email to users/, output)
- end
+ assert_match(/up\s+\d{3,}\s+Create users/, output)
+ assert_match(/up\s+\d{3,}\s+Add email to users/, output)
end
- test 'running migrations with not timestamp head migration files' do
- Dir.chdir(app_path) do
-
- app_file "db/migrate/1_one_migration.rb", <<-MIGRATION
- class OneMigration < ActiveRecord::Migration::Current
- end
- MIGRATION
+ 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
+ app_file "db/migrate/02_two_migration.rb", <<-MIGRATION
+ class TwoMigration < ActiveRecord::Migration::Current
+ end
+ MIGRATION
- `bin/rails db:migrate`
+ rails "db:migrate"
- output = `bin/rails db:migrate:status`
+ output = rails("db:migrate:status")
- assert_match(/up\s+001\s+One migration/, output)
- assert_match(/up\s+002\s+Two migration/, output)
- end
+ assert_match(/up\s+001\s+One migration/, output)
+ assert_match(/up\s+002\s+Two migration/, output)
end
- test 'schema generation when dump_schema_after_migration is set' do
- 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/rails 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/rails 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/rails 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/rails 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/rails 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..147b8f94e1
--- /dev/null
+++ b/railties/test/application/rake/multi_dbs_test.rb
@@ -0,0 +1,251 @@
+# 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 db_prepare
+ Dir.chdir(app_path) do
+ generate_models_for_animals
+ output = rails("db:prepare")
+
+ ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config|
+ if db_config.spec_name == "primary"
+ assert_match(/CreateBooks: migrated/, output)
+ else
+ assert_match(/CreateDogs: migrated/, output)
+ end
+ 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
+ reload
+ 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
+
+ test "db:prepare works on all databases" do
+ require "#{app_path}/config/environment"
+ db_prepare
+ end
+ end
+ end
+end
diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb
index 50def9beb0..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/rails 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,7 +112,7 @@ module ApplicationTests
task :notes_custom do
tags = 'TODO|FIXME'
opts = { dirs: %w(lib test), tag: true }
- SourceAnnotationExtractor.enumerate(tags, opts)
+ Rails::SourceAnnotationExtractor.enumerate(tags, opts)
end
EOS
@@ -113,45 +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/rails 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 30f662a9be..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,40 +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
- Dir.chdir(app_path) do
- FileUtils.remove_dir('tmp')
- `rake restart`
- assert File.exist?('tmp/restart.txt')
- end
- end
-
- test 'rake restart removes server.pid also' do
+ test "rails restart should work even if tmp folder does not exist" do
Dir.chdir(app_path) do
- FileUtils.mkdir_p("tmp/pids")
- FileUtils.touch("tmp/pids/server.pid")
- `rake restart`
- assert_not File.exist?("tmp/pids/server.pid")
+ 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..9879d1f047
--- /dev/null
+++ b/railties/test/application/rake/routes_test.rb
@@ -0,0 +1,59 @@
+# 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_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create
+ rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create
+ rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
+ rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create
+ rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create
+ rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create
+ rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index
+ POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create
+ new_rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/new(.:format) rails/conductor/action_mailbox/inbound_emails#new
+ edit_rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format) rails/conductor/action_mailbox/inbound_emails#edit
+ rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#show
+ PATCH /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update
+ PUT /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update
+ DELETE /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#destroy
+ rails_conductor_inbound_email_reroute POST /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) rails/conductor/action_mailbox/reroutes#create
+ 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 1a786a3fd3..fe56e3d076 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -1,13 +1,14 @@
+# 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,22 +26,22 @@ module ApplicationTests
end
test "task is protected when previous migration was production" do
- Dir.chdir(app_path) do
- output = `bin/rails generate model product name:string;
- env RAILS_ENV=production bin/rails db:create db:migrate;
- env RAILS_ENV=production bin/rails db:test:prepare test 2>&1`
+ 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
- Dir.chdir(app_path) do
- output = `bin/rails generate model product name:string;
- env RAILS_ENV=test bin/rails db:create db:migrate;
- env RAILS_ENV=test bin/rails db:test:prepare test 2>&1`
+ with_rails_env "test" do
+ rails "generate", "model", "product", "name:string"
+ rails "db:create", "db:migrate"
+ output = rails("db:test:prepare", "test")
- refute_match(/ActiveRecord::ProtectedEnvironmentError/, output)
+ assert_no_match(/ActiveRecord::ProtectedEnvironmentError/, output)
end
end
@@ -55,7 +56,7 @@ module ApplicationTests
Rails.application.initialize!
RUBY
- assert_match("SuperMiddleware", Dir.chdir(app_path){ `bin/rails middleware` })
+ assert_match("SuperMiddleware", rails("middleware"))
end
def test_initializers_are_executed_in_rake_tasks
@@ -70,7 +71,7 @@ module ApplicationTests
end
RUBY
- output = Dir.chdir(app_path){ `bin/rails do_nothing` }
+ output = rails("do_nothing")
assert_match "Doing something...", output
end
@@ -83,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'
@@ -91,141 +92,34 @@ module ApplicationTests
end
RUBY
- output = Dir.chdir(app_path) { `bin/rails 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/rails do_nothing RAILS_ENV=production'),
- 'should not be pre-required for rake even eager_load=true'
- end
+ output = rails("do_nothing", "RAILS_ENV=production")
+ assert_match "There is nothing", output
end
def test_code_statistics_sanity
- assert_match "Code LOC: 18 Test LOC: 0 Code to Test Ratio: 1:0.0",
- Dir.chdir(app_path){ `bin/rails stats` }
- end
-
- def test_rails_routes_calls_the_route_inspector
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- end
- RUBY
-
- output = Dir.chdir(app_path){ `bin/rails routes` }
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
- end
-
- def test_rails_routes_with_controller_environment
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- get '/basketball', to: 'basketball#index'
- end
- RUBY
-
- output = Dir.chdir(app_path){ `bin/rails routes CONTROLLER=cart` }
- assert_equal ["Passing `CONTROLLER` to `bin/rails routes` is deprecated and will be removed in Rails 5.1.",
- "Please use `bin/rails routes -c controller_name` instead.",
- "Prefix Verb URI Pattern Controller#Action",
- " cart GET /cart(.:format) cart#show\n"].join("\n"), output
-
- output = Dir.chdir(app_path){ `bin/rails routes -c cart` }
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
- end
-
- def test_rails_routes_with_namespaced_controller_environment
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- namespace :admin do
- resource :post
- end
- end
- RUBY
- expected_output = [" Prefix Verb URI Pattern Controller#Action",
- " admin_post POST /admin/post(.:format) admin/posts#create",
- " new_admin_post GET /admin/post/new(.:format) admin/posts#new",
- "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit",
- " 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\n"].join("\n")
-
- output = Dir.chdir(app_path){ `bin/rails routes -c Admin::PostController` }
- assert_equal expected_output, output
-
- output = Dir.chdir(app_path){ `bin/rails routes -c PostController` }
- assert_equal expected_output, output
- end
-
- def test_rails_routes_with_global_search_key
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- post '/cart', to: 'cart#create'
- get '/basketballs', to: 'basketball#index'
- end
- RUBY
-
- output = Dir.chdir(app_path){ `bin/rails routes -g show` }
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
-
- output = Dir.chdir(app_path){ `bin/rails routes -g POST` }
- assert_equal "Prefix Verb URI Pattern Controller#Action\n POST /cart(.:format) cart#create\n", output
-
- output = Dir.chdir(app_path){ `bin/rails routes -g basketballs` }
- assert_equal " Prefix Verb URI Pattern Controller#Action\n" \
- "basketballs GET /basketballs(.:format) basketball#index\n", output
- end
-
- def test_rails_routes_with_controller_search_key
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- get '/basketball', to: 'basketball#index'
- end
- RUBY
-
- output = Dir.chdir(app_path){ `bin/rails routes -c cart` }
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
-
- output = Dir.chdir(app_path){ `bin/rails routes -c Cart` }
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
-
- output = Dir.chdir(app_path){ `bin/rails routes -c CartController` }
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
- end
-
- def test_rails_routes_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/rails routes` }
- You don't have any routes defined!
-
- Please add some routes in config/routes.rb.
-
- For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html.
- MESSAGE
+ 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
@@ -237,46 +131,42 @@ module ApplicationTests
end
RUBY
- output = Dir.chdir(app_path){ `bin/rails 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/rails 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/rails 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
+ assert_equal 2, Product.count
+ assert_equal 0, User.count
end
def test_loading_only_yml_fixtures
- Dir.chdir(app_path) do
- `bin/rails db:migrate`
- end
+ rails "db:migrate"
app_file "test/fixtures/products.csv", ""
require "#{rails_root}/config/environment"
- errormsg = Dir.chdir(app_path) { `bin/rails 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;
- RAILS_ENV=test bin/rails db:migrate test`
+ rails "generate", "scaffold", "user", "username:string", "password:string"
+ 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
@@ -290,92 +180,71 @@ module ApplicationTests
end
RUBY
- output = Dir.chdir(app_path) do
- `bin/rails generate scaffold user username:string password:string;
- RAILS_ENV=test bin/rails 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_by_default
- output = Dir.chdir(app_path) do
- `bin/rails generate model Product;
- bin/rails generate model Cart;
- bin/rails generate scaffold LineItems product:references cart:belongs_to;
- RAILS_ENV=test bin/rails db:migrate test`
+ 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/rails db:migrate;
- bin/rails db:test:clone 2>&1 --trace`
- end
- assert_match(/Execute db:test:clone_structure/, output)
- end
-
def test_db_test_prepare_when_using_sql_format
add_to_config "config.active_record.schema_format = :sql"
- output = Dir.chdir(app_path) do
- `bin/rails generate scaffold user username:string;
- bin/rails db:migrate;
- bin/rails 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/rails 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/rails 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/rails 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/rails 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/rails app:templates:copy`
- %w(controller mailer scaffold).each do |dir|
- assert File.exist?(File.join(app_path, 'lib', 'templates', 'erb', dir))
- end
- %w(controller helper scaffold_controller assets).each do |dir|
- assert File.exist?(File.join(app_path, 'lib', 'templates', 'rails', dir))
- end
+ rails "app:templates:copy"
+ %w(controller mailer scaffold).each do |dir|
+ assert File.exist?(File.join(app_path, "lib", "templates", "erb", dir))
+ end
+ %w(controller helper scaffold_controller assets).each do |dir|
+ assert File.exist?(File.join(app_path, "lib", "templates", "rails", dir))
end
end
@@ -383,18 +252,8 @@ module ApplicationTests
app_file "config/initializers/dummy.rb", "puts 'Hello, World!'"
app_file "template.rb", ""
- output = Dir.chdir(app_path) do
- `bin/rails app: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/rails 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 e51f32aaed..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
@@ -39,27 +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")
- assert_welcome get('/')
+ 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
@@ -89,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
@@ -102,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"
@@ -110,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
@@ -130,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
@@ -166,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
@@ -230,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 '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#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
+
+ get "/mapping"
+ assert_equal "/profile", last_response.body
- app_file 'config/routes.rb', <<-RUBY
+ 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
@@ -291,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!
@@ -309,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
@@ -346,45 +422,74 @@ module ApplicationTests
controller :bar, <<-RUBY
class BarController < ApplicationController
def index
- render text: "bar"
+ render plain: "bar"
+ 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#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 "/custom"
+ assert_equal "http://www.apple.com", last_response.body
+ assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url
- get '/bar'
- assert_equal 'bar', last_response.body
- assert_equal '/bar', Rails.application.routes.url_helpers.bar_path
+ 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
+ app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get 'foo', to: 'foo#index'
end
@@ -392,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'
+ 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
+
+ get "/custom"
+ assert_equal 404, last_response.status
+ assert_raises NoMethodError do
+ assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url
+ end
+
+ get "/mapping"
+ assert_equal 404, last_response.status
+ assert_raises NoMethodError do
+ assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new)
end
end
- test 'named routes are cleared when reloading' do
- 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
@@ -417,63 +534,149 @@ module ApplicationTests
controller :bar, <<-RUBY
class BarController < ApplicationController
def index
- render text: "bar"
+ render plain: "bar"
+ 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 ':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 9f15ce5e85..dfb9540093 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,33 +98,55 @@ 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_environment_option
+ assert_match "production", rails("runner", "-e", "production", "puts Rails.env")
+ end
+
+ def test_environment_option_is_properly_expanded
+ assert_match "production", rails("runner", "-e", "prod", "puts Rails.env")
end
def test_runner_detects_syntax_errors
- Dir.chdir(app_path) { `bin/rails runner "puts 'hello world" 2>&1` }
- refute $?.success?
+ 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
- Dir.chdir(app_path) { `bin/rails runner "iuiqwiourowe" 2>&1` }
- refute $?.success?
+ 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..5fe1b4e6e7
--- /dev/null
+++ b/railties/test/application/server_test.rb
@@ -0,0 +1,52 @@
+# 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 "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
+
+ Bundler.with_original_env do
+ pid = Process.spawn("bin/rails server -b localhost -P tmp/dummy.pid", chdir: app_path, in: replica, out: replica, err: replica)
+ assert_output("Listening", primary)
+
+ rails("restart")
+
+ assert_output("Restarting", primary)
+ assert_output("tcp://localhost:3000", 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 a1735db5b3..0bdd2b314d 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_backwards_compatibility
+ 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
@@ -73,10 +98,21 @@ module ApplicationTests
end
end
+ def test_run_channels
+ create_test_file :channels, "foo_channel"
+ create_test_file :channels, "bar_channel"
+
+ rails("test:channels").tap do |output|
+ assert_match "FooChannelTest", output
+ assert_match "BarChannelTest", output
+ assert_match "2 runs, 2 assertions, 0 failures", output
+ end
+ 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 +121,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 +132,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
@@ -106,13 +142,25 @@ module ApplicationTests
end
end
+ def test_run_mailboxes
+ create_test_file :mailboxes, "foo_mailbox"
+ create_test_file :mailboxes, "bar_mailbox"
+ create_test_file :models, "foo"
+
+ rails("test:mailboxes").tap do |output|
+ assert_match "FooMailboxTest", output
+ assert_match "BarMailboxTest", output
+ assert_match "2 runs, 2 assertions, 0 failures", output
+ end
+ 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 +169,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
@@ -130,16 +178,16 @@ module ApplicationTests
end
def test_run_all_suites
- suites = [:models, :helpers, :unit, :controllers, :mailers, :functional, :integration, :jobs]
+ suites = [:models, :helpers, :unit, :channels, :controllers, :mailers, :functional, :integration, :jobs, :mailboxes]
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
+ assert_match "10 runs, 10 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 +201,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 +222,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 +243,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 +260,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,27 +279,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('')
+ 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
+
+ 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('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
+ 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
@@ -264,15 +324,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
@@ -287,15 +347,15 @@ 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
+ app_file "test/models/post_test.rb", <<-RUBY
require 'test_helper'
class PostTest < ActiveSupport::TestCase
@@ -309,21 +369,21 @@ module ApplicationTests
assert true
end
- test "test line filter does not run this" do
+ 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
+ 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
+ app_file "test/models/account_test.rb", <<-RUBY
require 'test_helper'
class AccountTest < ActiveSupport::TestCase
@@ -343,7 +403,7 @@ module ApplicationTests
end
RUBY
- app_file 'test/models/post_test.rb', <<-RUBY
+ app_file "test/models/post_test.rb", <<-RUBY
require 'test_helper'
class PostTest < ActiveSupport::TestCase
@@ -363,27 +423,27 @@ module ApplicationTests
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
+ 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_filters_trigger_only_one_runnable
- app_file 'test/models/post_test.rb', <<-RUBY
+ app_file "test/models/post_test.rb", <<-RUBY
require 'test_helper'
class PostTest < ActiveSupport::TestCase
@@ -400,14 +460,14 @@ module ApplicationTests
RUBY
# 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
+ 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
+ app_file "test/models/post_test.rb", <<-RUBY
require 'test_helper'
class PostTest < ActiveSupport::TestCase
@@ -423,120 +483,373 @@ module ApplicationTests
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
+ 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
+ create_test_file :models, "post", pass: false, print: false
- output = run_test_command('test/models/post_test.rb')
- expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/models/post_test.rb:6\]:\nwups!\n\nbin/rails test test/models/post_test.rb:4\n\n\n\n}
+ 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
+ create_test_file :models, "post", pass: false
- output = run_test_command('test/models/post_test.rb')
- assert_match %r{Finished in.*\n\n1 runs, 1 assertions}, output
+ 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
+ create_test_file :models, "post", pass: false
assert_match(/Interrupt/,
- capture(:stderr) { run_test_command('test/models/post_test.rb --fail-fast') })
+ capture(:stderr) { run_test_command("test/models/post_test.rb --fail-fast", stderr: true) })
+ end
+
+ def test_run_in_parallel_with_processes
+ exercise_parallelization_regardless_of_machine_core_count(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
+ exercise_parallelization_regardless_of_machine_core_count(with: :threads)
+
+ 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
+ exercise_parallelization_regardless_of_machine_core_count(with: :processes)
+
+ 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
+ exercise_parallelization_regardless_of_machine_core_count(with: :processes)
+
+ 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') }
+ 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
+ create_test_file :models, "account"
+ create_test_file :models, "post", pass: false
# This specifically verifies TEST for backwards compatibility with rake test
- # as bin/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` }
+ # 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
+ 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'
- output = Dir.chdir(app_path) { `bin/rails db:create:all db:migrate && echo ".tables" | rails dbconsole` }
+ 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'
- Dir.chdir(app_path) { `bin/rails db:create:all` } # create all to avoid warnings
- output = Dir.chdir(app_path) { `bin/rails db:drop:all db:create:all db:migrate && echo ".tables" | rails dbconsole` }
+ 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` }
+ 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'` }
+ 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"
+ require "selenium/webdriver"
+
+ 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"
+ require "selenium/webdriver"
+
+ 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"
+ require "selenium/webdriver"
+
+ 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
@@ -551,7 +864,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'
@@ -564,7 +877,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
@@ -576,24 +889,122 @@ module ApplicationTests
end
def create_schema
- app_file 'db/schema.rb', ''
+ app_file "db/schema.rb", ""
end
- def create_test_file(path = :unit, name = 'test', pass: true)
+ def create_test_for_env(env)
+ app_file "test/models/environment_test.rb", <<-RUBY
+ require 'test_helper'
+ class JSONReporter < Minitest::AbstractReporter
+ def record(result)
+ puts JSON.dump(klass: result.class.name,
+ name: result.name,
+ failures: result.failures,
+ assertions: result.assertions,
+ time: result.time)
+ end
+ end
+
+ def Minitest.plugin_json_reporter_init(opts)
+ Minitest.reporter.reporters.clear
+ Minitest.reporter.reporters << JSONReporter.new
+ end
+
+ Minitest.extensions << "rails"
+ Minitest.extensions << "json_reporter"
+
+ # Minitest uses RubyGems to find plugins, and since RubyGems
+ # doesn't know about the Rails installation we're pointing at,
+ # Minitest won't require the Rails minitest plugin when we run
+ # these integration tests. So we have to manually require the
+ # Minitest plugin here.
+ require 'minitest/rails_plugin'
+
+ class EnvironmentTest < ActiveSupport::TestCase
+ def test_environment
+ test_db = ActiveRecord::Base.configurations[#{env.dump}]["database"]
+ db_file = ActiveRecord::Base.connection_config[:database]
+ assert_match(test_db, db_file)
+ assert_equal #{env.dump}, ENV["RAILS_ENV"]
+ end
+ end
+ RUBY
+ end
+
+ def create_test_file(path = :unit, name = "test", pass: true, print: true)
app_file "test/#{path}/#{name}_test.rb", <<-RUBY
require 'test_helper'
class #{name.camelize}Test < ActiveSupport::TestCase
def test_truth
- puts "#{name.camelize}Test"
+ puts "#{name.camelize}Test" if #{print}
assert #{pass}, 'wups!'
end
end
RUBY
end
+ def create_parallel_processes_test_file
+ app_file "test/models/parallel_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class ParallelTest < ActiveSupport::TestCase
+ RD1, WR1 = IO.pipe
+ RD2, WR2 = IO.pipe
+
+ test "one" do
+ WR1.close
+ assert_equal "x", RD1.read(1) # blocks until two runs
+
+ RD2.close
+ WR2.write "y" # Allow two to run
+ WR2.close
+ end
+
+ test "two" do
+ RD1.close
+ WR1.write "x" # Allow one to run
+ WR1.close
+
+ WR2.close
+ assert_equal "y", RD2.read(1) # blocks until one runs
+ end
+ end
+ RUBY
+ end
+
+ def create_parallel_threads_test_file
+ app_file "test/models/parallel_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class ParallelTest < ActiveSupport::TestCase
+ Q1 = Queue.new
+ Q2 = Queue.new
+ test "one" do
+ assert_equal "x", Q1.pop # blocks until two runs
+
+ Q2 << "y"
+ end
+
+ test "two" do
+ Q1 << "x"
+
+ assert_equal "y", Q2.pop # blocks until one runs
+ end
+ end
+ RUBY
+ end
+
+ def exercise_parallelization_regardless_of_machine_core_count(with:)
+ app_path("test/test_helper.rb") do |file_name|
+ file = File.read(file_name)
+ file.sub!(/parallelize\(([^\)]*)\)/, "parallelize(workers: 2, with: :#{with})")
+ File.write(file_name, file)
+ end
+ 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
@@ -605,17 +1016,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
- script 'generate controller admin/dashboard index'
+ rails "generate", "controller", "admin/dashboard", "index"
end
def run_migration
- Dir.chdir(app_path) { `bin/rails 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 85b003fce9..83e63718df 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.
+ # 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/rails 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..dc737089f6 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,16 +11,16 @@ 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
+ config.hosts << proc { true }
+ config.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
end
Rails.application.initialize!
@@ -28,7 +30,7 @@ module ApplicationTests
class ::OmgController < ::ApplicationController
def index
- render text: omg_path
+ render plain: omg_path
end
end
@@ -36,7 +38,7 @@ module ApplicationTests
get "/" => "omg#index", as: :omg
end
- require 'rack/test'
+ require "rack/test"
extend Rack::Test::Methods
get "/"
@@ -44,12 +46,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/application/zeitwerk_integration_test.rb b/railties/test/application/zeitwerk_integration_test.rb
new file mode 100644
index 0000000000..5d2e34433a
--- /dev/null
+++ b/railties/test/application/zeitwerk_integration_test.rb
@@ -0,0 +1,304 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "active_support/dependencies/zeitwerk_integration"
+
+class ZeitwerkIntegrationTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ end
+
+ def boot(env = "development")
+ app(env)
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ def deps
+ ActiveSupport::Dependencies
+ end
+
+ def decorated?
+ deps.singleton_class < deps::ZeitwerkIntegration::Decorations
+ end
+
+ test "ActiveSupport::Dependencies is decorated by default" do
+ boot
+
+ assert decorated?
+ assert Rails.autoloaders.zeitwerk_enabled?
+ assert_instance_of Zeitwerk::Loader, Rails.autoloaders.main
+ assert_instance_of Zeitwerk::Loader, Rails.autoloaders.once
+ assert_equal [Rails.autoloaders.main, Rails.autoloaders.once], Rails.autoloaders.to_a
+ end
+
+ test "ActiveSupport::Dependencies is not decorated in classic mode" do
+ add_to_config "config.autoloader = :classic"
+ boot
+
+ assert_not decorated?
+ assert_not Rails.autoloaders.zeitwerk_enabled?
+ assert_nil Rails.autoloaders.main
+ assert_nil Rails.autoloaders.once
+ assert_equal 0, Rails.autoloaders.count
+ end
+
+ test "autoloaders inflect with Active Support" do
+ app_file "config/initializers/inflections.rb", <<-RUBY
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
+ inflect.acronym 'RESTful'
+ end
+ RUBY
+
+ app_file "app/controllers/restful_controller.rb", <<-RUBY
+ class RESTfulController < ApplicationController
+ end
+ RUBY
+
+ boot
+
+ basename = "restful_controller"
+ abspath = "#{Rails.root}/app/controllers/#{basename}.rb"
+ camelized = "RESTfulController"
+
+ Rails.autoloaders.each do |autoloader|
+ assert_equal camelized, autoloader.inflector.camelize(basename, abspath)
+ end
+
+ assert RESTfulController
+ end
+
+ test "constantize returns the value stored in the constant" do
+ app_file "app/models/admin/user.rb", "class Admin::User; end"
+ boot
+
+ assert_same Admin::User, deps.constantize("Admin::User")
+ end
+
+ test "constantize raises if the constant is unknown" do
+ boot
+
+ assert_raises(NameError) { deps.constantize("Admin") }
+ end
+
+ test "safe_constantize returns the value stored in the constant" do
+ app_file "app/models/admin/user.rb", "class Admin::User; end"
+ boot
+
+ assert_same Admin::User, deps.safe_constantize("Admin::User")
+ end
+
+ test "safe_constantize returns nil for unknown constants" do
+ boot
+
+ assert_nil deps.safe_constantize("Admin")
+ end
+
+ test "autoloaded_constants returns autoloaded constant paths" do
+ app_file "app/models/admin/user.rb", "class Admin::User; end"
+ app_file "app/models/post.rb", "class Post; end"
+ boot
+
+ assert Admin::User
+ assert_equal ["Admin", "Admin::User"], deps.autoloaded_constants
+ end
+
+ test "autoloaded? says if a constant has been autoloaded" do
+ app_file "app/models/user.rb", "class User; end"
+ app_file "app/models/post.rb", "class Post; end"
+ boot
+
+ assert Post
+ assert deps.autoloaded?("Post")
+ assert deps.autoloaded?(Post)
+ assert_not deps.autoloaded?("User")
+ end
+
+ test "eager loading loads the application code" do
+ $zeitwerk_integration_test_user = false
+ $zeitwerk_integration_test_post = false
+
+ app_file "app/models/user.rb", "class User; end; $zeitwerk_integration_test_user = true"
+ app_file "app/models/post.rb", "class Post; end; $zeitwerk_integration_test_post = true"
+
+ boot("production")
+
+ assert $zeitwerk_integration_test_user
+ assert $zeitwerk_integration_test_post
+ end
+
+ test "eager loading loads code in engines" do
+ $test_blog_engine_eager_loaded = false
+
+ engine("blog") do |bukkit|
+ bukkit.write("lib/blog.rb", "class BlogEngine < Rails::Engine; end")
+ bukkit.write("app/models/post.rb", "Post = $test_blog_engine_eager_loaded = true")
+ end
+
+ boot("production")
+
+ assert $test_blog_engine_eager_loaded
+ end
+
+ test "eager loading loads anything managed by Zeitwerk" do
+ $zeitwerk_integration_test_user = false
+ app_file "app/models/user.rb", "class User; end; $zeitwerk_integration_test_user = true"
+
+ $zeitwerk_integration_test_extras = false
+ app_dir "extras"
+ app_file "extras/webhook_hacks.rb", "WebhookHacks = 1; $zeitwerk_integration_test_extras = true"
+
+ require "zeitwerk"
+ autoloader = Zeitwerk::Loader.new
+ autoloader.push_dir("#{app_path}/extras")
+ autoloader.setup
+
+ boot("production")
+
+ assert $zeitwerk_integration_test_user
+ assert $zeitwerk_integration_test_extras
+ end
+
+ test "autoload directories not present in eager load paths are not eager loaded" do
+ $zeitwerk_integration_test_user = false
+ app_file "app/models/user.rb", "class User; end; $zeitwerk_integration_test_user = true"
+
+ $zeitwerk_integration_test_lib = false
+ app_dir "lib"
+ app_file "lib/webhook_hacks.rb", "WebhookHacks = 1; $zeitwerk_integration_test_lib = true"
+
+ $zeitwerk_integration_test_extras = false
+ app_dir "extras"
+ app_file "extras/websocket_hacks.rb", "WebsocketHacks = 1; $zeitwerk_integration_test_extras = true"
+
+ add_to_config "config.autoload_paths << '#{app_path}/lib'"
+ add_to_config "config.autoload_once_paths << '#{app_path}/extras'"
+
+ boot("production")
+
+ assert $zeitwerk_integration_test_user
+ assert !$zeitwerk_integration_test_lib
+ assert !$zeitwerk_integration_test_extras
+
+ assert WebhookHacks
+ assert WebsocketHacks
+
+ assert $zeitwerk_integration_test_lib
+ assert $zeitwerk_integration_test_extras
+ end
+
+ test "autoload_paths are set as root dirs of main, and in the same order" do
+ boot
+
+ existing_autoload_paths = deps.autoload_paths.select { |dir| File.directory?(dir) }
+ assert_equal existing_autoload_paths, Rails.autoloaders.main.dirs
+ end
+
+ test "autoload_once_paths go to the once autoloader, and in the same order" do
+ extras = %w(e1 e2 e3)
+ extras.each do |extra|
+ app_dir extra
+ add_to_config %(config.autoload_once_paths << "\#{Rails.root}/#{extra}")
+ end
+
+ boot
+
+ extras = extras.map { |extra| "#{app_path}/#{extra}" }
+ extras.each do |extra|
+ assert_not_includes Rails.autoloaders.main.dirs, extra
+ end
+ assert_equal extras, Rails.autoloaders.once.dirs
+ end
+
+ test "clear reloads the main autoloader, and does not reload the once one" do
+ boot
+
+ $zeitwerk_integration_reload_test = []
+
+ main_autoloader = Rails.autoloaders.main
+ def main_autoloader.reload
+ $zeitwerk_integration_reload_test << :main_autoloader
+ super
+ end
+
+ once_autoloader = Rails.autoloaders.once
+ def once_autoloader.reload
+ $zeitwerk_integration_reload_test << :once_autoloader
+ super
+ end
+
+ ActiveSupport::Dependencies.clear
+
+ assert_equal %i(main_autoloader), $zeitwerk_integration_reload_test
+ end
+
+ test "verbose = true sets the dependencies logger if present" do
+ boot
+
+ logger = Logger.new(File::NULL)
+ ActiveSupport::Dependencies.logger = logger
+ ActiveSupport::Dependencies.verbose = true
+
+ Rails.autoloaders.each do |autoloader|
+ assert_same logger, autoloader.logger
+ end
+ end
+
+ test "verbose = true sets the Rails logger as fallback" do
+ boot
+
+ ActiveSupport::Dependencies.verbose = true
+
+ Rails.autoloaders.each do |autoloader|
+ assert_same Rails.logger, autoloader.logger
+ end
+ end
+
+ test "verbose = false sets loggers to nil" do
+ boot
+
+ ActiveSupport::Dependencies.verbose = true
+ Rails.autoloaders.each do |autoloader|
+ assert autoloader.logger
+ end
+
+ ActiveSupport::Dependencies.verbose = false
+ Rails.autoloaders.each do |autoloader|
+ assert_nil autoloader.logger
+ end
+ end
+
+ test "unhooks" do
+ boot
+
+ assert_equal Module, Module.method(:const_missing).owner
+ assert_equal :no_op, deps.unhook!
+ end
+
+ test "autoloaders.logger=" do
+ boot
+
+ logger = ->(_msg) { }
+ Rails.autoloaders.logger = logger
+
+ Rails.autoloaders.each do |autoloader|
+ assert_same logger, autoloader.logger
+ end
+
+ Rails.autoloaders.logger = Rails.logger
+
+ Rails.autoloaders.each do |autoloader|
+ assert_same Rails.logger, autoloader.logger
+ end
+
+ Rails.autoloaders.logger = nil
+
+ Rails.autoloaders.each do |autoloader|
+ assert_nil autoloader.logger
+ end
+ end
+end
diff --git a/railties/test/backtrace_cleaner_test.rb b/railties/test/backtrace_cleaner_test.rb
index 2dd74f8fd1..ec512b6b64 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, locals: []).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 cecc3908b3..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,7 +301,7 @@ class Animal
assert_equal 0, @code_statistics_calculator.methods
end
- test 'count rake tasks' do
+ test "count rake tasks" do
code = <<-'CODE'
task :test_task do
puts 'foo'
@@ -317,7 +319,7 @@ class Animal
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 4d80901217..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.join(@tmp_path, '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,22 +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])
+ @code_statistics = CodeStatistics.new(["tmp dir", @tmp_path])
end
end
- test 'ignores hidden files' do
- File.write File.join(@tmp_path, '.example.rb'), <<-CODE
+ test "ignores hidden files" do
+ File.write File.join(@tmp_path, ".example.rb"), <<-CODE
def foo
puts 'foo'
end
CODE
assert_nothing_raised do
- CodeStatistics.new(['hidden file', @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..9132c8b4af
--- /dev/null
+++ b/railties/test/command/base_test.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "rails/command"
+require "rails/commands/generate/generate_command"
+require "rails/commands/secrets/secrets_command"
+require "rails/commands/db/system/change/change_command"
+
+class Rails::Command::BaseTest < ActiveSupport::TestCase
+ test "printing commands" do
+ assert_equal %w(generate), Rails::Command::GenerateCommand.printing_commands
+ assert_equal %w(secrets:setup secrets:edit secrets:show), Rails::Command::SecretsCommand.printing_commands
+ assert_equal %w(db:system:change), Rails::Command::Db::System::ChangeCommand.printing_commands
+ end
+end
diff --git a/railties/test/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..f6df2b694a 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,50 @@ 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']
- assert_match(/\sspecial-production\s/, output)
- end
-
- def test_environment_option
- start ['--environment=special-production']
+ start ["-e", "special-production"]
assert_match(/\sspecial-production\s/, output)
end
- def test_rails_env_is_production_when_first_argument_is_p
- start ['p']
+ def test_e_option_is_properly_expanded
+ start ["-e", "prod"]
assert_match(/\sproduction\s/, output)
end
- def test_rails_env_is_test_when_first_argument_is_t
- start ['t']
- assert_match(/\stest\s/, output)
+ def test_environment_option
+ start ["--environment=special-production"]
+ assert_match(/\sspecial-production\s/, output)
end
- def test_rails_env_is_development_when_argument_is_d
- start ['d']
- assert_match(/\sdevelopment\s/, output)
- end
+ def test_rails_env_is_dev_when_environment_option_is_dev_and_dev_env_is_present
+ Rails::Command::ConsoleCommand.class_eval do
+ alias_method :old_environments, :available_environments
- 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']
+ define_method :available_environments do
+ ["dev"]
end
end
- options = stubbed_console.parse_arguments(['dev'])
- assert_match('dev', options[:environment])
+
+ assert_match("dev", parse_arguments(["-e", "dev"])[:environment])
+ 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 +117,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, :disable_sandbox
- def initialize(console)
- @console = console
- end
+ def initialize(console)
+ @console = console
+ end
- def config
- self
- end
+ def config
+ self
+ end
- def sandbox=(arg)
- @sandbox = arg
- 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..0ee36081c0
--- /dev/null
+++ b/railties/test/commands/credentials_test.rb
@@ -0,0 +1,135 @@
+# 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 "edit command properly expands environment option" do
+ assert_match(/access_key_id: 123/, run_edit_command(environment: "prod"))
+ Dir.chdir(app_path) do
+ assert File.exist?("config/credentials/production.key")
+ assert File.exist?("config/credentials/production.yml.enc")
+ end
+ end
+
+ test "edit command does not raise when an initializer tries to access non-existent credentials" do
+ app_file "config/initializers/raise_when_loaded.rb", <<-RUBY
+ Rails.application.credentials.missing_key!
+ RUBY
+
+ assert_match(/access_key_id: 123/, run_edit_command(environment: "qa"))
+ end
+
+ test "edit command generates template file when the file does not exist" do
+ FileUtils.rm("#{app_path}/config/credentials.yml.enc")
+ run_edit_command
+
+ output = run_show_command
+ assert_match(/access_key_id: 123/, output)
+ assert_match(/secret_key_base/, output)
+ end
+
+ test "show credentials" do
+ assert_match(/access_key_id: 123/, run_show_command)
+ end
+
+ test "show command raises 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
+
+ test "show command properly expands environment option" do
+ run_edit_command(environment: "production")
+
+ output = run_show_command(environment: "prod")
+ assert_match(/access_key_id: 123/, output)
+ assert_no_match(/secret_key_base/, output)
+ 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/db_system_change_test.rb b/railties/test/commands/db_system_change_test.rb
new file mode 100644
index 0000000000..2ff45a7878
--- /dev/null
+++ b/railties/test/commands/db_system_change_test.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rails/command"
+require "rails/commands/db/system/change/change_command"
+
+class Rails::Command::Db::System::ChangeCommandTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ setup { build_app }
+
+ teardown { teardown_app }
+
+ test "change to existing database" do
+ change_database(to: "sqlite3")
+
+ output = change_database(to: "sqlite3")
+
+ assert_match "identical config/database.yml", output
+ assert_match "gsub Gemfile", output
+ end
+
+ test "change to invalid database" do
+ output = change_database(to: "invalid-db")
+
+ assert_match <<~MSG.squish, output
+ Invalid value for --to option.
+ Supported preconfigurations are:
+ mysql, postgresql, sqlite3, oracle, frontbase,
+ ibm_db, sqlserver, jdbcmysql, jdbcsqlite3,
+ jdbcpostgresql, jdbc.
+ MSG
+ end
+
+ test "change to postgresql" do
+ output = change_database(to: "postgresql")
+
+ assert_match "force config/database.yml", output
+ assert_match "gsub Gemfile", output
+ end
+
+ test "change to mysql" do
+ output = change_database(to: "mysql")
+
+ assert_match "force config/database.yml", output
+ assert_match "gsub Gemfile", output
+ end
+
+ test "change to sqlite3" do
+ change_database(to: "postgresql")
+ output = change_database(to: "sqlite3")
+
+ assert_match "force config/database.yml", output
+ assert_match "gsub Gemfile", output
+ end
+
+ private
+ def change_database(to:, **options)
+ args = ["--to", to]
+ rails "db:system:change", args, **options
+ end
+end
diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb
index a5aa6c14a2..76a7cd055f 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,193 @@ 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
- 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])
- end
+ ENV["RAILS_ENV"] = "test"
+ ENV["RACK_ENV"] = nil
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])
+ 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_mysql
- start(adapter: 'mysql2', 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: 'mysql2', 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: "localhost", port: 1234, socket: "socket", username: "user", password: "qwerty", encoding: "UTF-8")
+ assert_not aborted
+ assert_equal [%w[mysql mysql5], "--host=localhost", "--port=1234", "--socket=socket", "--user=user", "--default-character-set=UTF-8", "-p", "db"], dbconsole.find_cmd_and_exec_args
end
def test_mysql_include_password
- start({adapter: 'mysql2', 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_database_and_environment
+ stub_available_environments(["development"]) do
+ dbconsole = parse_arguments(["--db", "custom", "-e", "development"])
+
+ assert_equal "development", dbconsole[:environment]
+ assert_equal "custom", dbconsole.database
+ end
+ end
+
+ def test_specifying_a_missing_database
+ app_db_config({}) do
+ e = assert_raises(ActiveRecord::AdapterNotSpecified) do
+ Rails::Command.invoke(:dbconsole, ["--db", "i_do_not_exist"])
+ end
+
+ assert_includes e.message, "'i_do_not_exist' database 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_connection_options_is_deprecate
+ command = Rails::Command::DbconsoleCommand.new([], ["-c", "custom"])
+ Rails::DBConsole.stub(:start, nil) do
+ assert_deprecated("`connection` option is deprecated") do
+ command.perform
+ end
+ end
+
+ assert_equal "custom", command.options["connection"]
+ assert_equal "custom", command.options["database"]
+ 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 \[options\]/, 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 \[options\]/, stdout)
end
attr_reader :aborted, :output
@@ -226,39 +276,74 @@ 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
- def start(config = {}, argv = [])
- dbconsole.stub(:config, config.stringify_keys) do
- dbconsole.stub(:arguments, argv) do
- capture_abort { dbconsole.start }
+ attr_reader :dbconsole
+
+ 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
+ def capture_abort
+ @aborted = false
+ @output = capture(:stderr) do
yield
rescue SystemExit
@aborted = true
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..793365ef3d
--- /dev/null
+++ b/railties/test/commands/initializers_test.rb
@@ -0,0 +1,48 @@
+# 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
+
+
+ test "prints out initializers only specified in environment option" do
+ add_to_config <<-RUBY
+ initializer(:set_added_development_module) { } if Rails.env.development?
+ initializer(:set_added_production_module) { } if Rails.env.production?
+ RUBY
+
+ output = run_initializers_command.split("\n")
+ assert_includes output, "AppTemplate::Application.set_added_development_module"
+ assert_not_includes output, "AppTemplate::Application.set_added_production_module"
+
+ output = run_initializers_command(["-e", "production"]).split("\n")
+ assert_not_includes output, "AppTemplate::Application.set_added_development_module"
+ assert_includes output, "AppTemplate::Application.set_added_production_module"
+ end
+
+ private
+ def run_initializers_command(args = [])
+ rails "initializers", args
+ 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..b4f927060e
--- /dev/null
+++ b/railties/test/commands/routes_test.rb
@@ -0,0 +1,319 @@
+# 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
+
+ assert_equal <<~OUTPUT, run_routes_command([ "-c", "PostController" ])
+ 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
+ rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
+ OUTPUT
+
+ assert_equal <<~OUTPUT, run_routes_command([ "-c", "UserPermissionController" ])
+ 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
+ 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
+
+ assert_equal <<~MESSAGE, run_routes_command([ "-g", "show" ])
+ Prefix Verb URI Pattern Controller#Action
+ cart GET /cart(.:format) cart#show
+ rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#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
+
+ assert_equal <<~MESSAGE, run_routes_command([ "-g", "POST" ])
+ Prefix Verb URI Pattern Controller#Action
+ POST /cart(.:format) cart#create
+ rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create
+ rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create
+ rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
+ rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create
+ rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create
+ rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create
+ POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create
+ rails_conductor_inbound_email_reroute POST /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) rails/conductor/action_mailbox/reroutes#create
+ rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
+ MESSAGE
+
+ assert_equal <<~MESSAGE, run_routes_command([ "-g", "basketballs" ])
+ Prefix Verb URI Pattern Controller#Action
+ basketballs GET /basketballs(.:format) basketball#index
+ MESSAGE
+ 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
+
+ assert_equal <<~OUTPUT, run_routes_command([ "-c", "Admin::PostController" ])
+ 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
+ OUTPUT
+
+ assert_equal <<~OUTPUT, run_routes_command([ "-c", "PostController" ])
+ 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
+ rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
+ OUTPUT
+
+ expected_permission_output = <<~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
+ OUTPUT
+
+ assert_equal expected_permission_output, run_routes_command([ "-c", "Admin::UserPermissionController" ])
+ assert_equal expected_permission_output, run_routes_command([ "-c", "UserPermissionController" ])
+ 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_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create
+ rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create
+ rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create
+ rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create
+ rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create
+ rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create
+ rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index
+ POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create
+ new_rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/new(.:format) rails/conductor/action_mailbox/inbound_emails#new
+ edit_rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format) rails/conductor/action_mailbox/inbound_emails#edit
+ rails_conductor_inbound_email GET /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#show
+ PATCH /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update
+ PUT /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#update
+ DELETE /rails/conductor/action_mailbox/inbound_emails/:id(.:format) rails/conductor/action_mailbox/inbound_emails#destroy
+ rails_conductor_inbound_email_reroute POST /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) rails/conductor/action_mailbox/reroutes#create
+ 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
+ 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
+
+ # rubocop:disable Layout/TrailingWhitespace
+ assert_equal <<~MESSAGE, run_routes_command([ "--expanded" ])
+ --[ Route 1 ]--------------
+ Prefix | cart
+ Verb | GET
+ URI | /cart(.:format)
+ Controller#Action | cart#show
+ --[ Route 2 ]--------------
+ Prefix | rails_amazon_inbound_emails
+ Verb | POST
+ URI | /rails/action_mailbox/amazon/inbound_emails(.:format)
+ Controller#Action | action_mailbox/ingresses/amazon/inbound_emails#create
+ --[ Route 3 ]--------------
+ Prefix | rails_mandrill_inbound_emails
+ Verb | POST
+ URI | /rails/action_mailbox/mandrill/inbound_emails(.:format)
+ Controller#Action | action_mailbox/ingresses/mandrill/inbound_emails#create
+ --[ Route 4 ]--------------
+ Prefix | rails_postmark_inbound_emails
+ Verb | POST
+ URI | /rails/action_mailbox/postmark/inbound_emails(.:format)
+ Controller#Action | action_mailbox/ingresses/postmark/inbound_emails#create
+ --[ Route 5 ]--------------
+ Prefix | rails_relay_inbound_emails
+ Verb | POST
+ URI | /rails/action_mailbox/relay/inbound_emails(.:format)
+ Controller#Action | action_mailbox/ingresses/relay/inbound_emails#create
+ --[ Route 6 ]--------------
+ Prefix | rails_sendgrid_inbound_emails
+ Verb | POST
+ URI | /rails/action_mailbox/sendgrid/inbound_emails(.:format)
+ Controller#Action | action_mailbox/ingresses/sendgrid/inbound_emails#create
+ --[ Route 7 ]--------------
+ Prefix | rails_mailgun_inbound_emails
+ Verb | POST
+ URI | /rails/action_mailbox/mailgun/inbound_emails/mime(.:format)
+ Controller#Action | action_mailbox/ingresses/mailgun/inbound_emails#create
+ --[ Route 8 ]--------------
+ Prefix | rails_conductor_inbound_emails
+ Verb | GET
+ URI | /rails/conductor/action_mailbox/inbound_emails(.:format)
+ Controller#Action | rails/conductor/action_mailbox/inbound_emails#index
+ --[ Route 9 ]--------------
+ Prefix |
+ Verb | POST
+ URI | /rails/conductor/action_mailbox/inbound_emails(.:format)
+ Controller#Action | rails/conductor/action_mailbox/inbound_emails#create
+ --[ Route 10 ]-------------
+ Prefix | new_rails_conductor_inbound_email
+ Verb | GET
+ URI | /rails/conductor/action_mailbox/inbound_emails/new(.:format)
+ Controller#Action | rails/conductor/action_mailbox/inbound_emails#new
+ --[ Route 11 ]-------------
+ Prefix | edit_rails_conductor_inbound_email
+ Verb | GET
+ URI | /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format)
+ Controller#Action | rails/conductor/action_mailbox/inbound_emails#edit
+ --[ Route 12 ]-------------
+ Prefix | rails_conductor_inbound_email
+ Verb | GET
+ URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format)
+ Controller#Action | rails/conductor/action_mailbox/inbound_emails#show
+ --[ Route 13 ]-------------
+ Prefix |
+ Verb | PATCH
+ URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format)
+ Controller#Action | rails/conductor/action_mailbox/inbound_emails#update
+ --[ Route 14 ]-------------
+ Prefix |
+ Verb | PUT
+ URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format)
+ Controller#Action | rails/conductor/action_mailbox/inbound_emails#update
+ --[ Route 15 ]-------------
+ Prefix |
+ Verb | DELETE
+ URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format)
+ Controller#Action | rails/conductor/action_mailbox/inbound_emails#destroy
+ --[ Route 16 ]-------------
+ Prefix | rails_conductor_inbound_email_reroute
+ Verb | POST
+ URI | /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format)
+ Controller#Action | rails/conductor/action_mailbox/reroutes#create
+ --[ Route 17 ]-------------
+ Prefix | rails_service_blob
+ Verb | GET
+ URI | /rails/active_storage/blobs/:signed_id/*filename(.:format)
+ Controller#Action | active_storage/blobs#show
+ --[ Route 18 ]-------------
+ 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 19 ]-------------
+ Prefix | rails_disk_service
+ Verb | GET
+ URI | /rails/active_storage/disk/:encoded_key/*filename(.:format)
+ Controller#Action | active_storage/disk#show
+ --[ Route 20 ]-------------
+ Prefix | update_rails_disk_service
+ Verb | PUT
+ URI | /rails/active_storage/disk/:encoded_token(.:format)
+ Controller#Action | active_storage/disk#update
+ --[ Route 21 ]-------------
+ Prefix | rails_direct_uploads
+ Verb | POST
+ URI | /rails/active_storage/direct_uploads(.:format)
+ Controller#Action | active_storage/direct_uploads#create
+ MESSAGE
+ # rubocop:enable Layout/TrailingWhitespace
+ ensure
+ IO.console.winsize = previous_console_winsize
+ 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 38a1605d1f..b78370a233 100644
--- a/railties/test/commands/server_test.rb
+++ b/railties/test/commands/server_test.rb
@@ -1,115 +1,260 @@
-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_environment_option_is_properly_expanded
+ args = ["-e", "prod"]
+ options = parse_arguments(args)
+ assert_equal "production", options[:environment]
+ 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_server_mistype_without_suggestion
+ output = run_command("--using", "t")
+ assert_match(/Could not find server "t"/, output)
+ assert_no_match(/Maybe you meant/, output)
+ 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
- server = Rails::Server.new
- assert_equal 1234, server.options[:Port]
+ options = parse_arguments
+ assert_equal 1234, options[:Port]
+ end
+ end
+
+ def test_environment_with_host
+ switch_env "HOST", "1.2.3.4" do
+ 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 = Rails::Server::Options.new.parse!(args)
- merged_options = Rails::Server.new.default_options.merge(options)
- assert_equal nil, merged_options[:caching]
+ options = parse_arguments(args)
+ assert_nil options[:caching]
end
def test_caching_with_option
args = ["--dev-caching"]
- options = Rails::Server::Options.new.parse!(args)
+ options = parse_arguments(args)
assert_equal true, options[:caching]
args = ["--no-dev-caching"]
- options = Rails::Server::Options.new.parse!(args)
+ options = parse_arguments(args)
assert_equal false, options[:caching]
end
+ 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]
+
+ args = ["-e", "development", "--no-log-to-stdout"]
+ options = parse_arguments(args)
assert_equal false, options[:log_stdout]
- with_rack_env 'development' do
+ 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
@@ -121,15 +266,33 @@ class Rails::ServerTest < ActiveSupport::TestCase
def test_restart_command_contains_customized_options
original_args = ARGV.dup
- args = ["-p", "4567"]
+ args = %w(-p 4567 -b 127.0.0.1 -c dummy_config.ru -d -e test -P tmp/server.pid -C)
ARGV.replace args
- options = Rails::Server::Options.new.parse! args
- server = Rails::Server.new options
- expected = "bin/rails server -p 4567"
+ 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, server.default_options[:restart_cmd]
+ 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 = [])
+ command = Rails::Command::ServerCommand.new([], args)
+ command.send(:extract_environment_option_from_argument)
+ command.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/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 3b2b3c37d0..44d4e92256 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -1,6 +1,8 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/app/app_generator'
-require 'env_helpers'
+# 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
@@ -19,222 +21,353 @@ class ActionsTest < Rails::Generators::TestCase
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'
+ 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/
+ 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"$/
+ 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".freeze, "1.0.0".freeze
+ action :gem, -"frozen_gem", -"1.0.0"
- assert_file 'Gemfile', /^gem 'frozen_gem', '1.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
- assert_called_with(generator, :run, ['git init']) 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
assert_called_with(generator, :run, [ ["git rm README"], ["git add ."] ]) do
- action :git, rm: 'README', add: '.'
+ 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
- assert_called_with(generator, :run_ruby_script, ['bin/rails generate model MyModel', verbose: false]) do
- action :generate, 'model', 'MyModel'
+ run_generator
+ action :generate, "model", "MyModel"
+ assert_file "app/models/my_model.rb", /MyModel/
+ end
+
+ def test_generate_aborts_when_subprocess_fails_if_requested
+ run_generator
+ content = capture(:stderr) do
+ assert_raises SystemExit do
+ action :generate, "model", "MyModel:ADsad", abort_on_failure: true
+ action :generate, "model", "MyModel"
+ end
end
+ assert_match(/wrong constant name MyModel:aDsad/, content)
+ assert_no_file "app/models/my_model.rb"
end
- def test_rails_should_run_rake_command_with_default_env
+ def test_generate_should_run_command_without_env
+ assert_called_with(generator, :run, ["rails generate model MyModel name:string", verbose: false]) do
+ action :generate, "model", "MyModel", "name:string"
+ end
+ end
+
+ def test_rake_should_run_rake_command_with_default_env
assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=development", verbose: false]) do
with_rails_env nil do
- action :rake, 'log:clear'
+ action :rake, "log:clear"
end
end
end
- def test_rails_with_env_option_should_run_rake_command_in_env
- assert_called_with(generator, :run, ['rake log:clear RAILS_ENV=production', verbose: false]) do
- action :rake, 'log:clear', env: 'production'
+ def test_rake_with_env_option_should_run_rake_command_in_env
+ assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=production", verbose: false]) do
+ action :rake, "log:clear", env: "production"
end
end
- test "rails command with RAILS_ENV variable should run rake command in env" do
- assert_called_with(generator, :run, ['rake log:clear RAILS_ENV=production', verbose: false]) do
+ 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
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
+ 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
- test "rails command with sudo option should run rake command with sudo" 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
@@ -242,29 +375,29 @@ class ActionsTest < Rails::Generators::TestCase
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'
+ 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'
+ 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
+ assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=production", verbose: false]) do
with_rails_env "production" do
- action :rails_command, 'log:clear'
+ 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
+ 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'
+ action :rails_command, "log:clear", env: "production"
end
end
end
@@ -272,14 +405,16 @@ class ActionsTest < Rails::Generators::TestCase
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
+ action :rails_command, "log:clear", sudo: true
end
end
end
- def test_capify_should_run_the_capify_command
- assert_called_with(generator, :run, ['capify .', verbose: false]) do
- action :capify!
+ 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
@@ -287,12 +422,12 @@ class ActionsTest < Rails::Generators::TestCase
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)
+ route_path = File.expand_path("config/routes.rb", destination_root)
# runs first time, not asserting
action :route, "root 'welcome#index'"
@@ -312,10 +447,10 @@ 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.open(route_path, "wb") { |file| file.write(content) }
+ File.write(route_path, content)
routes = <<-F
Rails.application.routes.draw do
@@ -369,10 +504,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 8e1cd0891a..4b9878187b 100644
--- a/railties/test/generators/api_app_generator_test.rb
+++ b/railties/test/generators/api_app_generator_test.rb
@@ -1,20 +1,22 @@
-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
- Thor::Base.shell.send(:attr_accessor, :always_force)
+ Kernel.silence_warnings do
+ Thor::Base.shell.attr_accessor :always_force
@shell = Thor::Base.shell.new
- @shell.send(:always_force=, true)
+ @shell.always_force = true
end
end
@@ -33,27 +35,25 @@ 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 '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" do |content|
- assert_match(/config.api_only = true/, content)
- end
-
- assert_file "config/initializers/cors.rb"
-
- assert_file "config/initializers/wrap_parameters.rb"
-
+ 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, "--skip-action-cable"]
+ 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"
@@ -62,49 +62,120 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
end
end
- private
+ def test_generator_if_skip_action_mailer_is_given
+ run_generator [destination_root, "--api", "--skip-action-mailer"]
+ assert_file "config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/
+ assert_file "config/environments/development.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_file "config/environments/test.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_file "config/environments/production.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_no_directory "app/mailers"
+ assert_no_directory "test/mailers"
+ assert_no_directory "app/views"
+ end
- def default_files
- files = %W(
- .gitignore
- Gemfile
- Rakefile
- config.ru
- app/controllers
- app/mailers
- app/models
- app/views/layouts/mailer.html.erb
- app/views/layouts/mailer.text.erb
- 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
+ def test_app_update_does_not_generate_unnecessary_config_files
+ run_generator
+
+ generator = Rails::Generators::AppGenerator.new ["rails"],
+ { api: true, update: true }, { destination_root: destination_root, shell: @shell }
+ quietly { generator.send(:update_config_files) }
+
+ assert_no_file "config/initializers/cookies_serializer.rb"
+ assert_no_file "config/initializers/assets.rb"
+ assert_no_file "config/initializers/content_security_policy.rb"
end
- def skipped_files
- %w(app/assets
- app/helpers
- app/views/layouts/application.html.erb
- config/initializers/assets.rb
- config/initializers/cookies_serializer.rb
- config/initializers/session_store.rb
- config/initializers/request_forgery_protection.rb
- config/initializers/per_form_csrf_tokens.rb
- lib/assets
- vendor/assets
- test/helpers
- tmp/cache/assets)
+ def test_app_update_does_not_generate_unnecessary_bin_files
+ run_generator
+
+ generator = Rails::Generators::AppGenerator.new ["rails"],
+ { api: true, update: true }, { destination_root: destination_root, shell: @shell }
+ quietly { generator.send(:update_bin_files) }
+
+ assert_no_file "bin/yarn"
end
+
+ private
+
+ 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 0572a23df9..d30cd9e718 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -1,54 +1,98 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/app/app_generator'
-require 'generators/shared_generator_tests'
+# 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/cable.yml
+ 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
+ test/channels/application_cable/connection_test.rb
test/controllers
test/models
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
@@ -62,13 +106,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' => 'reload'/)
- assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+'application', 'data-turbolinks-track' => 'reload'/)
+ 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
@@ -77,7 +130,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
@@ -88,12 +141,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
@@ -104,7 +157,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`
@@ -131,24 +184,22 @@ class AppGeneratorTest < Rails::Generators::TestCase
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/
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]
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 "myapp/config/initializers/session_store.rb", /_myapp_session/
end
end
- def test_new_application_use_json_serialzier
+ def test_new_application_use_json_serializer
run_generator
assert_file("config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/)
@@ -157,51 +208,51 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_new_application_not_include_api_initializers
run_generator
- assert_no_file 'config/initializers/cors.rb'
+ assert_no_file "config/initializers/cors.rb"
+ end
+
+ 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_keep_the_cookie_serializer_if_it_is_already_configured
- app_root = File.join(destination_root, 'myapp')
+ def test_new_application_load_defaults
+ app_root = File.join(destination_root, "myfirstapp")
run_generator [app_root]
- stub_rails_application(app_root) do
- generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/)
+ output = nil
+
+ assert_file "#{app_root}/config/application.rb", /\s+config\.load_defaults #{Rails::VERSION::STRING.to_f}/
+
+ Dir.chdir(app_root) do
+ output = `SKIP_REQUIRE_WEBPACKER=true ./bin/rails r "puts Rails.application.config.assets.unknown_asset_fallback"`
end
- end
- def test_rails_update_does_not_create_callback_terminator_initializer
- app_root = File.join(destination_root, 'myapp')
- run_generator [app_root]
+ assert_equal "false\n", output
+ end
- FileUtils.rm("#{app_root}/config/initializers/callback_terminator.rb")
+ def test_csp_initializer_include_connect_src_example
+ run_generator
- 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/callback_terminator.rb"
+ assert_file "config/initializers/content_security_policy.rb" do |content|
+ assert_match(/# policy\.connect_src/, content)
end
end
- def test_rails_update_does_not_remove_callback_terminator_initializer_if_already_present
- app_root = File.join(destination_root, 'myapp')
+ def test_app_update_keep_the_cookie_serializer_if_it_is_already_configured
+ app_root = File.join(destination_root, "myapp")
run_generator [app_root]
- FileUtils.touch("#{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/callback_terminator.rb"
+ 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_marshal_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")
@@ -215,99 +266,242 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
- def test_rails_update_dont_set_file_watcher
- 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]
+ assert_no_file "#{app_root}/config/initializers/new_framework_defaults_6_0.rb"
+
stub_rails_application(app_root) do
- generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
+ generator = Rails::Generators::AppGenerator.new ["rails"], { update: true }, { destination_root: app_root, shell: @shell }
generator.send(:app_const)
quietly { generator.send(:update_config_files) }
- assert_file "#{app_root}/config/environments/development.rb" do |content|
- assert_match(/# config.file_watcher/, content)
- end
+
+ assert_file "#{app_root}/config/initializers/new_framework_defaults_6_0.rb"
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_does_not_create_rack_cors
+ 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")
-
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/active_record_belongs_to_required_by_default.rb"
+ 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/active_record_belongs_to_required_by_default.rb"
+ assert_file "#{app_root}/config/initializers/cors.rb"
end
end
- def test_rails_update_does_not_create_ssl_options_by_default
- app_root = File.join(destination_root, 'myapp')
- run_generator [app_root]
-
- FileUtils.rm("#{app_root}/config/initializers/ssl_options.rb")
+ 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"], [], destination_root: app_root, shell: @shell
+ 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_config_files) }
- assert_no_file "#{app_root}/config/initializers/ssl_options.rb"
+ 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_rails_update_does_not_remove_ssl_options_if_already_present
- app_root = File.join(destination_root, 'myapp')
- run_generator [app_root]
-
- FileUtils.touch("#{app_root}/config/initializers/ssl_options.rb")
+ 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"], [], destination_root: app_root, shell: @shell
+ 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_file "#{app_root}/config/initializers/ssl_options.rb"
+
+ assert_no_file "#{app_root}/config/initializers/assets.rb"
end
end
- def test_rails_update_does_not_create_rack_cors
- app_root = File.join(destination_root, 'myapp')
- run_generator [app_root]
+ 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"]
- 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"
+ FileUtils.cd(app_root) do
+ quietly { system("bin/rails app:update") }
end
+
+ assert_no_file "#{app_root}/config/spring.rb"
end
- def test_rails_update_does_not_remove_rack_cors_if_already_present
- app_root = File.join(destination_root, 'myapp')
- run_generator [app_root]
+ 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.touch("#{app_root}/config/initializers/cors.rb")
+ FileUtils.cd(app_root) do
+ quietly { system("bin/rails app:update") }
+ end
- 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"
+ 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
+
+ assert_no_file "#{app_root}/test/channels/application_cable/connection_test.rb"
+ 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_generator_skips_action_mailbox_when_skip_action_mailbox_is_given
+ run_generator [destination_root, "--skip-action-mailbox"]
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_mailbox\/engine["']/
+ end
+
+ def test_generator_skips_action_mailbox_when_skip_active_record_is_given
+ run_generator [destination_root, "--skip-active-record"]
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_mailbox\/engine["']/
+ end
+
+ def test_generator_skips_action_mailbox_when_skip_active_storage_is_given
+ run_generator [destination_root, "--skip-active-storage"]
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_mailbox\/engine["']/
+ end
+
+ def test_generator_skips_action_text_when_skip_action_text_is_given
+ run_generator [destination_root, "--skip-action-text"]
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_text\/engine["']/
+ end
+
+ def test_generator_skips_action_text_when_skip_active_record_is_given
+ run_generator [destination_root, "--skip-active-record"]
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_text\/engine["']/
+ end
+
+ def test_generator_skips_action_text_when_skip_active_storage_is_given
+ run_generator [destination_root, "--skip-active-storage"]
+ assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_text\/engine["']/
+ 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
+
+ FileUtils.cd(app_root) do
+ quietly { system("bin/rails app:update") }
+ end
+
+ assert_file "#{app_root}/config/cable.yml" do |content|
+ assert_match(/hyphenated_app/, content)
+ assert_no_match(/hyphenated-app/, content)
end
end
@@ -319,7 +513,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
@@ -332,17 +526,17 @@ class AppGeneratorTest < Rails::Generators::TestCase
if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcsqlite3-adapter"
else
- assert_gem "sqlite3"
+ assert_gem "sqlite3", "'~> 1.3', '>= 1.3.6'"
end
end
- def test_config_another_database
+ def test_config_mysql_database
run_generator([destination_root, "-d", "mysql"])
assert_file "config/database.yml", /mysql/
if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcmysql-adapter"
else
- assert_gem "mysql2", "'>= 0.3.18', '< 0.5'"
+ assert_gem "mysql2", "'>= 0.4.4'"
end
end
@@ -357,7 +551,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcpostgresql-adapter"
else
- assert_gem "pg", "'~> 0.18'"
+ assert_gem "pg", "'>= 0.18', '< 2.0'"
end
end
@@ -394,96 +588,21 @@ 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
- end
-
def test_generator_defaults_to_puma_version
run_generator [destination_root]
- assert_gem "puma", "'~> 3.0'"
+ assert_gem "puma", "'~> 3.11'"
end
def test_generator_if_skip_puma_is_given
run_generator [destination_root, "--skip-puma"]
assert_no_file "config/puma.rb"
- assert_file "Gemfile" do |content|
- assert_no_match(/puma/, content)
- end
- 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_no_file "app/models/application_record.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
- 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
+ assert_no_gem "puma"
end
def test_generator_has_assets_gems
run_generator
- assert_gem 'sass-rails'
- assert_gem 'uglifier'
- end
-
- 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(/jquery-rails/, content)
- assert_no_match(/sass-rails/, content)
- assert_no_match(/uglifier/, content)
- assert_no_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
- end
-
- def test_generator_if_skip_action_cable_is_given
- run_generator [destination_root, "--skip-action-cable"]
- assert_file "config/application.rb", /#\s+require\s+["']action_cable\/engine["']/
- assert_no_file "config/cable.yml"
- assert_no_file "app/assets/javascripts/cable.js"
- assert_no_file "app/channels"
- assert_file "Gemfile" do |content|
- assert_no_match(/redis/, content)
- end
+ assert_gem "sass-rails"
end
def test_action_cable_redis_gems
@@ -491,69 +610,68 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile", /^# gem 'redis'/
end
- def test_inclusion_of_javascript_runtime
- run_generator
- if defined?(JRUBY_VERSION)
- assert_gem "therubyrhino"
- else
- assert_file "Gemfile", /# gem 'therubyracer', platforms: :ruby/
- end
+ 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 "webdrivers"
+
+ assert_no_directory("test")
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_system_test_is_given
+ run_generator [destination_root, "--skip-system-test"]
+ assert_no_gem "capybara"
+ assert_no_gem "selenium-webdriver"
+ assert_no_gem "webdrivers"
+
+ 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'
+ assert_gem "byebug"
end
end
def test_inclusion_of_listen_related_configuration_by_default
run_generator
- if RbConfig::CONFIG['host_os'] =~ /darwin|linux/
+ if RbConfig::CONFIG["host_os"] =~ /darwin|linux/
assert_listen_related_configuration
else
assert_no_listen_related_configuration
@@ -561,17 +679,17 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_non_inclusion_of_listen_related_configuration_if_skip_listen
- run_generator [destination_root, '--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)
+ 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)
+ assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content)
end
end
end
@@ -598,55 +716,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'
+ 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["']/
- 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(/\Agem 'web-console'\z/, content)
+ assert_no_match(/\Agem 'web-console', '>= 3\.3\.0'\z/, content)
end
end
@@ -655,32 +766,66 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile" do |content|
assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content)
- assert_no_match(/\Agem 'web-console'\z/, 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_generation_use_original_bundle_environment
+ generator([destination_root], skip_webpack_install: true)
+
+ mock_original_env = -> do
+ { "BUNDLE_RUBYONRAILS__ORG" => "user:pass" }
+ end
+
+ ensure_environment_is_set = -> *_args do
+ assert_equal "user:pass", ENV["BUNDLE_RUBYONRAILS__ORG"]
+ end
+
+ Bundler.stub :original_env, mock_original_env do
+ generator.stub :exec_bundle_command, ensure_environment_is_set do
+ quietly { generator.invoke_all }
+ end
+ end
+ 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"
- command_check = -> command do
- @binstub_called ||= 0
- case command
- when 'install'
- # Called when running bundle, we just want to stub it so nothing to do here.
- when 'exec spring binstub --all'
- @binstub_called += 1
- assert_equal 1, @binstub_called, "exec spring binstub --all expected to be called once, but was called #{@install_called} times."
- end
- end
+ generator([destination_root], skip_webpack_install: true)
- generator.stub :bundle_command, command_check do
- quietly { generator.invoke_all }
- end
+ assert_bundler_command_called("exec spring binstub --all")
end
def test_spring_no_fork
@@ -688,67 +833,151 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_called_with(Process, :respond_to?, [[:fork], [:fork]], returns: false) do
run_generator
- assert_file "Gemfile" do |content|
- assert_no_match(/spring/, content)
- end
+ assert_no_gem "spring"
end
end
def test_skip_spring
run_generator [destination_root, "--skip-spring"]
- assert_no_file 'config/spring.rb'
- assert_file "Gemfile" do |content|
- assert_no_match(/spring/, content)
- end
+ 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_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
- assert_file '.gitignore' do |content|
- assert_no_match(/sqlite/i, content)
+ assert content.end_with?("\n"), "expected .ruby-version to end with newline"
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(
@@ -766,7 +995,6 @@ class AppGeneratorTest < Rails::Generators::TestCase
test/helpers
test/integration
tmp
- vendor/assets/stylesheets
)
folders_with_keep.each do |folder|
assert_file("#{folder}/.keep")
@@ -775,7 +1003,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)
@@ -787,18 +1015,18 @@ 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
check_open = -> *args do
- assert_equal [ path, 'Accept' => 'application/x-thor-template' ], args
+ assert_equal [ path, "Accept" => "application/x-thor-template" ], args
template
end
- sequence = ['install', 'exec spring binstub --all', 'echo ran after_bundle']
- @sequence_step ||= 0
- ensure_bundler_first = -> command do
+ 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
@@ -806,51 +1034,96 @@ class AppGeneratorTest < Rails::Generators::TestCase
generator([destination_root], template: path).stub(:open, check_open, template) do
generator.stub(:bundle_command, ensure_bundler_first) do
generator.stub(:run, ensure_bundler_first) do
- quietly { generator.invoke_all }
+ generator.stub(:rails_command, ensure_bundler_first) do
+ quietly { generator.invoke_all }
+ end
end
end
end
- assert_equal 3, @sequence_step
+ assert_equal 6, @sequence_step
end
- protected
+ def test_gitignore
+ run_generator
- def stub_rails_application(root)
- Rails.application.config.root = root
- Rails.application.class.stub(:name, "Myapp") do
- yield
+ assert_file ".gitignore" do |content|
+ assert_match(/config\/master\.key/, content)
end
end
- def action(*args, &block)
- capture(:stdout) { generator.send(*args, &block) }
+ def test_system_tests_directory_generated
+ run_generator
+
+ assert_directory("test/system")
+ assert_file("test/system/.keep")
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}["']$*/
+ 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
- def assert_listen_related_configuration
- assert_gem 'listen'
- assert_gem 'spring-watcher-listen'
+ private
+ def stub_rails_application(root)
+ Rails.application.config.root = root
+ Rails.application.class.stub(:name, "Myapp") do
+ yield
+ end
+ end
- assert_file 'config/environments/development.rb' do |content|
- assert_match(/^\s*config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content)
+ 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
- end
- def assert_no_listen_related_configuration
- assert_file 'Gemfile' do |content|
- assert_no_match(/listen/, content)
+ def assert_no_gem(gem)
+ assert_file "Gemfile" do |content|
+ assert_no_match(gem, content)
+ end
end
- assert_file 'config/environments/development.rb' do |content|
- assert_match(/^\s*# config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content)
+ 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, env = {}) 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
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
index cda9e8b910..1a25422c3c 100644
--- a/railties/test/generators/channel_generator_test.rb
+++ b/railties/test/generators/channel_generator_test.rb
@@ -1,12 +1,14 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/channel/channel_generator'
+# 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']
+ 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)
@@ -18,24 +20,87 @@ class ChannelGeneratorTest < Rails::Generators::TestCase
end
def test_channel_is_created
- run_generator ['chat']
+ run_generator ["chat"]
assert_file "app/channels/chat_channel.rb" do |channel|
assert_match(/class ChatChannel < ApplicationCable::Channel/, channel)
end
- assert_file "app/assets/javascripts/channels/chat.coffee" do |channel|
- assert_match(/App.cable.subscriptions.create "ChatChannel"/, channel)
+ 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']
+ 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/assets/javascripts/channels/chat.coffee"
+ 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_invokes_default_test_framework
+ run_generator %w(chat -t=test_unit)
+
+ assert_file "test/channels/chat_channel_test.rb" do |test|
+ assert_match(/class ChatChannelTest < ActionCable::Channel::TestCase/, test)
+ assert_match(/# test "subscribes" do/, test)
+ assert_match(/# assert subscription.confirmed\?/, test)
+ end
+ 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_no_file "test/channels/chat_channel_test.rb"
+
+ 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"
+
+ assert_no_file "test/channels/chat_channel_channel_test.rb"
+ assert_file "test/channels/chat_channel_test.rb"
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/db_system_change_generator_test.rb b/railties/test/generators/db_system_change_generator_test.rb
new file mode 100644
index 0000000000..d3d27b616a
--- /dev/null
+++ b/railties/test/generators/db_system_change_generator_test.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require "generators/generators_test_helper"
+require "rails/generators/rails/db/system/change/change_generator"
+
+module Rails
+ module Generators
+ module Db
+ module System
+ class ChangeGeneratorTest < Rails::Generators::TestCase
+ include GeneratorsTestHelper
+
+ setup do
+ copy_gemfile(
+ GemfileEntry.new("sqlite3", nil, "Use sqlite3 as the database for Active Record")
+ )
+ end
+
+ test "change to invalid database" do
+ output = capture(:stderr) do
+ run_generator ["--to", "invalid-db"]
+ end
+
+ assert_match <<~MSG.squish, output
+ Invalid value for --to option.
+ Supported preconfigurations are:
+ mysql, postgresql, sqlite3, oracle, frontbase,
+ ibm_db, sqlserver, jdbcmysql, jdbcsqlite3,
+ jdbcpostgresql, jdbc.
+ MSG
+ end
+
+ test "change to postgresql" do
+ run_generator ["--to", "postgresql"]
+
+ assert_file("config/database.yml") do |content|
+ assert_match "adapter: postgresql", content
+ assert_match "database: test_app", content
+ end
+
+ assert_file("Gemfile") do |content|
+ assert_match "# Use pg as the database for Active Record", content
+ assert_match "gem 'pg', '>= 0.18', '< 2.0'", content
+ end
+ end
+
+ test "change to mysql" do
+ run_generator ["--to", "mysql"]
+
+ assert_file("config/database.yml") do |content|
+ assert_match "adapter: mysql2", content
+ assert_match "database: test_app", content
+ end
+
+ assert_file("Gemfile") do |content|
+ assert_match "# Use mysql2 as the database for Active Record", content
+ assert_match "gem 'mysql2', '>= 0.4.4'", content
+ end
+ end
+
+ test "change to sqlite3" do
+ run_generator ["--to", "sqlite3"]
+
+ assert_file("config/database.yml") do |content|
+ assert_match "adapter: sqlite3", content
+ assert_match "db/development.sqlite3", content
+ end
+
+ assert_file("Gemfile") do |content|
+ assert_match "# Use sqlite3 as the database for Active Record", content
+ assert_match "gem 'sqlite3', '~> 1.3', '>= 1.3.6'", content
+ end
+ end
+
+ test "change from versioned gem to other versioned gem" do
+ run_generator ["--to", "sqlite3"]
+ run_generator ["--to", "mysql", "--force"]
+
+ assert_file("config/database.yml") do |content|
+ assert_match "adapter: mysql2", content
+ assert_match "database: test_app", content
+ end
+
+ assert_file("Gemfile") do |content|
+ assert_match "# Use mysql2 as the database for Active Record", content
+ assert_match "gem 'mysql2', '>= 0.4.4'", content
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/railties/test/generators/generated_attribute_test.rb b/railties/test/generators/generated_attribute_test.rb
index ee7c009305..bf60d6bc22 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
@@ -36,6 +38,16 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
assert_field_type :boolean, :check_box
end
+ def test_field_type_returns_rich_text_area
+ assert_field_type :rich_text, :rich_text_area
+ end
+
+ def test_field_type_returns_file_field
+ %w(attachment attachments).each do |attribute_type|
+ assert_field_type attribute_type, :file_field
+ end
+ end
+
def test_field_type_with_unknown_type_returns_text_field
%w(foo bar baz).each do |attribute_type|
assert_field_type attribute_type, :text_field
@@ -51,7 +63,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 +77,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 +86,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
@@ -82,71 +94,71 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
end
def test_default_value_is_nil
- %w(references belongs_to).each do |attribute_type|
+ %w(references belongs_to rich_text attachment attachments).each do |attribute_type|
assert_field_default_value attribute_type, nil
end
end
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 8ef44a8dcb..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
@@ -83,17 +85,17 @@ module Rails
def test_recommended_rails_versions
klass = make_builder_class
- generator = klass.start(['new', 'blah'])
+ 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', '< 4.2'], specifier_for['4.1.6.rc1']
- assert_equal ['~> 4.1.7', '>= 4.1.7.1'], specifier_for['4.1.7.1']
- assert_equal ['~> 4.1.7', '>= 4.1.7.1.2'], specifier_for['4.1.7.1.2']
- assert_equal ['>= 4.1.7.1.rc2', '< 4.2'], specifier_for['4.1.7.1.rc2']
- assert_equal ['>= 4.2.0.beta1', '< 4.3'], specifier_for['4.2.0.beta1']
- assert_equal ['>= 5.0.0.beta1', '< 5.1'], specifier_for['5.0.0.beta1']
+ assert_equal "~> 4.1.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
diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb
index b19a5a7144..8b42cb83db 100644
--- a/railties/test/generators/generators_test_helper.rb
+++ b/railties/test/generators/generators_test_helper.rb
@@ -1,15 +1,17 @@
-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'
+# 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
@@ -20,31 +22,85 @@ 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
+ GemfileEntry = Struct.new(:name, :version, :comment, :options, :commented_out) do
+ def initialize(name, version, comment, options = {}, commented_out = false)
+ super
+ end
+ end
+
def self.included(base)
base.class_eval do
destination File.join(Rails.root, "tmp")
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
+
+ def copy_gemfile(*gemfile_entries)
+ locals = gemfile_locals.merge(gemfile_entries: gemfile_entries)
+ gemfile = File.expand_path("../../lib/rails/generators/rails/app/templates/Gemfile.tt", __dir__)
+ gemfile = evaluate_template(gemfile, locals)
+ destination = File.join(destination_root)
+ File.write File.join(destination, "Gemfile"), gemfile
end
+ def evaluate_template(file, locals = {})
+ erb = if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
+ ERB.new(File.read(file), trim_mode: "-", eoutvar: "@output_buffer")
+ else
+ ERB.new(File.read(file), nil, "-", "@output_buffer")
+ end
+ context = Class.new do
+ locals.each do |local, value|
+ class_attribute local, default: value
+ end
+ end
+ erb.result(context.new.instance_eval("binding"))
+ end
+
+ private
+ def gemfile_locals
+ {
+ skip_active_storage: true,
+ depend_on_bootsnap: false,
+ depend_on_listen: false,
+ spring_install: false,
+ depends_on_system_test: false,
+ options: ActiveSupport::OrderedOptions.new,
+ }
+ end
end
diff --git a/railties/test/generators/helper_generator_test.rb b/railties/test/generators/helper_generator_test.rb
index add04f21a4..192839799e 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
@@ -28,12 +30,17 @@ class HelperGeneratorTest < Rails::Generators::TestCase
require "#{destination_root}/app/helpers/products_helper"
assert_nothing_raised do
- begin
- run_generator ["admin::products"]
- ensure
- # cleanup
- Object.send(:remove_const, :ProductsHelper)
- end
+ run_generator ["admin::products"]
+ ensure
+ # cleanup
+ Object.send(:remove_const, :ProductsHelper)
end
end
+
+ def test_helper_suffix_is_not_duplicated
+ run_generator %w(products_helper)
+
+ assert_no_file "app/helpers/products_helper_helper.rb"
+ assert_file "app/helpers/products_helper.rb"
+ end
end
diff --git a/railties/test/generators/integration_test_generator_test.rb b/railties/test/generators/integration_test_generator_test.rb
index d05ed76d24..2ec4895096 100644
--- a/railties/test/generators/integration_test_generator_test.rb
+++ b/railties/test/generators/integration_test_generator_test.rb
@@ -1,12 +1,25 @@
-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
+
+ def test_test_suffix_is_not_duplicated
+ run_generator %w(integration_test)
+
+ assert_no_file "test/integration/integration_test_test.rb"
+ assert_file "test/integration/integration_test.rb"
+ 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 8728b39dae..099e06c4d3 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,13 +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
- assert_file 'app/mailers/application_mailer.rb' do |mailer|
+ 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
@@ -30,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
@@ -48,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
@@ -67,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
@@ -84,6 +86,10 @@ class MailerGeneratorTest < Rails::Generators::TestCase
assert_match(%r(\sapp/views/notifier_mailer/bar\.text\.erb), view)
assert_match(/<%= @greeting %>/, view)
end
+
+ assert_file "app/views/layouts/mailer.text.erb" do |view|
+ assert_match(/<%= yield %>/, view)
+ end
end
def test_invokes_default_html_template_engine
@@ -97,6 +103,10 @@ class MailerGeneratorTest < Rails::Generators::TestCase
assert_match(%r(\sapp/views/notifier_mailer/bar\.html\.erb), view)
assert_match(/<%= @greeting %>/, view)
end
+
+ assert_file "app/views/layouts/mailer.html.erb" do |view|
+ assert_match(%r{<body>\n <%= yield %>\n </body>}, view)
+ end
end
def test_invokes_default_template_engine_even_with_no_action
@@ -109,7 +119,7 @@ class MailerGeneratorTest < Rails::Generators::TestCase
assert_match(/haml \[not found\]/, content)
end
- def test_mailer_with_namedspaced_mailer
+ def test_mailer_with_namespaced_mailer
run_generator ["Farm::Animal", "moos"]
assert_file "app/mailers/farm/animal_mailer.rb" do |mailer|
assert_match(/class Farm::AnimalMailer < ApplicationMailer/, mailer)
@@ -129,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
diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb
index 46154b7db2..540bed551b 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
@@ -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
@@ -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,6 +86,17 @@ 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}"]
@@ -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
@@ -230,6 +254,28 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
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_database_puts_migrations_in_configured_folder_with_aliases
+ with_secondary_database_configuration do
+ run_generator ["create_books", "--db=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"]
@@ -248,7 +294,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)
@@ -265,8 +311,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
@@ -309,6 +355,55 @@ 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
+
+ def test_add_migration_ignores_virtual_attributes
+ migration = "add_rich_text_content_to_messages"
+ run_generator [migration, "content:rich_text", "video:attachment", "photos:attachments"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_no_match(/add_column :messages, :content, :rich_text/, change)
+ assert_no_match(/add_column :messages, :video, :attachment/, change)
+ assert_no_match(/add_column :messages, :photos, :attachments/, change)
+ end
+ end
+ end
+
+ def test_create_table_migration_ignores_virtual_attributes
+ run_generator ["create_messages", "content:rich_text", "video:attachment", "photos:attachments"]
+ assert_migration "db/migrate/create_messages.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/create_table :messages/, change)
+ assert_no_match(/ t\.rich_text :content/, change)
+ assert_no_match(/ t\.attachment :video/, change)
+ assert_no_match(/ t\.attachments :photos/, change)
+ end
+ end
+ end
+
+ def test_remove_migration_with_virtual_attributes
+ migration = "remove_content_from_messages"
+ run_generator [migration, "content:rich_text", "video:attachment", "photos:attachments"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_no_match(/remove_column :messages, :content, :rich_text/, change)
+ assert_no_match(/remove_column :messages, :video, :attachment/, change)
+ assert_no_match(/remove_column :messages, :photos, :attachments/, change)
+ end
+ end
+ 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 ed6846abc3..c97cd17ec6 100644
--- a/railties/test/generators/model_generator_test.rb
+++ b/railties/test/generators/model_generator_test.rb
@@ -1,18 +1,16 @@
-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 test_application_record_skeleton_is_created
- run_generator
- assert_file "app/models/application_record.rb" do |record|
- assert_match(/class ApplicationRecord < ActiveRecord::Base/, record)
- assert_match(/self.abstract_class = true/, record)
- end
- end
+ def setup
+ super
+ Rails::Generators::ModelHelpers.skip_warn = false
+ end
def test_help_shows_invoked_generators_options
content = run_generator ["--help"]
@@ -34,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
@@ -43,24 +41,25 @@ class ModelGeneratorTest < Rails::Generators::TestCase
assert_no_migration "db/migrate/create_accounts.rb"
end
- def test_model_with_existent_application_record
- mkdir_p "#{destination_root}/app/models"
- touch "#{destination_root}/app/models/application_record.rb"
-
- Dir.chdir(destination_root) do
- run_generator ["account"]
- end
-
- assert_file "app/models/account.rb", /class Account < ApplicationRecord/
- 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/
@@ -71,7 +70,7 @@ 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
@@ -105,7 +104,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase
ActiveRecord::Base.pluralize_table_names = true
end
- def test_migration_with_namespaces_in_model_name_without_plurization
+ def test_migration_with_namespaces_in_model_name_without_pluralization
ActiveRecord::Base.pluralize_table_names = false
run_generator ["Gallery::Image"]
assert_migration "db/migrate/create_gallery_image", /class CreateGalleryImage < ActiveRecord::Migration\[[0-9.]+\]/
@@ -212,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\[[0-9.]+\]/
+ 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\[[0-9.]+\]/
+ 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/
@@ -242,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
@@ -250,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
@@ -258,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
@@ -300,7 +310,7 @@ 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
@@ -308,7 +318,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase
assert_file "test/fixtures/line_items.yml", /product: one\n cart: one/
assert_generated_fixture("test/fixtures/line_items.yml",
- {"one"=>{"product"=>"one", "cart"=>"one"}, "two"=>{"product"=>"two", "cart"=>"two"}})
+ "one" => { "product" => "one", "cart" => "one" }, "two" => { "product" => "two", "cart" => "two" })
end
def test_fixtures_use_the_references_ids_and_type
@@ -316,15 +326,15 @@ class ModelGeneratorTest < Rails::Generators::TestCase
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"=>"one", "product_type"=>"Product", "cart"=>"one"},
- "two"=>{"product"=>"two", "product_type"=>"Product", "cart"=>"two"}})
+ "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
@@ -343,13 +353,13 @@ 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
@@ -382,13 +392,35 @@ class ModelGeneratorTest < Rails::Generators::TestCase
end
end
+ 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
+
+ def test_database_puts_migrations_in_configured_folder_with_aliases
+ with_secondary_database_configuration do
+ run_generator ["account", "--db=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
+
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
@@ -396,10 +428,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
@@ -407,10 +439,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
@@ -458,15 +490,52 @@ 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
+ def test_model_with_rich_text_attribute_adds_has_rich_text
+ run_generator ["message", "content:rich_text"]
+ expected_file = <<~FILE
+ class Message < ApplicationRecord
+ has_rich_text :content
+ end
+ FILE
+ assert_file "app/models/message.rb", expected_file
+ end
+
+ def test_model_with_attachment_attribute_adds_has_one_attached
+ run_generator ["message", "video:attachment"]
+ expected_file = <<~FILE
+ class Message < ApplicationRecord
+ has_one_attached :video
+ end
+ FILE
+ assert_file "app/models/message.rb", expected_file
+ end
+
+ def test_model_with_attachments_attribute_adds_has_many_attached
+ run_generator ["message", "photos:attachments"]
+ expected_file = <<~FILE
+ class Message < ApplicationRecord
+ has_many_attached :photos
+ end
+ FILE
+ assert_file "app/models/message.rb", expected_file
+ end
+
+ def test_skip_virtual_fields_in_fixtures
+ run_generator ["message", "content:rich_text", "video:attachment", "photos:attachments"]
+
+ assert_generated_fixture("test/fixtures/messages.yml",
+ "one" => nil, "two" => nil)
+ end
+
private
def assert_generated_fixture(path, parsed_contents)
fixture_file = File.new File.expand_path(path, destination_root)
diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb
index 291f5e06c3..4e61b660d7 100644
--- a/railties/test/generators/named_base_test.rb
+++ b/railties/test/generators/named_base_test.rb
@@ -1,84 +1,108 @@
-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"
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
@@ -89,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 d76759a7d1..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,7 +102,7 @@ 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
@@ -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"
@@ -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"
@@ -336,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"
@@ -402,7 +405,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"
@@ -421,3 +424,13 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
/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 17a2c6a327..f45464f8d0 100644
--- a/railties/test/generators/plugin_generator_test.rb
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -1,6 +1,9 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/plugin/plugin_generator'
-require 'generators/shared_generator_tests'
+# 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
@@ -23,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
@@ -47,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
@@ -62,14 +69,29 @@ class PluginGeneratorTest < Rails::Generators::TestCase
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'
+ 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
@@ -79,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)
@@ -94,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
@@ -123,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
@@ -143,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
@@ -225,28 +221,27 @@ class PluginGeneratorTest < Rails::Generators::TestCase
def test_ensure_that_tests_work
run_generator
FileUtils.cd destination_root
- quietly { system 'bundle install' }
+ 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' }
+ 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"
@@ -254,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"/
@@ -262,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"
@@ -274,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"
@@ -288,33 +283,36 @@ 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/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)
@@ -322,86 +320,87 @@ class PluginGeneratorTest < Rails::Generators::TestCase
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'
+ 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.1.0'\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 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/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.1.0'\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 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/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.1.0'\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 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/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\.md"\]/
- 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
@@ -417,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
@@ -428,27 +427,26 @@ 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
+ def test_dummy_application_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)
+ assert_file "test/dummy/config/environments/development.rb" do |contents|
+ assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, contents)
end
end
@@ -462,22 +460,24 @@ class PluginGeneratorTest < Rails::Generators::TestCase
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_directory 'test/dummy/lib/tasks'
- assert_no_directory 'test/dummy/doc'
- assert_no_directory 'test/dummy/test'
- assert_no_directory 'test/dummy/vendor'
+ 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 '.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
@@ -485,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_no_match("gemspec", contents)
assert_match(/gem 'rails'/, contents)
assert_match_sqlite3(contents)
- assert_no_match(/# gem "jquery-rails"/, contents)
end
end
@@ -496,7 +495,7 @@ 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_no_match("gemspec", contents)
assert_match(/gem 'rails'/, contents)
assert_match_sqlite3(contents)
end
@@ -505,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')
+ 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")
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"]
@@ -528,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
@@ -569,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
@@ -580,8 +595,8 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
def test_skipping_useless_folders_generation_for_api_engines
- ['--full', '--mountable'].each do |option|
- run_generator [destination_root, option, '--api']
+ ["--full", "--mountable"].each do |option|
+ run_generator [destination_root, option, "--api"]
assert_no_directory "app/assets"
assert_no_directory "app/helpers"
@@ -592,7 +607,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
def test_application_controller_parent_for_mountable_api_plugins
- run_generator [destination_root, '--mountable', '--api']
+ run_generator [destination_root, "--mountable", "--api"]
assert_file "app/controllers/bukkits/application_controller.rb" do |content|
assert_match "ApplicationController < ActionController::API", content
@@ -600,16 +615,15 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
def test_dummy_api_application_for_api_plugins
- run_generator [destination_root, '--api']
+ 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']
+ run_generator [destination_root, "--full", "--api"]
assert_file "lib/bukkits/engine.rb" do |content|
assert_match "config.generators.api_only = true", content
@@ -617,7 +631,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
def test_scaffold_generator_for_mountable_api_plugins
- run_generator [destination_root, '--mountable', '--api']
+ run_generator [destination_root, "--mountable", "--api"]
capture(:stdout) do
`#{destination_root}/bin/rails g scaffold article`
@@ -634,7 +648,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
def test_model_with_existent_application_record_in_mountable_engine
- run_generator [destination_root, '--mountable']
+ run_generator [destination_root, "--mountable"]
capture(:stdout) do
`#{destination_root}/bin/rails g model article`
end
@@ -642,22 +656,8 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "app/models/bukkits/article.rb", /class Article < ApplicationRecord/
end
- def test_generate_application_record_when_does_not_exist_in_mountable_engine
- run_generator [destination_root, '--mountable']
- FileUtils.rm "#{destination_root}/app/models/bukkits/application_record.rb"
- capture(:stdout) do
- `#{destination_root}/bin/rails g model article`
- end
-
- assert_file "#{destination_root}/app/models/bukkits/application_record.rb" do |record|
- assert_match(/module Bukkits/, record)
- assert_match(/class ApplicationRecord < ActiveRecord::Base/, record)
- assert_match(/self.abstract_class = true/, record)
- end
- end
-
def test_generate_application_mailer_when_does_not_exist_in_mountable_engine
- run_generator [destination_root, '--mountable']
+ 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`
@@ -669,48 +669,81 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
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
+ 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
- check_open = -> *args do
- assert_equal [ path, 'Accept' => 'application/x-thor-template' ], args
- template
+ assert_file "#{destination_root}/app/views/layouts/bukkits/mailer.text.erb" do |view|
+ assert_match(/<%= yield %>/, view)
end
- sequence = ['install', '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
+ assert_file "#{destination_root}/app/views/layouts/bukkits/mailer.html.erb" do |view|
+ assert_match(%r{<body>\n <%= yield %>\n </body>}, view)
end
+ end
- generator([destination_root], template: path).stub(:open, check_open, template) do
- generator.stub(:bundle_command, ensure_bundler_first) do
- generator.stub(:run, ensure_bundler_first) do
- quietly { generator.invoke_all }
- 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_equal 2, @sequence_step
+ 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
-protected
- def action(*args, &block)
- silence(:stdout){ generator.send(*args, &block) }
- end
+ def test_app_update_generates_bin_file
+ run_generator [destination_root, "--mountable"]
- def default_files
- ::DEFAULT_PLUGIN_FILES
- end
+ Object.const_set("ENGINE_ROOT", destination_root)
+ FileUtils.rm("#{destination_root}/bin/rails")
- 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)
+ 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
+
+ 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
index 96c1b1d31f..528f8d88f9 100644
--- a/railties/test/generators/plugin_test_helper.rb
+++ b/railties/test/generators/plugin_test_helper.rb
@@ -1,5 +1,7 @@
-require 'abstract_unit'
-require 'tmpdir'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "tmpdir"
module PluginTestHelper
def create_test_file(name, pass: true)
@@ -15,7 +17,7 @@ module PluginTestHelper
RUBY
end
- def plugin_file(path, contents, mode: 'w')
+ 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
diff --git a/railties/test/generators/plugin_test_runner_test.rb b/railties/test/generators/plugin_test_runner_test.rb
index ef6359fece..89c3f1e496 100644
--- a/railties/test/generators/plugin_test_runner_test.rb
+++ b/railties/test/generators/plugin_test_runner_test.rb
@@ -1,12 +1,14 @@
-require 'generators/plugin_test_helper'
+# frozen_string_literal: true
+
+require "generators/plugin_test_helper"
class PluginTestRunnerTest < ActiveSupport::TestCase
include PluginTestHelper
def setup
- @destination_root = Dir.mktmpdir('bukkits')
+ @destination_root = Dir.mktmpdir("bukkits")
Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --skip-bundle` }
- plugin_file 'test/dummy/db/schema.rb', ''
+ plugin_file "test/dummy/db/schema.rb", ""
end
def teardown
@@ -14,20 +16,20 @@ class PluginTestRunnerTest < ActiveSupport::TestCase
end
def test_run_single_file
- create_test_file 'foo'
- create_test_file 'bar'
+ 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'
+ 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
+ create_test_file "account"
+ plugin_file "test/post_test.rb", <<-RUBY
require 'test_helper'
class PostTest < ActiveSupport::TestCase
@@ -42,50 +44,73 @@ class PluginTestRunnerTest < ActiveSupport::TestCase
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
+ 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'
+ 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
+ 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
+ create_test_file "post", pass: false
- output = run_test_command('test/post_test.rb')
+ 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
+ create_test_file "post", pass: false
- output = run_test_command('test/post_test.rb')
- assert_match %r{Finished in.*\n\n1 runs, 1 assertions}, output
+ 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
+ create_test_file "post", pass: false
assert_match(/Interrupt/,
- capture(:stderr) { run_test_command('test/post_test.rb --fail-fast') })
+ 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') }
+ 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"
diff --git a/railties/test/generators/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb
index addaf83bc8..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"]
@@ -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 c37e289f4b..1348744b0b 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
@@ -78,6 +80,15 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_controller_permit_attachment_attributes
+ run_generator ["Message", "video:attachment", "photos:attachments"]
+
+ assert_file "app/controllers/messages_controller.rb" do |content|
+ assert_match(/def message_params/, content)
+ assert_match(/params\.require\(:message\)\.permit\(:video, photos: \[\]\)/, content)
+ end
+ end
+
def test_helper_are_invoked_with_a_pluralized_name
run_generator
assert_file "app/helpers/users_helper.rb", /module UsersHelper/
@@ -172,6 +183,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,6 +216,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
Dir.chdir(engine_path) do
quietly { `bin/rails g controller dashboard foo` }
+ quietly { `bin/rails db:migrate RAILS_ENV=test` }
assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`)
end
end
@@ -193,6 +228,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
Dir.chdir(engine_path) do
quietly { `bin/rails g controller dashboard foo` }
+ quietly { `bin/rails db:migrate RAILS_ENV=test` }
assert_match(/2 runs, 2 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`)
end
end
@@ -230,6 +266,12 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
assert_match(/@user\.destroy/, m)
end
end
+
+ assert_no_file "app/views/users/index.html.erb"
+ assert_no_file "app/views/users/edit.html.erb"
+ assert_no_file "app/views/users/show.html.erb"
+ assert_no_file "app/views/users/new.html.erb"
+ assert_no_file "app/views/users/_form.html.erb"
end
def test_api_controller_tests
@@ -238,9 +280,18 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
assert_file "test/controllers/users_controller_test.rb" do |content|
assert_match(/class UsersControllerTest < ActionDispatch::IntegrationTest/, content)
assert_match(/test "should get index"/, 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)
+ 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
+
+ def test_api_only_generates_params_for_attachments
+ run_generator ["Message", "video:attachment", "photos:attachments", "--api"]
+
+ assert_file "app/controllers/messages_controller.rb" do |content|
+ assert_match(/def message_params/, content)
+ assert_match(/params\.require\(:message\)\.permit\(:video, photos: \[\]\)/, content)
+ end
+ end
end
diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb
index 5e45120704..bfa52a1beb 100644
--- a/railties/test/generators/scaffold_generator_test.rb
+++ b/railties/test/generators/scaffold_generator_test.rb
@@ -1,9 +1,11 @@
-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
- arguments %w(product_line title:string product:belongs_to user:references)
+ arguments %w(product_line title:string approved:boolean product:belongs_to user:references)
setup :copy_routes
@@ -11,10 +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/
+ assert_migration "db/migrate/create_product_lines.rb", /boolean :approved/
assert_migration "db/migrate/create_product_lines.rb", /references :user/
# Route
@@ -58,8 +61,17 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_file "test/controllers/product_lines_controller_test.rb" do |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_match(/post product_lines_url, params: \{ product_line: \{ approved: @product_line\.approved, 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: \{ approved: @product_line\.approved, 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(/check "Approved" if @product_line\.approved/, test)
+ assert_match(/assert_text "Product line was successfully updated"/, test)
end
# Views
@@ -74,8 +86,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 +95,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,7 +102,7 @@ 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/
@@ -141,6 +152,9 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
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 +167,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
@@ -168,6 +181,16 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
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
+
def test_scaffold_on_revoke
run_generator
run_generator ["product_line"], behavior: :revoke
@@ -187,6 +210,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 +222,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 +230,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"
@@ -252,8 +277,18 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_file "test/controllers/admin/roles_controller_test.rb",
/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 +298,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 +321,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 +333,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 +344,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 +358,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 +377,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 +422,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 +437,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 +451,57 @@ 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_attachments
+ run_generator ["message", "video:attachment", "photos:attachments", "images:attachments"]
+
+ assert_file "app/models/message.rb", /has_one_attached :video/
+ assert_file "app/models/message.rb", /has_many_attached :photos/
+
+ assert_file "app/controllers/messages_controller.rb" do |content|
+ assert_instance_method :message_params, content do |m|
+ assert_match(/permit\(:video, photos: \[\], images: \[\]\)/, m)
+ end
+ end
+
+ assert_file "app/views/messages/_form.html.erb" do |content|
+ assert_match(/^\W{4}<%= form\.file_field :video %>/, content)
+ assert_match(/^\W{4}<%= form\.file_field :photos, multiple: true %>/, 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
+
+ def test_scaffold_generator_database_with_aliases
+ with_secondary_database_configuration do
+ run_generator ["posts", "--db=secondary"]
+
+ assert_migration "db/secondary_migrate/create_posts.rb"
end
end
@@ -456,8 +524,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 +541,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
@@ -488,7 +561,27 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
`bin/rails g scaffold User name:string age:integer;
bin/rails db:migrate`
end
- assert_match(/8 runs, 13 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`)
+ 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_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
@@ -502,7 +595,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
`bin/rails g scaffold User name:string age:integer;
bin/rails db:migrate`
end
- assert_match(/8 runs, 13 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`)
+ assert_match(/8 runs, 10 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`)
end
end
@@ -533,4 +626,61 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_match(/6 runs, 8 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`)
end
end
+
+ def test_scaffold_on_invoke_inside_mountable_engine
+ Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --mountable` }
+ engine_path = File.join(destination_root, "bukkits")
+
+ Dir.chdir(engine_path) do
+ quietly { `bin/rails generate scaffold User name:string age:integer` }
+
+ assert File.exist?("app/models/bukkits/user.rb")
+ assert File.exist?("test/models/bukkits/user_test.rb")
+ assert File.exist?("test/fixtures/bukkits/users.yml")
+
+ assert File.exist?("app/controllers/bukkits/users_controller.rb")
+ assert File.exist?("test/controllers/bukkits/users_controller_test.rb")
+
+ assert File.exist?("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 e83d54890a..26ce487c5f 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,58 +7,38 @@ 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
- Thor::Base.shell.send(:attr_accessor, :always_force)
+ Kernel.silence_warnings do
+ Thor::Base.shell.attr_accessor :always_force
@shell = Thor::Base.shell.new
- @shell.send(:always_force=, true)
+ @shell.always_force = true
end
end
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)
-
- command_check = -> command do
- @install_called ||= 0
-
- case command
- when 'install'
- @install_called += 1
- assert_equal 1, @install_called, "install expected to be called once, but was called #{@install_called} times"
- 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
-
- 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
@@ -68,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
@@ -92,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)
@@ -101,58 +83,279 @@ 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
check_open = -> *args do
- assert_equal [ path, 'Accept' => 'application/x-thor-template' ], args
+ assert_equal [ path, "Accept" => "application/x-thor-template" ], args
template
end
- generator([destination_root], template: path).stub(:open, check_open, template) do
- quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) }
+ 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
- assert_not_called(generator([destination_root], skip_gemfile: true), :bundle_command) do
- 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
- assert_not_called(generator([destination_root], skip_bundle: true), :bundle_command) do
- quietly { generator.invoke_all }
- # skip_bundle is only about running bundle install, ensure the Gemfile is still
- # generated.
- assert_file 'Gemfile'
+ 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-mailbox",
+ "--skip-action-text",
+ "--skip-action-cable",
+ "--skip-sprockets"
+ ]
+
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']rails["']/
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']active_model\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']active_job\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']active_record\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']active_storage\/engine["']/
+ assert_file "#{application_path}/config/application.rb", /^require\s+["']action_controller\/railtie["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']action_mailer\/railtie["']/
+ unless generator_class.name == "Rails::Generators::PluginGenerator"
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']action_mailbox\/engine["']/
+ assert_file "#{application_path}/config/application.rb", /^# require\s+["']action_text\/engine["']/
end
+ 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')
+ 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(/^require\("@rails\/activestorage"\)\.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(/^require\("@rails\/activestorage"\)\.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
- assert_no_file('app/models/concerns/.keep')
+ 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"
+ assert_file "#{application_path}/config/initializers/assets.rb", /node_modules/
+ end
+
+ 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"
+
+ assert_file "#{application_path}/config/initializers/assets.rb" do |content|
+ assert_no_match(/node_modules/, content)
+ end
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..5742ba444d
--- /dev/null
+++ b/railties/test/generators/system_test_generator_test.rb
@@ -0,0 +1,33 @@
+# 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
+
+ def test_test_name_is_pluralized
+ run_generator %w(user)
+
+ assert_no_file "test/system/user_test.rb"
+ assert_file "test/system/users_test.rb"
+ end
+
+ def test_test_suffix_is_not_duplicated
+ run_generator %w(users_test)
+
+ assert_no_file "test/system/users_test_test.rb"
+ assert_file "test/system/users_test.rb"
+ 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
index d37e261fbb..bd102a32b5 100644
--- a/railties/test/generators/test_runner_in_engine_test.rb
+++ b/railties/test/generators/test_runner_in_engine_test.rb
@@ -1,12 +1,14 @@
-require 'generators/plugin_test_helper'
+# frozen_string_literal: true
+
+require "generators/plugin_test_helper"
class TestRunnerInEngineTest < ActiveSupport::TestCase
include PluginTestHelper
def setup
- @destination_root = Dir.mktmpdir('bukkits')
+ @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', ''
+ plugin_file "test/dummy/db/schema.rb", ""
end
def teardown
@@ -14,10 +16,10 @@ class TestRunnerInEngineTest < ActiveSupport::TestCase
end
def test_rerun_snippet_is_relative_path
- create_test_file 'post', pass: false
+ 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/rails test test/post_test.rb:4}
+ 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
diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb
index 291415858c..abdc04a8d3 100644
--- a/railties/test/generators_test.rb
+++ b/railties/test/generators_test.rb
@@ -1,6 +1,8 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/model/model_generator'
-require 'rails/generators/test_unit/model/model_generator'
+# 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
@@ -15,7 +17,7 @@ class GeneratorsTest < Rails::Generators::TestCase
end
def test_simple_invoke
- assert File.exist?(File.join(@path, 'generators', 'model_generator.rb'))
+ 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
@@ -23,33 +25,41 @@ class GeneratorsTest < Rails::Generators::TestCase
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
+ assert_no_match "Maybe you meant", 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'))
+ assert File.exist?(File.join(@path, "generators", "model_generator.rb"))
assert_called_with(Rails::Generators::ModelGenerator, :start, [["Account"], {}]) do
- warnings = capture(:stderr){ Rails::Generators.invoke :model, ["Account"] }
- assert warnings.empty?
+ warnings = capture(:stderr) { Rails::Generators.invoke :model, ["Account"] }
+ assert_empty warnings
end
end
@@ -108,14 +118,14 @@ class GeneratorsTest < Rails::Generators::TestCase
def test_invoke_with_nested_namespaces
model_generator = Minitest::Mock.new
model_generator.expect(:start, nil, [["Account"], {}])
- assert_called_with(Rails::Generators, :find_by_namespace, ['namespace', 'my:awesome'], returns: model_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)
@@ -123,19 +133,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
@@ -200,7 +210,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
@@ -214,7 +224,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
@@ -229,18 +239,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 52e0277633..3fcfaa9623 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
@@ -52,10 +65,7 @@ module TestHelpers
@app ||= begin
ENV["RAILS_ENV"] = env
- # FIXME: shush Sass warning spam, not relevant to testing Railties
- Kernel.silence_warnings do
- require "#{app_path}/config/environment"
- end
+ require "#{app_path}/config/environment"
Rails.application
end
@@ -64,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
@@ -77,87 +87,130 @@ module TestHelpers
resp = Array(resp)
assert_equal 200, resp[0]
- assert_match 'text/html', resp[1]["Content-Type"]
- assert_match 'charset=utf-8', resp[1]["Content-Type"]
+ 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
-
- def assert_success(resp)
- assert_equal 202, resp[0]
- end
-
- def assert_missing(resp)
- assert_equal 404, resp[0]
- end
-
- def assert_header(key, value, resp)
- assert_equal value, resp[1][key.to_s]
- end
-
- def assert_body(expected, resp)
- assert_equal expected, extract_body(resp)
- end
end
module Generation
# 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|
+ 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
+ variables:
+ statement_timeout: 1000
+ 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.hosts << proc { true }
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.
@@ -166,15 +219,16 @@ module TestHelpers
require "rails"
require "action_controller/railtie"
require "action_view/railtie"
- require 'action_dispatch/middleware/flash'
- @app = Class.new(Rails::Application)
+ @app = Class.new(Rails::Application) do
+ def self.name; "RailtiesTestApp"; end
+ end
+ @app.config.hosts << proc { true }
@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.secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
yield @app if block_given?
@app.initialize!
@@ -183,7 +237,7 @@ module TestHelpers
get "/" => "omg#index"
end
- require 'rack/test'
+ require "rack/test"
extend ::Rack::Test::Methods
end
@@ -191,12 +245,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
@@ -213,7 +267,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)
@@ -226,10 +280,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
@@ -238,16 +292,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
@@ -255,8 +385,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
@@ -264,8 +394,8 @@ 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
@@ -281,15 +411,21 @@ module TestHelpers
def remove_from_file(file, str)
contents = File.read(file)
- contents.sub!(/#{str}/, '')
+ contents.sub!(/#{str}/, "")
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 app_dir(path)
+ FileUtils.mkdir_p("#{app_path}/#{path}")
end
def remove_file(path)
@@ -301,16 +437,34 @@ module TestHelpers
end
def use_frameworks(arr)
- to_remove = [:actionmailer, :activerecord] - arr
+ to_remove = [:actionmailer, :activerecord, :activestorage, :activejob, :actionmailbox] - 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 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
- def boot_rails
+ module Reload
+ def reload
+ ActiveSupport::Dependencies.clear
end
end
end
@@ -319,9 +473,9 @@ class ActiveSupport::TestCase
include TestHelpers::Paths
include TestHelpers::Rack
include TestHelpers::Generation
+ include TestHelpers::Reload
include ActiveSupport::Testing::Stream
-
- self.test_order = :sorted
+ include ActiveSupport::Testing::MethodCallAssertions
end
# Create a scope and build a fixture rails app
@@ -330,10 +484,47 @@ Module.new do
# Build a rails app
FileUtils.rm_rf(app_template_path)
- FileUtils.mkdir(app_template_path)
+ FileUtils.mkdir_p(app_template_path)
- `#{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|
+ `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-bundle --skip-listen --no-rc --skip-webpack-install`
+ File.open("#{app_template_path}/config/boot.rb", "w") do |f|
f.puts "require 'rails/all'"
end
+
+ unless File.exist?("#{RAILS_FRAMEWORK_ROOT}/actionview/lib/assets/compiled/rails-ujs.js")
+ Dir.chdir("#{RAILS_FRAMEWORK_ROOT}/actionview") { `yarn build` }
+ end
+
+ assets_path = "#{RAILS_FRAMEWORK_ROOT}/railties/test/isolation/assets"
+ unless Dir.exist?("#{assets_path}/node_modules")
+ Dir.chdir(assets_path) { `yarn install` }
+ end
+ FileUtils.cp("#{assets_path}/package.json", "#{app_template_path}/package.json")
+ FileUtils.cp("#{assets_path}/config/webpacker.yml", "#{app_template_path}/config/webpacker.yml")
+ FileUtils.cp_r("#{assets_path}/config/webpack", "#{app_template_path}/config/webpack")
+ FileUtils.ln_s("#{assets_path}/node_modules", "#{app_template_path}/node_modules")
+ FileUtils.chdir(app_template_path) { `bin/rails webpacker:binstubs` }
+
+ # Fake 'Bundler.require' -- we run using the repo's Gemfile, not an
+ # app-specific one: we don't want to require every gem that lists.
+ contents = File.read("#{app_template_path}/config/application.rb")
+ contents.sub!(/^Bundler\.require.*/, "%w(turbolinks webpacker).each { |r| require r }")
+ 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 "action_mailbox"
+ require "action_text"
+ require "sprockets"
+
+ require "action_view/helpers"
+ require "action_dispatch/routing/route_set"
end unless defined?(RAILS_ISOLATED_ENGINE)
diff --git a/railties/test/isolation/assets/config/webpack/development.js b/railties/test/isolation/assets/config/webpack/development.js
new file mode 100644
index 0000000000..395290f431
--- /dev/null
+++ b/railties/test/isolation/assets/config/webpack/development.js
@@ -0,0 +1,3 @@
+process.env.NODE_ENV = process.env.NODE_ENV || 'development'
+const { environment } = require('@rails/webpacker')
+module.exports = environment.toWebpackConfig()
diff --git a/railties/test/isolation/assets/config/webpack/production.js b/railties/test/isolation/assets/config/webpack/production.js
new file mode 100644
index 0000000000..d064a6a7fb
--- /dev/null
+++ b/railties/test/isolation/assets/config/webpack/production.js
@@ -0,0 +1,3 @@
+process.env.NODE_ENV = process.env.NODE_ENV || 'production'
+const { environment } = require('@rails/webpacker')
+module.exports = environment.toWebpackConfig()
diff --git a/railties/test/isolation/assets/config/webpack/test.js b/railties/test/isolation/assets/config/webpack/test.js
new file mode 100644
index 0000000000..395290f431
--- /dev/null
+++ b/railties/test/isolation/assets/config/webpack/test.js
@@ -0,0 +1,3 @@
+process.env.NODE_ENV = process.env.NODE_ENV || 'development'
+const { environment } = require('@rails/webpacker')
+module.exports = environment.toWebpackConfig()
diff --git a/railties/test/isolation/assets/config/webpacker.yml b/railties/test/isolation/assets/config/webpacker.yml
new file mode 100644
index 0000000000..0b1f43a407
--- /dev/null
+++ b/railties/test/isolation/assets/config/webpacker.yml
@@ -0,0 +1,8 @@
+default: &default
+ check_yarn_integrity: false
+development:
+ <<: *default
+test:
+ <<: *default
+production:
+ <<: *default
diff --git a/railties/test/isolation/assets/package.json b/railties/test/isolation/assets/package.json
new file mode 100644
index 0000000000..7c34450fe0
--- /dev/null
+++ b/railties/test/isolation/assets/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "dummy",
+ "private": true,
+ "dependencies": {
+ "@rails/actioncable": "file:../../../../actioncable",
+ "@rails/activestorage": "file:../../../../activestorage",
+ "@rails/ujs": "file:../../../../actionview",
+ "@rails/webpacker": "https://github.com/rails/webpacker.git",
+ "turbolinks": "^5.2.0"
+ }
+}
diff --git a/railties/test/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 a16adc72a6..0c1ee259b0 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
@@ -30,7 +32,7 @@ class PathGenerationTest < ActiveSupport::TestCase
end
def make_request(env)
- Request.new super, self.url_helpers, @block
+ 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,13 +58,15 @@ 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 96b54c7264..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
@@ -102,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
@@ -139,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
@@ -156,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
@@ -203,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
@@ -252,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
@@ -269,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..185a2f9f09 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
@@ -8,11 +10,12 @@ end
class InfoControllerTest < ActionController::TestCase
tests Rails::InfoController
+ Rails.application.config.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33"
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 +33,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 +48,7 @@ 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 with routes" do
@@ -54,28 +57,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"
+ end
- get :routes, params: { path: 'rails/info/routes.html' }
- assert fuzzy_count.call == 0, 'should match optional parts of route literally'
+ 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..50522c1be6 100644
--- a/railties/test/rails_info_test.rb
+++ b/railties/test/rails_info_test.rb
@@ -1,60 +1,49 @@
-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
- 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 4a47ab32b4..69f6e34d58 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
@@ -84,35 +87,35 @@ module RailtiesTest
end
RUBY
- add_to_config "ActiveRecord::Base.timestamped_migrations = false"
-
+ restrict_frameworks
boot_rails
Dir.chdir(app_path) do
output = `bundle exec rake bukkits:install:migrations`
- assert File.exist?("#{app_path}/db/migrate/2_create_users.bukkits.rb")
- assert File.exist?("#{app_path}/db/migrate/3_add_last_name_to_users.bukkits.rb")
- assert_match(/Copied migration 2_create_users.bukkits.rb from bukkits/, output)
- assert_match(/Copied migration 3_add_last_name_to_users.bukkits.rb from bukkits/, output)
- assert_match(/NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/, output)
- assert_equal 3, Dir["#{app_path}/db/migrate/*.rb"].length
-
- output = `bundle exec rake railties:install:migrations`.split("\n")
+ ["CreateUsers", "AddLastNameToUsers", "CreateSessions"].each do |migration_name|
+ assert migrations.detect { |migration| migration.name == migration_name }
+ end
+ assert_match(/Copied migration \d+_create_users\.bukkits\.rb from bukkits/, output)
+ assert_match(/Copied migration \d+_add_last_name_to_users\.bukkits\.rb from bukkits/, output)
+ assert_match(/NOTE: Migration \d+_create_sessions\.rb from bukkits has been skipped/, output)
- assert_no_match(/2_create_users/, output.join("\n"))
+ migrations_count = Dir["#{app_path}/db/migrate/*.rb"].length
- bukkits_migration_order = output.index(output.detect{|o| /NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/ =~ o })
- assert_not_nil bukkits_migration_order, "Expected migration to be skipped"
+ assert_equal migrations.length, migrations_count
- migrations_count = Dir["#{app_path}/db/migrate/*.rb"].length
- `bundle exec rake railties:install:migrations`
+ output = `bundle exec rake railties:install:migrations`.split("\n")
assert_equal migrations_count, Dir["#{app_path}/db/migrate/*.rb"].length
+
+ assert_no_match(/\d+_create_users/, output.join("\n"))
+
+ bukkits_migration_order = output.index(output.detect { |o| /NOTE: Migration \d+_create_sessions\.rb from bukkits has been skipped/ =~ o })
+ assert_not_nil bukkits_migration_order, "Expected migration to be skipped"
end
end
- 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
@@ -137,10 +140,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
@@ -170,13 +173,14 @@ module RailtiesTest
class CreateKeys < ActiveRecord::Migration::Current; end
RUBY
+ restrict_frameworks
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.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
@@ -205,19 +209,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 +329,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 +338,7 @@ module RailtiesTest
controller "foo", <<-RUBY
class FooController < ActionController::Base
def index
- render :text => "foo"
+ render plain: "foo"
end
end
RUBY
@@ -347,7 +352,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 +365,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 +382,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 +395,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 +447,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 +482,7 @@ YAML
end
RUBY
- add_to_config "config.middleware.use \"Bukkits\""
+ add_to_config "config.middleware.use Bukkits"
boot_rails
end
@@ -513,7 +514,7 @@ YAML
def call(env)
response = @app.call(env)
- response[2].each(&:upcase!)
+ response[2] = response[2].collect(&:upcase)
response
end
end
@@ -546,7 +547,7 @@ YAML
controller "foo", <<-RUBY
class FooController < ActionController::Base
def index
- render text: params[:username]
+ render plain: params[:username]
end
end
RUBY
@@ -567,7 +568,6 @@ YAML
get("/arunagw")
assert_equal "arunagw", last_response.body
-
end
test "it provides routes as default endpoint" do
@@ -637,11 +637,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
@@ -704,25 +704,27 @@ YAML
RUBY
@plugin.write "app/controllers/bukkits/foo_controller.rb", <<-RUBY
- class Bukkits::FooController < ActionController::Base
- def index
- render inline: "<%= help_the_engine %>"
- end
+ module Bukkits
+ class FooController < ActionController::Base
+ def index
+ render inline: "<%= help_the_engine %>"
+ end
- def show
- render text: foo_path
- end
+ def show
+ render plain: foo_path
+ end
- def from_app
- render inline: "<%= (self.respond_to?(:bar_path) || self.respond_to?(:something)) %>"
- end
+ def from_app
+ render inline: "<%= (self.respond_to?(:bar_path) || self.respond_to?(:something)) %>"
+ end
- def routes_helpers_in_view
- render inline: "<%= foo_path %>, <%= main_app.bar_path %>"
- end
+ def routes_helpers_in_view
+ render inline: "<%= foo_path %>, <%= main_app.bar_path %>"
+ end
- def polymorphic_path_without_namespace
- render text: polymorphic_path(Post.new)
+ def polymorphic_path_without_namespace
+ render plain: polymorphic_path(Post.new)
+ end
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
@@ -877,6 +879,18 @@ YAML
assert Bukkits::Engine.config.bukkits_seeds_loaded
end
+ test "jobs are ran inline while loading seeds" do
+ app_file "db/seeds.rb", <<-RUBY
+ Rails.application.config.seed_queue_adapter = ActiveJob::Base.queue_adapter
+ RUBY
+
+ boot_rails
+ Rails.application.load_seed
+
+ assert_instance_of ActiveJob::QueueAdapters::InlineAdapter, Rails.application.config.seed_queue_adapter
+ assert_instance_of ActiveJob::QueueAdapters::AsyncAdapter, ActiveJob::Base.queue_adapter
+ end
+
test "skips nonexistent seed data" do
FileUtils.rm "#{app_path}/db/seeds.rb"
boot_rails
@@ -892,7 +906,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 +924,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 +991,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 +1014,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 +1052,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 +1179,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)
@@ -1230,13 +1254,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 +1291,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 +1309,21 @@ YAML
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("/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
+ 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
@@ -1318,7 +1340,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
@@ -1339,23 +1361,173 @@ YAML
@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
+
+ 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
- get("/bar", {}, {'SCRIPT_NAME' => '/foo'})
- assert_equal '/foo/bukkits/bukkit', last_response.body
+ active_storage_migration = migrations.detect { |migration| migration.name == "CreateActiveStorageTables" }
+ assert active_storage_migration
+ end
end
private
def app
Rails.application
end
+
+ # Restrict frameworks to load in order to avoid engine frameworks affect tests.
+ def restrict_frameworks
+ remove_from_config("require 'rails/all'")
+ remove_from_config("require_relative 'boot'")
+ remove_from_env_config("development", "config.active_storage.*")
+ frameworks = <<~RUBY
+ require "rails"
+ require "active_model/railtie"
+ require "active_job/railtie"
+ require "active_record/railtie"
+ require "action_controller/railtie"
+ require "action_mailer/railtie"
+ require "action_view/railtie"
+ require "sprockets/railtie"
+ require "rails/test_unit/railtie"
+ RUBY
+ environment = File.read("#{app_path}/config/application.rb")
+ File.open("#{app_path}/config/application.rb", "w") { |f| f.puts frameworks + "\n" + environment }
+ end
end
end
diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb
index b85e16c040..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)
@@ -29,7 +31,7 @@ module RailtiesTests
`#{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)
@@ -39,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..9755823e51 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,36 +222,35 @@ 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
get "/someone/blog/generate_application_route"
assert_equal "/", last_response.body
- get "/somone/blog/application_route_in_view"
+ get "/someone/blog/application_route_in_view"
assert_equal "/", last_response.body
# test generating engine's route from other engine
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
@@ -251,8 +258,8 @@ module ApplicationTests
assert_equal "http://example.org/anonymous/blog/posts/44", last_response.body
# test that correct path is generated for the same polymorphic_path call in an engine
- get "/somone/blog/engine_polymorphic_path"
- assert_equal "/somone/blog/posts/44", last_response.body
+ get "/someone/blog/engine_polymorphic_path"
+ assert_equal "/someone/blog/posts/44", last_response.body
# and in an application
get "/application_polymorphic_path"
diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb
index 5042d628cf..b9725ca0ad 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
@@ -24,12 +25,10 @@ module RailtiesTest
end
test "Railtie provides railtie_name" do
- begin
- class ::FooBarBaz < Rails::Railtie ; end
- assert_equal "foo_bar_baz", FooBarBaz.railtie_name
- ensure
- Object.send(:remove_const, :"FooBarBaz")
- end
+ class ::FooBarBaz < Rails::Railtie ; end
+ assert_equal "foo_bar_baz", FooBarBaz.railtie_name
+ ensure
+ Object.send(:remove_const, :"FooBarBaz")
end
test "railtie_name can be set manually" do
@@ -64,7 +63,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 +79,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 +105,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 +130,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 +149,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 +165,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 +181,7 @@ module RailtiesTest
require "#{app_path}/config/environment"
- assert !$ran_block
+ assert_not $ran_block
Rails.application.load_runner
assert $ran_block
end
@@ -189,20 +195,18 @@ module RailtiesTest
end
end
- assert !$ran_block
+ assert_not $ran_block
require "#{app_path}/config/environment"
assert $ran_block
end
test "we can change our environment if we want to" do
- begin
- original_env = Rails.env
- Rails.env = 'foo'
- assert_equal('foo', Rails.env)
- ensure
- Rails.env = original_env
- assert_equal(original_env, Rails.env)
- end
+ original_env = Rails.env
+ Rails.env = "foo"
+ assert_equal("foo", Rails.env)
+ ensure
+ Rails.env = original_env
+ assert_equal(original_env, Rails.env)
end
end
end
diff --git a/railties/test/secrets_test.rb b/railties/test/secrets_test.rb
new file mode 100644
index 0000000000..133d851819
--- /dev/null
+++ b/railties/test/secrets_test.rb
@@ -0,0 +1,176 @@
+# 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
+ 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
+
+ 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 0d64b48550..81b7ab19a1 100644
--- a/railties/test/test_unit/reporter_test.rb
+++ b/railties/test/test_unit/reporter_test.rb
@@ -1,6 +1,8 @@
-require 'abstract_unit'
-require 'rails/test_unit/reporter'
-require 'minitest/mock'
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "rails/test_unit/reporter"
+require "minitest/mock"
class TestUnitReporterTest < ActiveSupport::TestCase
class ExampleTest < Minitest::Test
@@ -16,7 +18,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
@reporter.record(failed_test)
@reporter.report
- assert_match %r{^bin/rails test .*test/test_unit/reporter_test.rb:\d+$}, @output.string
+ assert_match %r{^rails test .*test/test_unit/reporter_test\.rb:\d+$}, @output.string
assert_rerun_snippet_count 1
end
@@ -33,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
@@ -52,7 +54,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
@reporter.record(failed_test)
@reporter.report
- assert_match %r{^bin/test .*test/test_unit/reporter_test.rb:\d+$}, @output.string
+ assert_match %r{^bin/test .*test/test_unit/reporter_test\.rb:\d+$}, @output.string
ensure
Rails::TestUnitReporter.executable = original_executable
end
@@ -62,7 +64,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
@reporter.record(failed_test)
@reporter.report
- expect = %r{\AF\n\nFailure:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nboo\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z}
+ expect = %r{\AF\n\nFailure:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nboo\n\nrails test test/test_unit/reporter_test\.rb:\d+\n\n\z}
assert_match expect, @output.string
end
@@ -70,7 +72,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
@reporter.record(errored_test)
@reporter.report
- expect = %r{\AE\n\nError:\nTestUnitReporterTest::ExampleTest#woot:\nArgumentError: wups\n No backtrace\n\nbin/rails test .*test/test_unit/reporter_test.rb:\d+\n\n\z}
+ 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
@@ -79,7 +81,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
verbose.record(skipped_test)
verbose.report
- expect = %r{\ATestUnitReporterTest::ExampleTest#woot = 10\.00 s = S\n\n\nSkipped:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nskipchurches, misstemples\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z}
+ expect = %r{\ATestUnitReporterTest::ExampleTest#woot = 10\.00 s = S\n\n\nSkipped:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nskipchurches, misstemples\n\nrails test test/test_unit/reporter_test\.rb:\d+\n\n\z}
assert_match expect, @output.string
end
@@ -87,7 +89,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
@reporter.record(failed_test)
@reporter.report
- assert_no_match 'Failed tests:', @output.string
+ assert_no_match "Failed tests:", @output.string
end
test "fail fast interrupts run on failure" do
@@ -100,7 +102,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
rescue Interrupt
interrupt_raised = true
ensure
- assert interrupt_raised, 'Expected Interrupt to be raised.'
+ assert interrupt_raised, "Expected Interrupt to be raised."
end
end
@@ -114,7 +116,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
rescue Interrupt
interrupt_raised = true
ensure
- assert interrupt_raised, 'Expected Interrupt to be raised.'
+ assert interrupt_raised, "Expected Interrupt to be raised."
end
end
@@ -122,7 +124,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true
fail_fast.record(skipped_test)
- assert_no_match 'Failed tests:', @output.string
+ assert_no_match "Failed tests:", @output.string
end
test "outputs colored passing results" do
@@ -150,44 +152,47 @@ class TestUnitReporterTest < ActiveSupport::TestCase
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 No backtrace\n\e\[0m}
+ 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{^bin/rails test }).size
- end
+ def assert_rerun_snippet_count(snippet_count)
+ assert_equal snippet_count, @output.string.scan(%r{^rails test }).size
+ end
- def failed_test
- ft = ExampleTest.new(:woot)
- ft.failures << begin
- raise Minitest::Assertion, "boo"
- rescue Minitest::Assertion => e
- e
- end
- ft
- 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
- et = ExampleTest.new(:woot)
- et.failures << Minitest::UnexpectedError.new(ArgumentError.new("wups"))
- et
- end
+ def errored_test
+ error = ArgumentError.new("wups")
+ error.set_backtrace([ "some_test.rb:4" ])
- def passing_test
- ExampleTest.new(:woot)
- end
+ et = Minitest::Result.from(ExampleTest.new(:woot))
+ et.failures << Minitest::UnexpectedError.new(error)
+ et
+ end
- def skipped_test
- st = ExampleTest.new(:woot)
- st.failures << begin
- raise Minitest::Skip, "skipchurches, misstemples"
- rescue Minitest::Assertion => e
- e
- end
- st.time = 10
- st
- 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 de9c51a140..82d1fb6a68 100644
--- a/tasks/release.rb
+++ b/tasks/release.rb
@@ -1,19 +1,47 @@
-FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack activejob actionmailer actioncable railties )
+# frozen_string_literal: true
-root = File.expand_path('../../', __FILE__)
+# Order dependent. E.g. Action Mailbox depends on Active Record so it should be after.
+FRAMEWORKS = %w(
+ activesupport
+ activemodel
+ activerecord
+ actionview
+ actionpack
+ activejob
+ actionmailer
+ actioncable
+ activestorage
+ actionmailbox
+ actiontext
+ 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"
task :clean do
rm_f gem
- sh "cd #{framework} && bundle exec rake package:clean" unless framework == "rails"
end
task :update_versions do
@@ -28,7 +56,7 @@ directory "pkg"
file = Dir[glob].first
ruby = File.read(file)
- major, minor, tiny, pre = version.split('.', 4)
+ major, minor, tiny, pre = version.split(".", 4)
pre = pre ? pre.inspect : "nil"
ruby.gsub!(/^(\s*)MAJOR(\s*)= .*?$/, "\\1MAJOR = #{major}")
@@ -43,103 +71,253 @@ 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 << "bundle exec rake package && " 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 :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'
+ (FRAMEWORKS + ["guides"]).each do |fw|
+ require "date"
+ fname = File.join fw, "CHANGELOG.md"
+ current_contents = File.read(fname)
- header = "## Rails #{version} (#{Date.today.strftime('%B %d, %Y')}) ##\n\n* No changes.\n\n\n"
- contents = header + File.read(fname)
- File.open(fname, 'wb') { |f| f.write contents }
+ 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'
+ (FRAMEWORKS + ["guides"]).each do |fw|
+ require "date"
replace = "## Rails #{version} (#{Date.today.strftime('%B %d, %Y')}) ##\n"
- fname = File.join fw, 'CHANGELOG.md'
+ fname = File.join fw, "CHANGELOG.md"
contents = File.read(fname).sub(/^(## Rails .*)\n/, replace)
- File.open(fname, 'wb') { |f| f.write contents }
+ File.write(fname, contents)
end
end
- task :release_summary do
- (FRAMEWORKS + ['guides']).each do |fw|
- puts "## #{fw}"
- fname = File.join fw, 'CHANGELOG.md'
+ task :release_summary, [:base_release, :release] do |_, args|
+ release_regexp = args[:base_release] ? Regexp.escape(args[:base_release]) : /\d+\.\d+\.\d+/
+
+ puts release
+
+ FRAMEWORKS.each do |fw|
+ puts "## #{FRAMEWORK_NAMES[fw]}"
+ fname = File.join fw, "CHANGELOG.md"
contents = File.readlines fname
contents.shift
changes = []
- changes << contents.shift until contents.first =~ /^\*Rails \d+\.\d+\.\d+/
- puts changes.reject { |change| change.strip.empty? }.join
+ until contents.first =~ /^## Rails #{release_regexp}.*$/ ||
+ contents.first =~ /^Please check.*for previous changes\.$/ ||
+ contents.empty?
+ changes << contents.shift
+ end
+
+ puts changes.join
puts
end
end
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\\|CHANGELOG\\|Gemfile.lock'`.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 verify: :install do
+ require "tmpdir"
+
+ cd Dir.tmpdir
+ app_name = "verify-#{version}-#{Time.now.to_i}"
+ sh "rails _#{version}_ new #{app_name} --skip-bundle" # Generate with the right version.
+ cd app_name
+
+ substitute = -> (file_name, regex, replacement) do
+ File.write(file_name, File.read(file_name).sub(regex, replacement))
+ end
+
+ # Replace the generated gemfile entry with the exact version.
+ substitute.call("Gemfile", /^gem 'rails.*/, "gem 'rails', '#{version}'")
+ substitute.call("Gemfile", /^# gem 'image_processing/, "gem 'image_processing")
+ sh "bundle"
+ sh "rails action_mailbox:install"
+ sh "rails action_text:install"
+
+ sh "rails generate scaffold user name description:text admin:boolean"
+ sh "rails db:migrate"
+
+ # Replace the generated gemfile entry with the exact version.
+ substitute.call("app/models/user.rb", /end\n\z/, <<~CODE)
+ has_one_attached :avatar
+ has_rich_text :description
+ end
+ CODE
+
+ substitute.call("app/views/users/_form.html.erb", /text_area :description %>\n <\/div>/, <<~CODE)
+ rich_text_area :description %>\n </div>
+
+ <div class="field">
+ Avatar: <%= form.file_field :avatar %>
+ </div>
+ CODE
+
+ substitute.call("app/views/users/show.html.erb", /description %>\n<\/p>/, <<~CODE)
+ description %>\n</p>
+
+ <p>
+ <% if @user.avatar.attached? -%>
+ <%= image_tag @user.avatar.representation(resize_to_limit: [500, 500]) %>
+ <% end -%>
+ </p>
+ CODE
+
+ # Permit the avatar param.
+ substitute.call("app/controllers/users_controller.rb", /:admin/, ":admin, :avatar")
+
+ if ENV["EDITOR"]
+ `#{ENV["EDITOR"]} #{File.expand_path(app_name)}`
+ end
+
+ 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
+
task :bundle do
- sh 'bundle check'
+ sh "bundle check"
end
task :commit do
- File.open('pkg/commit_message.txt', 'w') do |f|
- f.puts "# Preparing for #{version} release\n"
- f.puts
- f.puts "# UNCOMMENT THE LINE ABOVE TO APPROVE THIS COMMIT"
- end
+ unless `git status -s`.strip.empty?
+ File.open("pkg/commit_message.txt", "w") do |f|
+ f.puts "# Preparing for #{version} release\n"
+ f.puts
+ f.puts "# UNCOMMENT THE LINE ABOVE TO APPROVE THIS COMMIT"
+ end
- sh "git add . && git commit --verbose --template=pkg/commit_message.txt"
- rm_f "pkg/commit_message.txt"
+ sh "git add . && git commit --verbose --template=pkg/commit_message.txt"
+ rm_f "pkg/commit_message.txt"
+ end
end
task :tag do
- sh "git tag -m '#{tag} release' #{tag}"
+ sh "git tag -s -m '#{tag} release' #{tag}"
sh "git push --tags"
end
- task :prep_release => %w(ensure_clean_state build)
+ task prep_release: %w(ensure_clean_state build bundle commit)
+
+ task release: %w(prep_release tag push)
+end
+
+module Announcement
+ class Version
+ def initialize(version)
+ @version, @gem_version = version, Gem::Version.new(version)
+ end
+
+ def to_s
+ @version
+ end
- task :release => %w(ensure_clean_state build bundle commit tag push)
+ 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")
+
+ if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
+ puts ERB.new(template, trim_mode: "<>").result(binding)
+ else
+ puts ERB.new(template, nil, "<>").result(binding)
+ end
+ end
end
diff --git a/tasks/release_announcement_draft.erb b/tasks/release_announcement_draft.erb
new file mode 100644
index 0000000000..19c4b14cd4
--- /dev/null
+++ b/tasks/release_announcement_draft.erb
@@ -0,0 +1,45 @@
+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 %>
+
+To see a summary of changes, please read the release on GitHub:
+
+<%= "[#{version} CHANGELOG](https://github.com/rails/rails/releases/tag/v#{version})" %>
+
+
+*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 b2e7e4b0ae..f133b27128 100644
--- a/tools/README.md
+++ b/tools/README.md
@@ -6,3 +6,5 @@ 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 98a848ff6b..ee08e22502 100755
--- a/tools/console
+++ b/tools/console
@@ -1,9 +1,11 @@
#!/usr/bin/env ruby
-require 'bundler'
+# frozen_string_literal: true
+
+require "bundler"
Bundler.setup
-require 'rails/all'
-require 'active_support/all'
-require 'irb'
-require 'irb/completion'
+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 62e0faa3db..1fd3ee30eb 100644
--- a/tools/test.rb
+++ b/tools/test.rb
@@ -1,15 +1,26 @@
+# frozen_string_literal: true
+
$: << File.expand_path("test", COMPONENT_ROOT)
-require 'bundler'
+require "bundler"
Bundler.setup
-require "rails/test_unit/minitest_plugin"
+require "rails/test_unit/runner"
+require "rails/test_unit/reporter"
+require "rails/test_unit/line_filtering"
+require "active_support"
+require "active_support/test_case"
-module Rails
- # Necessary to get rerun-snippts working.
- def self.root
+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 081222425c..fea24810f5 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 = "beta3"
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 0000000000..fdf508b9a3
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,6078 @@
+# 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"
+ integrity sha512-F/v7t1LwS4vnXuPooJQGBRKRGIoxWUTmA4VHfqjOccFsNDThD5bfUNpITive6s352O7o384wcpEaDV8rHCehDA==
+
+"@types/node@*":
+ version "10.12.11"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.11.tgz#715c476c99a5f6898a1ae61caf9825e43c03912e"
+ integrity sha512-3iIOhNiPGTdcUNVCv9e5G7GotfvJJe2pc9w2UgDXlUwnxSZ3RgcUocIU+xYm+rTU54jIKih998QE4dMOyMN1NQ==
+
+"@webassemblyjs/ast@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.7.11.tgz#b988582cafbb2b095e8b556526f30c90d057cace"
+ integrity sha512-ZEzy4vjvTzScC+SH8RBssQUawpaInUdMTYwYYLh54/s8TuT0gBLuyUnppKsVyZEi876VmmStKsUs28UxPgdvrA==
+ dependencies:
+ "@webassemblyjs/helper-module-context" "1.7.11"
+ "@webassemblyjs/helper-wasm-bytecode" "1.7.11"
+ "@webassemblyjs/wast-parser" "1.7.11"
+
+"@webassemblyjs/floating-point-hex-parser@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.7.11.tgz#a69f0af6502eb9a3c045555b1a6129d3d3f2e313"
+ integrity sha512-zY8dSNyYcgzNRNT666/zOoAyImshm3ycKdoLsyDw/Bwo6+/uktb7p4xyApuef1dwEBo/U/SYQzbGBvV+nru2Xg==
+
+"@webassemblyjs/helper-api-error@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.7.11.tgz#c7b6bb8105f84039511a2b39ce494f193818a32a"
+ integrity sha512-7r1qXLmiglC+wPNkGuXCvkmalyEstKVwcueZRP2GNC2PAvxbLYwLLPr14rcdJaE4UtHxQKfFkuDFuv91ipqvXg==
+
+"@webassemblyjs/helper-buffer@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.7.11.tgz#3122d48dcc6c9456ed982debe16c8f37101df39b"
+ integrity sha512-MynuervdylPPh3ix+mKZloTcL06P8tenNH3sx6s0qE8SLR6DdwnfgA7Hc9NSYeob2jrW5Vql6GVlsQzKQCa13w==
+
+"@webassemblyjs/helper-code-frame@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.7.11.tgz#cf8f106e746662a0da29bdef635fcd3d1248364b"
+ integrity sha512-T8ESC9KMXFTXA5urJcyor5cn6qWeZ4/zLPyWeEXZ03hj/x9weSokGNkVCdnhSabKGYWxElSdgJ+sFa9G/RdHNw==
+ dependencies:
+ "@webassemblyjs/wast-printer" "1.7.11"
+
+"@webassemblyjs/helper-fsm@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.7.11.tgz#df38882a624080d03f7503f93e3f17ac5ac01181"
+ integrity sha512-nsAQWNP1+8Z6tkzdYlXT0kxfa2Z1tRTARd8wYnc/e3Zv3VydVVnaeePgqUzFrpkGUyhUUxOl5ML7f1NuT+gC0A==
+
+"@webassemblyjs/helper-module-context@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.7.11.tgz#d874d722e51e62ac202476935d649c802fa0e209"
+ integrity sha512-JxfD5DX8Ygq4PvXDucq0M+sbUFA7BJAv/GGl9ITovqE+idGX+J3QSzJYz+LwQmL7fC3Rs+utvWoJxDb6pmC0qg==
+
+"@webassemblyjs/helper-wasm-bytecode@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.7.11.tgz#dd9a1e817f1c2eb105b4cf1013093cb9f3c9cb06"
+ integrity sha512-cMXeVS9rhoXsI9LLL4tJxBgVD/KMOKXuFqYb5oCJ/opScWpkCMEz9EJtkonaNcnLv2R3K5jIeS4TRj/drde1JQ==
+
+"@webassemblyjs/helper-wasm-section@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.7.11.tgz#9c9ac41ecf9fbcfffc96f6d2675e2de33811e68a"
+ integrity sha512-8ZRY5iZbZdtNFE5UFunB8mmBEAbSI3guwbrsCl4fWdfRiAcvqQpeqd5KHhSWLL5wuxo53zcaGZDBU64qgn4I4Q==
+ dependencies:
+ "@webassemblyjs/ast" "1.7.11"
+ "@webassemblyjs/helper-buffer" "1.7.11"
+ "@webassemblyjs/helper-wasm-bytecode" "1.7.11"
+ "@webassemblyjs/wasm-gen" "1.7.11"
+
+"@webassemblyjs/ieee754@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.7.11.tgz#c95839eb63757a31880aaec7b6512d4191ac640b"
+ integrity sha512-Mmqx/cS68K1tSrvRLtaV/Lp3NZWzXtOHUW2IvDvl2sihAwJh4ACE0eL6A8FvMyDG9abes3saB6dMimLOs+HMoQ==
+ dependencies:
+ "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.7.11.tgz#d7267a1ee9c4594fd3f7e37298818ec65687db63"
+ integrity sha512-vuGmgZjjp3zjcerQg+JA+tGOncOnJLWVkt8Aze5eWQLwTQGNgVLcyOTqgSCxWTR4J42ijHbBxnuRaL1Rv7XMdw==
+ dependencies:
+ "@xtuc/long" "4.2.1"
+
+"@webassemblyjs/utf8@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.7.11.tgz#06d7218ea9fdc94a6793aa92208160db3d26ee82"
+ integrity sha512-C6GFkc7aErQIAH+BMrIdVSmW+6HSe20wg57HEC1uqJP8E/xpMjXqQUxkQw07MhNDSDcGpxI9G5JSNOQCqJk4sA==
+
+"@webassemblyjs/wasm-edit@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.7.11.tgz#8c74ca474d4f951d01dbae9bd70814ee22a82005"
+ integrity sha512-FUd97guNGsCZQgeTPKdgxJhBXkUbMTY6hFPf2Y4OedXd48H97J+sOY2Ltaq6WGVpIH8o/TGOVNiVz/SbpEMJGg==
+ dependencies:
+ "@webassemblyjs/ast" "1.7.11"
+ "@webassemblyjs/helper-buffer" "1.7.11"
+ "@webassemblyjs/helper-wasm-bytecode" "1.7.11"
+ "@webassemblyjs/helper-wasm-section" "1.7.11"
+ "@webassemblyjs/wasm-gen" "1.7.11"
+ "@webassemblyjs/wasm-opt" "1.7.11"
+ "@webassemblyjs/wasm-parser" "1.7.11"
+ "@webassemblyjs/wast-printer" "1.7.11"
+
+"@webassemblyjs/wasm-gen@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.7.11.tgz#9bbba942f22375686a6fb759afcd7ac9c45da1a8"
+ integrity sha512-U/KDYp7fgAZX5KPfq4NOupK/BmhDc5Kjy2GIqstMhvvdJRcER/kUsMThpWeRP8BMn4LXaKhSTggIJPOeYHwISA==
+ dependencies:
+ "@webassemblyjs/ast" "1.7.11"
+ "@webassemblyjs/helper-wasm-bytecode" "1.7.11"
+ "@webassemblyjs/ieee754" "1.7.11"
+ "@webassemblyjs/leb128" "1.7.11"
+ "@webassemblyjs/utf8" "1.7.11"
+
+"@webassemblyjs/wasm-opt@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.7.11.tgz#b331e8e7cef8f8e2f007d42c3a36a0580a7d6ca7"
+ integrity sha512-XynkOwQyiRidh0GLua7SkeHvAPXQV/RxsUeERILmAInZegApOUAIJfRuPYe2F7RcjOC9tW3Cb9juPvAC/sCqvg==
+ dependencies:
+ "@webassemblyjs/ast" "1.7.11"
+ "@webassemblyjs/helper-buffer" "1.7.11"
+ "@webassemblyjs/wasm-gen" "1.7.11"
+ "@webassemblyjs/wasm-parser" "1.7.11"
+
+"@webassemblyjs/wasm-parser@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.7.11.tgz#6e3d20fa6a3519f6b084ef9391ad58211efb0a1a"
+ integrity sha512-6lmXRTrrZjYD8Ng8xRyvyXQJYUQKYSXhJqXOBLw24rdiXsHAOlvw5PhesjdcaMadU/pyPQOJ5dHreMjBxwnQKg==
+ dependencies:
+ "@webassemblyjs/ast" "1.7.11"
+ "@webassemblyjs/helper-api-error" "1.7.11"
+ "@webassemblyjs/helper-wasm-bytecode" "1.7.11"
+ "@webassemblyjs/ieee754" "1.7.11"
+ "@webassemblyjs/leb128" "1.7.11"
+ "@webassemblyjs/utf8" "1.7.11"
+
+"@webassemblyjs/wast-parser@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.7.11.tgz#25bd117562ca8c002720ff8116ef9072d9ca869c"
+ integrity sha512-lEyVCg2np15tS+dm7+JJTNhNWq9yTZvi3qEhAIIOaofcYlUp0UR5/tVqOwa/gXYr3gjwSZqw+/lS9dscyLelbQ==
+ dependencies:
+ "@webassemblyjs/ast" "1.7.11"
+ "@webassemblyjs/floating-point-hex-parser" "1.7.11"
+ "@webassemblyjs/helper-api-error" "1.7.11"
+ "@webassemblyjs/helper-code-frame" "1.7.11"
+ "@webassemblyjs/helper-fsm" "1.7.11"
+ "@xtuc/long" "4.2.1"
+
+"@webassemblyjs/wast-printer@1.7.11":
+ version "1.7.11"
+ resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.7.11.tgz#c4245b6de242cb50a2cc950174fdbf65c78d7813"
+ integrity sha512-m5vkAsuJ32QpkdkDOUPGSltrg8Cuk3KBx4YrmAGQwCZPRdUHXxG4phIOuuycLemHFr74sWL9Wthqss4fzdzSwg==
+ dependencies:
+ "@webassemblyjs/ast" "1.7.11"
+ "@webassemblyjs/wast-parser" "1.7.11"
+ "@xtuc/long" "4.2.1"
+
+"@xtuc/ieee754@^1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+ integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.1":
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.1.tgz#5c85d662f76fa1d34575766c5dcd6615abcd30d8"
+ integrity sha512-FZdkNBDqBRHKQ2MEbSC17xnPFOhZxeJ2YGSfr2BKf3sujG49Qe3bB+rGCwQfIaA7WHnGeGkSijX4FuBCdrzW/g==
+
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+ integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
+accepts@~1.3.4:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
+ integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I=
+ dependencies:
+ mime-types "~2.1.18"
+ negotiator "0.6.1"
+
+acorn-dynamic-import@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278"
+ integrity sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==
+ dependencies:
+ acorn "^5.0.0"
+
+acorn-jsx@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
+ integrity sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=
+ 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"
+ integrity sha1-ReN/s56No/JbruP/U2niu18iAXo=
+
+acorn@^5.0.0, acorn@^5.5.0, acorn@^5.6.2:
+ version "5.7.3"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
+ integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
+
+adm-zip@~0.4.3:
+ version "0.4.13"
+ resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.13.tgz#597e2f8cc3672151e1307d3e95cddbc75672314a"
+ integrity sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw==
+
+after@0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
+ integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=
+
+agent-base@^4.1.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
+ integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
+ dependencies:
+ es6-promisify "^5.0.0"
+
+ajv-errors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59"
+ integrity sha1-7PAh+hCP0X37Xms4Py3SM+Mf/Fk=
+
+ajv-keywords@^1.0.0:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
+ integrity sha1-MU3QpLM2j609/NxU7eYXG4htrzw=
+
+ajv-keywords@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
+ integrity sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=
+
+ajv-keywords@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
+ integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=
+
+ajv@^4.7.0:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+ integrity sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=
+ dependencies:
+ co "^4.6.0"
+ json-stable-stringify "^1.0.1"
+
+ajv@^5.2.3, ajv@^5.3.0:
+ version "5.5.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
+ integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.3.0"
+
+ajv@^6.1.0, ajv@^6.5.5:
+ version "6.6.1"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.1.tgz#6360f5ed0d80f232cc2b294c362d5dc2e538dd61"
+ integrity sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==
+ dependencies:
+ fast-deep-equal "^2.0.1"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.4.1"
+ uri-js "^4.2.2"
+
+ansi-escapes@^1.1.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
+ integrity sha1-06ioOzGapneTZisT52HHkRQiMG4=
+
+ansi-escapes@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30"
+ integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+ integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+ integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+ integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=
+
+ansi-styles@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
+ integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
+ dependencies:
+ color-convert "^1.9.0"
+
+anymatch@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+ integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==
+ dependencies:
+ micromatch "^3.1.4"
+ normalize-path "^2.1.1"
+
+aproba@^1.0.3, aproba@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+ integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+
+archiver-utils@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-1.3.0.tgz#e50b4c09c70bf3d680e32ff1b7994e9f9d895174"
+ integrity sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=
+ dependencies:
+ glob "^7.0.0"
+ graceful-fs "^4.1.0"
+ lazystream "^1.0.0"
+ lodash "^4.8.0"
+ normalize-path "^2.0.0"
+ readable-stream "^2.0.0"
+
+archiver@2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/archiver/-/archiver-2.1.1.tgz#ff662b4a78201494a3ee544d3a33fe7496509ebc"
+ integrity sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw=
+ dependencies:
+ archiver-utils "^1.3.0"
+ async "^2.0.0"
+ buffer-crc32 "^0.2.1"
+ glob "^7.0.0"
+ lodash "^4.8.0"
+ readable-stream "^2.0.0"
+ tar-stream "^1.5.0"
+ zip-stream "^1.2.0"
+
+are-we-there-yet@~1.1.2:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
+ integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+argparse@^1.0.7:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
+ integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+ 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"
+ integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=
+ dependencies:
+ arr-flatten "^1.0.1"
+
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+ integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=
+
+arr-flatten@^1.0.1, arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+ integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+ integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
+
+array-slice@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5"
+ integrity sha1-3Tz7gO15c6dRF82sabC5nshhhvU=
+
+array-unique@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+ integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+ integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
+
+arraybuffer.slice@~0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
+ integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
+
+asn1.js@^4.0.0:
+ version "4.10.1"
+ resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
+ integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==
+ dependencies:
+ bn.js "^4.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+asn1@~0.2.3:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
+ integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
+ dependencies:
+ safer-buffer "~2.1.0"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+ integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
+
+assert@^1.1.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
+ integrity sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=
+ dependencies:
+ util "0.10.3"
+
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+ integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
+
+async-each@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+ integrity sha1-GdOGodntxufByF04iu28xW0zYC0=
+
+async-limiter@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
+ integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==
+
+async@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.0.1.tgz#b709cc0280a9c36f09f4536be823c838a9049e25"
+ integrity sha1-twnMAoCpw28J9FNr6CPIOKkEniU=
+ dependencies:
+ lodash "^4.8.0"
+
+async@^2.0.0, async@^2.1.2:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
+ integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==
+ dependencies:
+ lodash "^4.17.10"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
+
+atob@^2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
+ integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
+
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+ integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
+
+aws4@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
+ integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
+
+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"
+ integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=
+ 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"
+ integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==
+ 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"
+ integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==
+ 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"
+ integrity sha1-zORReto1b0IgvK6KAsKzRvmlZmQ=
+ 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"
+ integrity sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=
+ 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"
+ integrity sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=
+ 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"
+ integrity sha1-8luCz33BBDPFX3BZLVdGQArCLKo=
+ 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"
+ integrity sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=
+ 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"
+ integrity sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=
+ 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"
+ integrity sha1-HssnaJydJVE+rbyZFKc/VAi+enY=
+ 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"
+ integrity sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=
+ 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"
+ integrity sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=
+ 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"
+ integrity sha1-XsWBgnrXI/7N04HxySg5BnbkVRs=
+ 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"
+ integrity sha1-v22/5Dk40XNpohPKiov3S2qQqxo=
+ 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"
+ integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=
+ 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"
+ integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=
+ 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"
+ integrity sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=
+ 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"
+ integrity sha1-IoX0iwK9Xe3oUXXK+MYuhq3M76E=
+ 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"
+ integrity sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=
+
+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"
+ integrity sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=
+
+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"
+ integrity sha1-ugNgk3+NBuQBgKQ/4NVhb/9TLPM=
+
+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"
+ integrity sha1-ZTbjeK/2yx1VF6wOQOs+n8jQh2E=
+ 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"
+ integrity sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=
+ 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"
+ integrity sha1-u8UbSflk1wy42OC5ToICRs46YUE=
+ 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"
+ integrity sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=
+ 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"
+ integrity sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=
+ 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"
+ integrity sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=
+ 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"
+ integrity sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=
+ 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"
+ integrity sha1-c+s9MQypaePvnskcU3QabxV2Qj4=
+ 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"
+ integrity sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=
+ 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"
+ integrity sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=
+ 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"
+ integrity sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=
+ 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"
+ integrity sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=
+ 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"
+ integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==
+ 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"
+ integrity sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=
+ 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"
+ integrity sha1-rJl+YoXNGO1hdq22B9YCNErThGg=
+ 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"
+ integrity sha1-JM72muIcuDp/hgPa0CH1cusnj40=
+ 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"
+ integrity sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=
+ 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"
+ integrity sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=
+ 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"
+ integrity sha1-1taKmfia7cRTbIGlQujdnxdG+NE=
+ 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"
+ integrity sha1-AMHNsaynERLN8M9hJsLta0V8zbw=
+ 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"
+ integrity sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=
+ 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"
+ integrity sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=
+ 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"
+ integrity sha1-04sS9C6nMj9yk4fxinxa4frrNek=
+ 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"
+ integrity sha1-KrDJx/MJj6SJB3cruBP+QejeOg4=
+ 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"
+ integrity sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=
+ 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"
+ integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=
+ 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"
+ integrity sha512-9OR2afuKDneX2/q2EurSftUYM0xGu4O2D9adAhVfADDhrYDaxXV0rBbevVYoY9n6nyX1PmQW/0jtpJvUNr9CHg==
+ 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"
+ integrity sha1-btAhFz4vy0htestFxgCahW9kcHE=
+ 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"
+ integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
+ 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"
+ integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=
+ 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"
+ integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=
+ 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"
+ integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=
+ 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"
+ integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==
+
+backo2@1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
+ integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+ integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
+
+base64-arraybuffer@0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
+ integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
+
+base64-js@^1.0.2:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
+ integrity sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==
+
+base64id@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
+ integrity sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=
+
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==
+ dependencies:
+ cache-base "^1.0.1"
+ class-utils "^0.3.5"
+ component-emitter "^1.2.1"
+ define-property "^1.0.0"
+ isobject "^3.0.1"
+ mixin-deep "^1.2.0"
+ pascalcase "^0.1.1"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
+ integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
+ dependencies:
+ tweetnacl "^0.14.3"
+
+better-assert@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
+ integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=
+ dependencies:
+ callsite "1.0.0"
+
+big.js@^3.1.3:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
+ integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==
+
+binary-extensions@^1.0.0:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14"
+ integrity sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==
+
+bl@^1.0.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c"
+ integrity sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==
+ dependencies:
+ readable-stream "^2.3.5"
+ safe-buffer "^5.1.1"
+
+blob@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
+ integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
+
+bluebird@^3.3.0, bluebird@^3.5.1:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7"
+ integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
+ integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
+
+body-parser@^1.16.1:
+ version "1.18.3"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4"
+ integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=
+ dependencies:
+ bytes "3.0.0"
+ content-type "~1.0.4"
+ debug "2.6.9"
+ depd "~1.1.2"
+ http-errors "~1.6.3"
+ iconv-lite "0.4.23"
+ on-finished "~2.3.0"
+ qs "6.5.2"
+ raw-body "2.3.3"
+ type-is "~1.6.16"
+
+brace-expansion@^1.1.7:
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+ integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^0.1.2:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6"
+ integrity sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=
+ dependencies:
+ expand-range "^0.1.0"
+
+braces@^1.8.2:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+ integrity sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=
+ dependencies:
+ expand-range "^1.8.1"
+ preserve "^0.2.0"
+ repeat-element "^1.1.2"
+
+braces@^2.3.0, braces@^2.3.1:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
+ integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
+brorand@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+ integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
+ integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==
+ dependencies:
+ buffer-xor "^1.0.3"
+ cipher-base "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.3"
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+browserify-cipher@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0"
+ integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==
+ dependencies:
+ browserify-aes "^1.0.4"
+ browserify-des "^1.0.0"
+ evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c"
+ integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==
+ dependencies:
+ cipher-base "^1.0.1"
+ des.js "^1.0.0"
+ inherits "^2.0.1"
+ safe-buffer "^5.1.2"
+
+browserify-rsa@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
+ integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=
+ dependencies:
+ bn.js "^4.1.0"
+ randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
+ integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=
+ dependencies:
+ bn.js "^4.1.1"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.2"
+ elliptic "^6.0.0"
+ inherits "^2.0.1"
+ parse-asn1 "^5.0.0"
+
+browserify-zlib@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+ integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==
+ dependencies:
+ pako "~1.0.5"
+
+browserslist@^3.2.6:
+ version "3.2.8"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6"
+ integrity sha512-WHVocJYavUwVgVViC0ORikPHQquXwVh939TaelZ4WDqpWgTX/FsGhl/+P4qBUAGcRvtOgDgC+xftNWWp2RUTAQ==
+ dependencies:
+ caniuse-lite "^1.0.30000844"
+ electron-to-chromium "^1.3.47"
+
+bser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
+ integrity sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=
+ dependencies:
+ node-int64 "^0.4.0"
+
+buffer-alloc-unsafe@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
+ integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
+
+buffer-alloc@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
+ integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
+ dependencies:
+ buffer-alloc-unsafe "^1.1.0"
+ buffer-fill "^1.0.0"
+
+buffer-crc32@^0.2.1:
+ version "0.2.13"
+ resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
+ integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
+
+buffer-fill@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
+ integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
+
+buffer-from@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
+ integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
+
+buffer-xor@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+ integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
+
+buffer@^4.3.0:
+ version "4.9.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
+ integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+ isarray "^1.0.0"
+
+buffer@^5.1.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6"
+ integrity sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+
+builtin-modules@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+ integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=
+
+builtin-modules@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-2.0.0.tgz#60b7ef5ae6546bd7deefa74b08b62a43a232648e"
+ integrity sha512-3U5kUA5VPsRUA3nofm/BXX7GVHKfxz0hOBAPxXrIvHzlDRkQVqEn6yi8QJegxl4LzOHLdvb7XF5dVawa/VVYBg==
+
+builtin-status-codes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+ integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
+
+bytes@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
+ integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
+
+cacache@^11.0.2:
+ version "11.3.1"
+ resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.1.tgz#d09d25f6c4aca7a6d305d141ae332613aa1d515f"
+ integrity sha512-2PEw4cRRDu+iQvBTTuttQifacYjLPhET+SYO/gEFMy8uhi+jlJREDAjSF5FWSdV/Aw5h18caHA7vMTw2c+wDzA==
+ dependencies:
+ bluebird "^3.5.1"
+ chownr "^1.0.1"
+ figgy-pudding "^3.1.0"
+ glob "^7.1.2"
+ graceful-fs "^4.1.11"
+ lru-cache "^4.1.3"
+ mississippi "^3.0.0"
+ mkdirp "^0.5.1"
+ move-concurrently "^1.0.1"
+ promise-inflight "^1.0.1"
+ rimraf "^2.6.2"
+ ssri "^6.0.0"
+ unique-filename "^1.1.0"
+ y18n "^4.0.0"
+
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==
+ dependencies:
+ collection-visit "^1.0.0"
+ component-emitter "^1.2.1"
+ get-value "^2.0.6"
+ has-value "^1.0.0"
+ isobject "^3.0.1"
+ set-value "^2.0.0"
+ to-object-path "^0.3.0"
+ union-value "^1.0.0"
+ unset-value "^1.0.0"
+
+caller-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
+ integrity sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=
+ dependencies:
+ callsites "^0.2.0"
+
+callsite@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
+ integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
+
+callsites@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
+ integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=
+
+caniuse-lite@^1.0.30000844:
+ version "1.0.30000912"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000912.tgz#08e650d4090a9c0ab06bfd2b46b7d3ad6dcaea28"
+ integrity sha512-M3zAtV36U+xw5mMROlTXpAHClmPAor6GPKAMD5Yi7glCB5sbMPFtnQ3rGpk4XqPdUrrTIaVYSJZxREZWNy8QJg==
+
+capture-exit@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f"
+ integrity sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28=
+ dependencies:
+ rsvp "^3.3.3"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+ integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
+
+chalk@^1.0.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"
+ integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
+ 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.4.1"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
+ integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==
+ dependencies:
+ ansi-styles "^3.2.1"
+ escape-string-regexp "^1.0.5"
+ supports-color "^5.3.0"
+
+chardet@^0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
+ integrity sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=
+
+chokidar@^2.0.2, chokidar@^2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26"
+ integrity sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==
+ dependencies:
+ anymatch "^2.0.0"
+ async-each "^1.0.0"
+ braces "^2.3.0"
+ glob-parent "^3.1.0"
+ inherits "^2.0.1"
+ is-binary-path "^1.0.0"
+ is-glob "^4.0.0"
+ lodash.debounce "^4.0.8"
+ normalize-path "^2.1.1"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.0.0"
+ upath "^1.0.5"
+ optionalDependencies:
+ fsevents "^1.2.2"
+
+chownr@^1.0.1, chownr@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
+ integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==
+
+chrome-trace-event@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz#45a91bd2c20c9411f0963b5aaeb9a1b95e09cc48"
+ integrity sha512-xDbVgyfDTT2piup/h8dK/y4QZfJRSa73bw1WZ8b4XM1o7fsFubUVGYcE+1ANtOzJJELGpYoG2961z0Z6OAld9A==
+ dependencies:
+ tslib "^1.9.0"
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+ integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+circular-json@^0.3.1:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
+ integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==
+
+circular-json@^0.5.5:
+ version "0.5.9"
+ resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.9.tgz#932763ae88f4f7dead7a0d09c8a51a4743a53b1d"
+ integrity sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ==
+
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
+cli-cursor@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
+ integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=
+ dependencies:
+ restore-cursor "^1.0.1"
+
+cli-cursor@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+ integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
+ 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"
+ integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+ integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+ integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+
+coffeelint@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/coffeelint/-/coffeelint-2.1.0.tgz#af65df3634e999d9ac01480736c36d3cd2f5dad8"
+ integrity sha1-r2XfNjTpmdmsAUgHNsNtPNL12tg=
+ dependencies:
+ coffeescript "^2.1.0"
+ glob "^7.0.6"
+ ignore "^3.0.9"
+ optimist "^0.6.1"
+ resolve "^0.6.3"
+ strip-json-comments "^1.0.2"
+
+coffeescript@^2.1.0:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/coffeescript/-/coffeescript-2.3.2.tgz#e854a7020dfe47b7cf4dd412042e32ef1e269810"
+ integrity sha512-YObiFDoukx7qPBi/K0kUKyntEZDfBQiqs/DbrR1xzASKOBjGT7auD85/DiPeRr9k++lRj7l3uA9TNMLfyfcD/Q==
+
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=
+ dependencies:
+ map-visit "^1.0.0"
+ object-visit "^1.0.0"
+
+color-convert@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
+ integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
+ dependencies:
+ color-name "1.1.3"
+
+color-name@1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+ integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
+
+colors@^1.1.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.2.tgz#2df8ff573dfbf255af562f8ce7181d6b971a359b"
+ integrity sha512-rhP0JSBGYvpcNQj4s5AdShMeE5ahMop96cTeDl/v9qQQm2fYClE2QXZRi8wLzc+GmXSxdIqqbOIAhyObEXDbfQ==
+
+combine-lists@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/combine-lists/-/combine-lists-1.0.1.tgz#458c07e09e0d900fc28b70a3fec2dacd1d2cb7f6"
+ integrity sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=
+ dependencies:
+ lodash "^4.5.0"
+
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
+ integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@2.12.2:
+ version "2.12.2"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.2.tgz#0f5946c427ed9ec0d91a46bb9def53e54650e555"
+ integrity sha512-BFnaq5ZOGcDN7FlrtBT4xxkgIToalIIxwjxLWVJ8bGTpe1LroqMiqQXdA7ygc7CRvaYS+9zfPGFnJqFSayx+AA==
+
+commander@~2.13.0:
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
+ integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==
+
+commander@~2.17.1:
+ version "2.17.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
+ integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
+
+commondir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
+ integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
+
+component-bind@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
+ integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=
+
+component-emitter@1.2.1, component-emitter@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+ integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
+
+component-inherit@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
+ integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=
+
+compress-commons@^1.2.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-1.2.2.tgz#524a9f10903f3a813389b0225d27c48bb751890f"
+ integrity sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=
+ dependencies:
+ buffer-crc32 "^0.2.1"
+ crc32-stream "^2.0.0"
+ normalize-path "^2.0.0"
+ readable-stream "^2.0.0"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+ integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+concat-stream@^1.4.6, concat-stream@^1.5.0, concat-stream@^1.6.0:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
+ integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
+ dependencies:
+ buffer-from "^1.0.0"
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+connect@^3.6.0:
+ version "3.6.6"
+ resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.6.tgz#09eff6c55af7236e137135a72574858b6786f524"
+ integrity sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=
+ dependencies:
+ debug "2.6.9"
+ finalhandler "1.1.0"
+ parseurl "~1.3.2"
+ utils-merge "1.0.1"
+
+console-browserify@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
+ integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=
+ dependencies:
+ date-now "^0.1.4"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+ integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
+
+constants-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+ integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=
+
+contains-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
+ integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=
+
+content-type@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+ integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+
+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"
+ integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==
+ dependencies:
+ safe-buffer "~5.1.1"
+
+cookie@0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
+ integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
+
+copy-concurrently@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0"
+ integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==
+ dependencies:
+ aproba "^1.1.1"
+ fs-write-stream-atomic "^1.0.8"
+ iferr "^0.1.5"
+ mkdirp "^0.5.1"
+ rimraf "^2.5.4"
+ run-queue "^1.0.0"
+
+copy-descriptor@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+ integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
+
+core-js@^2.2.0, 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"
+ integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==
+
+core-util-is@1.0.2, 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"
+ integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+
+crc32-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-2.0.0.tgz#e3cdd3b4df3168dd74e3de3fbbcb7b297fe908f4"
+ integrity sha1-483TtN8xaN10494/u8t7KX/pCPQ=
+ dependencies:
+ crc "^3.4.4"
+ readable-stream "^2.0.0"
+
+crc@^3.4.4:
+ version "3.8.0"
+ resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6"
+ integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==
+ dependencies:
+ buffer "^5.1.0"
+
+create-ecdh@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff"
+ integrity sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==
+ dependencies:
+ bn.js "^4.1.0"
+ elliptic "^6.0.0"
+
+create-hash@^1.1.0, create-hash@^1.1.2:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
+ integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
+ dependencies:
+ cipher-base "^1.0.1"
+ inherits "^2.0.1"
+ md5.js "^1.3.4"
+ ripemd160 "^2.0.1"
+ sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
+ integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
+ dependencies:
+ cipher-base "^1.0.3"
+ create-hash "^1.1.0"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+cross-spawn@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+cross-spawn@^6.0.0:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+crypto-browserify@^3.11.0:
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
+ integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==
+ dependencies:
+ browserify-cipher "^1.0.0"
+ browserify-sign "^4.0.0"
+ create-ecdh "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.0"
+ diffie-hellman "^5.0.0"
+ inherits "^2.0.1"
+ pbkdf2 "^3.0.3"
+ public-encrypt "^4.0.0"
+ randombytes "^2.0.0"
+ randomfill "^1.0.3"
+
+custom-event@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
+ integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=
+
+cyclist@~0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
+ integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=
+
+d@1:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
+ integrity sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=
+ dependencies:
+ es5-ext "^0.10.9"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
+ dependencies:
+ assert-plus "^1.0.0"
+
+date-format@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8"
+ integrity sha1-YV6CjiM90aubua4JUODOzPpuytg=
+
+date-now@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+ integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=
+
+debug@2.6.9, debug@^2.1.1, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+debug@=3.1.0, debug@~3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+ integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
+ dependencies:
+ ms "2.0.0"
+
+debug@^3.1.0:
+ version "3.2.6"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
+ integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
+ dependencies:
+ ms "^2.1.1"
+
+decode-uri-component@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+ integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+
+deep-extend@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
+ integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+ integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
+
+define-property@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+ integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=
+ dependencies:
+ is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+ integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY=
+ dependencies:
+ is-descriptor "^1.0.0"
+
+define-property@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d"
+ integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==
+ dependencies:
+ is-descriptor "^1.0.2"
+ isobject "^3.0.1"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+ integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
+
+depd@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+ integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+
+des.js@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
+ integrity sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=
+ dependencies:
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+detect-file@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
+ integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
+
+detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg=
+ dependencies:
+ repeating "^2.0.0"
+
+detect-libc@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+ integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
+
+di@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
+ integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=
+
+diffie-hellman@^5.0.0:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
+ integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==
+ dependencies:
+ bn.js "^4.1.0"
+ miller-rabin "^4.0.0"
+ randombytes "^2.0.0"
+
+doctrine@1.5.0, doctrine@^1.2.2:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
+ integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=
+ 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"
+ integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
+ dependencies:
+ esutils "^2.0.2"
+
+dom-serialize@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
+ integrity sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=
+ dependencies:
+ custom-event "~1.0.0"
+ ent "~2.2.0"
+ extend "^3.0.0"
+ void-elements "^2.0.0"
+
+domain-browser@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
+ integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
+
+duplexify@^3.4.2, duplexify@^3.6.0:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.1.tgz#b1a7a29c4abfd639585efaecce80d666b1e34125"
+ integrity sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==
+ dependencies:
+ end-of-stream "^1.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+ stream-shift "^1.0.0"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
+ integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
+ dependencies:
+ jsbn "~0.1.0"
+ safer-buffer "^2.1.0"
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+ integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+
+electron-to-chromium@^1.3.47:
+ version "1.3.86"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.86.tgz#a45ea01da5b26500d12bca5e0f194ebb3e1fd14e"
+ integrity sha512-BcmXOu37FCPxrrh0wyKgKi5dAjIu2ohxN5ptapkLPKRC3IBK2NeIwh9n1x/8HzSRQiEKamJkDce1ZgOGgEX9iw==
+
+elliptic@^6.0.0:
+ version "6.4.1"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a"
+ integrity sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==
+ dependencies:
+ bn.js "^4.4.0"
+ brorand "^1.0.1"
+ hash.js "^1.0.0"
+ hmac-drbg "^1.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.0"
+
+emojis-list@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
+ integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
+
+encodeurl@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
+ integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
+ integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==
+ dependencies:
+ once "^1.4.0"
+
+engine.io-client@~3.2.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.2.1.tgz#6f54c0475de487158a1a7c77d10178708b6add36"
+ integrity sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==
+ dependencies:
+ component-emitter "1.2.1"
+ component-inherit "0.0.3"
+ debug "~3.1.0"
+ engine.io-parser "~2.1.1"
+ has-cors "1.1.0"
+ indexof "0.0.1"
+ parseqs "0.0.5"
+ parseuri "0.0.5"
+ ws "~3.3.1"
+ xmlhttprequest-ssl "~1.5.4"
+ yeast "0.1.2"
+
+engine.io-parser@~2.1.0, engine.io-parser@~2.1.1:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.1.3.tgz#757ab970fbf2dfb32c7b74b033216d5739ef79a6"
+ integrity sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==
+ dependencies:
+ after "0.8.2"
+ arraybuffer.slice "~0.0.7"
+ base64-arraybuffer "0.1.5"
+ blob "0.0.5"
+ has-binary2 "~1.0.2"
+
+engine.io@~3.2.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.2.1.tgz#b60281c35484a70ee0351ea0ebff83ec8c9522a2"
+ integrity sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==
+ dependencies:
+ accepts "~1.3.4"
+ base64id "1.0.0"
+ cookie "0.3.1"
+ debug "~3.1.0"
+ engine.io-parser "~2.1.0"
+ ws "~3.3.1"
+
+enhanced-resolve@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f"
+ integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==
+ dependencies:
+ graceful-fs "^4.1.2"
+ memory-fs "^0.4.0"
+ tapable "^1.0.0"
+
+ensure-posix-path@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/ensure-posix-path/-/ensure-posix-path-1.0.2.tgz#a65b3e42d0b71cfc585eb774f9943c8d9b91b0c2"
+ integrity sha1-pls+QtC3HPxYXrd0+ZQ8jZuRsMI=
+
+ent@~2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
+ integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
+
+errno@^0.1.3, errno@~0.1.7:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
+ integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==
+ dependencies:
+ prr "~1.0.1"
+
+error-ex@^1.2.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
+ integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14:
+ version "0.10.46"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.46.tgz#efd99f67c5a7ec789baa3daa7f79870388f7f572"
+ integrity sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==
+ dependencies:
+ es6-iterator "~2.0.3"
+ es6-symbol "~3.1.1"
+ next-tick "1"
+
+es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+ integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c=
+ dependencies:
+ d "1"
+ es5-ext "^0.10.35"
+ es6-symbol "^3.1.1"
+
+es6-map@^0.1.3:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
+ integrity sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-set "~0.1.5"
+ es6-symbol "~3.1.1"
+ event-emitter "~0.3.5"
+
+es6-promise@^4.0.3:
+ version "4.2.5"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054"
+ integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg==
+
+es6-promisify@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
+ integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
+ dependencies:
+ es6-promise "^4.0.3"
+
+es6-set@~0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
+ integrity sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-symbol "3.1.1"
+ event-emitter "~0.3.5"
+
+es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
+ integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+es6-weak-map@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f"
+ integrity sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=
+ dependencies:
+ d "1"
+ es5-ext "^0.10.14"
+ es6-iterator "^2.0.1"
+ es6-symbol "^3.1.1"
+
+escape-html@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
+ integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+
+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"
+ integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
+
+escope@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3"
+ integrity sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=
+ dependencies:
+ es6-map "^0.1.3"
+ es6-weak-map "^2.0.1"
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+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"
+ integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==
+ dependencies:
+ debug "^2.6.9"
+ resolve "^1.5.0"
+
+eslint-module-utils@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz#b270362cd88b1a48ad308976ce7fa54e98411746"
+ integrity sha1-snA2LNiLGkitMIl2zn+lTphBF0Y=
+ dependencies:
+ debug "^2.6.8"
+ pkg-dir "^1.0.0"
+
+eslint-plugin-import@^2.7.0:
+ version "2.14.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz#6b17626d2e3e6ad52cfce8807a845d15e22111a8"
+ integrity sha512-FpuRtniD/AY6sXByma2Wr0TXvXJ4nA/2/04VPlfpmUDPOpOY264x+ILiwnrk/k4RINgDAyFZByxqPUbSQ5YE7g==
+ dependencies:
+ contains-path "^0.1.0"
+ debug "^2.6.8"
+ doctrine "1.5.0"
+ eslint-import-resolver-node "^0.3.1"
+ eslint-module-utils "^2.2.0"
+ has "^1.0.1"
+ lodash "^4.17.4"
+ minimatch "^3.0.3"
+ read-pkg-up "^2.0.0"
+ resolve "^1.6.0"
+
+eslint-scope@^3.7.1:
+ version "3.7.3"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.3.tgz#bb507200d3d17f60247636160b4826284b108535"
+ integrity sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint-scope@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172"
+ integrity sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA==
+ 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"
+ integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==
+
+eslint@^2.13.1:
+ version "2.13.1"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-2.13.1.tgz#e4cc8fa0f009fb829aaae23855a29360be1f6c11"
+ integrity sha1-5MyPoPAJ+4KaquI4VaKTYL4fbBE=
+ dependencies:
+ chalk "^1.1.3"
+ concat-stream "^1.4.6"
+ debug "^2.1.1"
+ doctrine "^1.2.2"
+ es6-map "^0.1.3"
+ escope "^3.6.0"
+ espree "^3.1.6"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ file-entry-cache "^1.1.1"
+ glob "^7.0.3"
+ globals "^9.2.0"
+ ignore "^3.1.2"
+ imurmurhash "^0.1.4"
+ inquirer "^0.12.0"
+ is-my-json-valid "^2.10.0"
+ is-resolvable "^1.0.0"
+ js-yaml "^3.5.1"
+ json-stable-stringify "^1.0.0"
+ levn "^0.3.0"
+ lodash "^4.0.0"
+ mkdirp "^0.5.0"
+ optionator "^0.8.1"
+ path-is-absolute "^1.0.0"
+ path-is-inside "^1.0.1"
+ pluralize "^1.2.1"
+ progress "^1.1.8"
+ require-uncached "^1.0.2"
+ shelljs "^0.6.0"
+ strip-json-comments "~1.0.1"
+ table "^3.7.8"
+ text-table "~0.2.0"
+ user-home "^2.0.0"
+
+eslint@^4.3.0:
+ version "4.19.1"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300"
+ integrity sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==
+ 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.4"
+ 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"
+ regexpp "^1.0.1"
+ require-uncached "^1.0.3"
+ semver "^5.3.0"
+ strip-ansi "^4.0.0"
+ strip-json-comments "~2.0.1"
+ table "4.0.2"
+ text-table "~0.2.0"
+
+espree@^3.1.6, espree@^3.5.4:
+ version "3.5.4"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7"
+ integrity sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==
+ dependencies:
+ acorn "^5.5.0"
+ acorn-jsx "^3.0.0"
+
+esprima@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
+ integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+esquery@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708"
+ integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==
+ dependencies:
+ estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf"
+ integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==
+ dependencies:
+ estraverse "^4.1.0"
+
+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"
+ integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=
+
+estree-walker@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.2.1.tgz#bdafe8095383d8414d5dc2ecf4c9173b6db9412e"
+ integrity sha1-va/oCVOD2EFNXcLs9MkXO225QS4=
+
+estree-walker@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.2.tgz#d3850be7529c9580d815600b53126515e146dd39"
+ integrity sha512-XpCnW/AE10ws/kDAs37cngSkvgIR8aN3G0MS85m7dUpuK2EREo9VJ00uvw6Dg/hXEpfsE1I1TvJOJr+Z+TL+ig==
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+ integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=
+
+event-emitter@~0.3.5:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+ integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+eventemitter3@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
+ integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==
+
+events@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+ integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
+
+evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+ integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==
+ dependencies:
+ md5.js "^1.3.4"
+ safe-buffer "^5.1.1"
+
+exec-sh@^0.2.0:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36"
+ integrity sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==
+ dependencies:
+ merge "^1.2.0"
+
+exec-sh@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.2.tgz#6738de2eb7c8e671d0366aea0b0db8c6f7d7391b"
+ integrity sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg==
+
+execa@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
+ integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
+ dependencies:
+ cross-spawn "^6.0.0"
+ get-stream "^4.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+exists-stat@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/exists-stat/-/exists-stat-1.0.0.tgz#0660e3525a2e89d9e446129440c272edfa24b529"
+ integrity sha1-BmDjUlouidnkRhKUQMJy7foktSk=
+
+exit-hook@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
+ integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=
+
+expand-braces@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea"
+ integrity sha1-SIsdHSRRyz06axks/AMPRMWFX+o=
+ dependencies:
+ array-slice "^0.2.3"
+ array-unique "^0.2.1"
+ braces "^0.1.2"
+
+expand-brackets@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+ integrity sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=
+ dependencies:
+ is-posix-bracket "^0.1.0"
+
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI=
+ dependencies:
+ debug "^2.3.3"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ posix-character-classes "^0.1.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+expand-range@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-0.1.1.tgz#4cb8eda0993ca56fa4f41fc42f3cbb4ccadff044"
+ integrity sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=
+ dependencies:
+ is-number "^0.1.1"
+ repeat-string "^0.2.2"
+
+expand-range@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+ integrity sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=
+ dependencies:
+ fill-range "^2.1.0"
+
+expand-tilde@^2.0.0, expand-tilde@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
+ integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=
+ dependencies:
+ homedir-polyfill "^1.0.1"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0, extend-shallow@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+ integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=
+ dependencies:
+ assign-symbols "^1.0.0"
+ is-extendable "^1.0.1"
+
+extend@^3.0.0, extend@~3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+ integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+external-editor@^2.0.4:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
+ integrity sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==
+ 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"
+ integrity sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=
+ dependencies:
+ is-extglob "^1.0.0"
+
+extglob@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==
+ dependencies:
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ expand-brackets "^2.1.4"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+extsprintf@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+ integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
+
+extsprintf@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+ integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
+
+fast-deep-equal@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
+ integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=
+
+fast-deep-equal@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
+ integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
+
+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"
+ integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I=
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+ integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+
+fb-watchman@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
+ integrity sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=
+ dependencies:
+ bser "^2.0.0"
+
+figgy-pudding@^3.1.0, figgy-pudding@^3.5.1:
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
+ integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==
+
+figures@^1.3.5:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
+ integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=
+ dependencies:
+ escape-string-regexp "^1.0.5"
+ object-assign "^4.1.0"
+
+figures@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
+ integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
+file-entry-cache@^1.1.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-1.3.1.tgz#44c61ea607ae4be9c1402f41f44270cbfe334ff8"
+ integrity sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g=
+ dependencies:
+ flat-cache "^1.2.1"
+ object-assign "^4.0.1"
+
+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"
+ integrity sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=
+ 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"
+ integrity sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=
+
+fill-range@^2.1.0:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565"
+ integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==
+ dependencies:
+ is-number "^2.1.0"
+ isobject "^2.0.0"
+ randomatic "^3.0.0"
+ repeat-element "^1.1.2"
+ repeat-string "^1.5.2"
+
+fill-range@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+ integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
+finalhandler@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5"
+ integrity sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=
+ dependencies:
+ debug "2.6.9"
+ encodeurl "~1.0.1"
+ escape-html "~1.0.3"
+ on-finished "~2.3.0"
+ parseurl "~1.3.2"
+ statuses "~1.3.1"
+ unpipe "~1.0.0"
+
+find-cache-dir@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d"
+ integrity sha512-LDUY6V1Xs5eFskUVYtIwatojt6+9xC9Chnlk/jYOOvn3FAFfSaWddxahDGyNHh0b2dMXa6YW2m0tk8TdVaXHlA==
+ dependencies:
+ commondir "^1.0.1"
+ make-dir "^1.0.0"
+ pkg-dir "^3.0.0"
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=
+ 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"
+ integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
+ dependencies:
+ locate-path "^2.0.0"
+
+find-up@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
+ integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
+ dependencies:
+ locate-path "^3.0.0"
+
+findup-sync@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc"
+ integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=
+ dependencies:
+ detect-file "^1.0.0"
+ is-glob "^3.1.0"
+ micromatch "^3.0.4"
+ resolve-dir "^1.0.1"
+
+flat-cache@^1.2.1:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.4.tgz#2c2ef77525cc2929007dfffa1dd314aa9c9dee6f"
+ integrity sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==
+ dependencies:
+ circular-json "^0.3.1"
+ graceful-fs "^4.1.2"
+ rimraf "~2.6.2"
+ write "^0.2.1"
+
+flatted@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916"
+ integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==
+
+flush-write-stream@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd"
+ integrity sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.0.4"
+
+follow-redirects@^1.0.0:
+ version "1.5.10"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
+ integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
+ dependencies:
+ debug "=3.1.0"
+
+for-in@^1.0.1, for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+ integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=
+
+for-own@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+ integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=
+ dependencies:
+ for-in "^1.0.1"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+ integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
+
+form-data@~2.3.2:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+ integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.6"
+ mime-types "^2.1.12"
+
+fragment-cache@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+ integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=
+ dependencies:
+ map-cache "^0.2.2"
+
+from2@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af"
+ integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+
+fs-access@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a"
+ integrity sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=
+ dependencies:
+ null-check "^1.0.0"
+
+fs-constants@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
+ integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
+
+fs-minipass@^1.2.5:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
+ integrity sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==
+ dependencies:
+ minipass "^2.2.1"
+
+fs-write-stream-atomic@^1.0.8:
+ version "1.0.10"
+ resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9"
+ integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=
+ dependencies:
+ graceful-fs "^4.1.2"
+ iferr "^0.1.5"
+ imurmurhash "^0.1.4"
+ readable-stream "1 || 2"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+ integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@^1.2.2:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
+ integrity sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==
+ dependencies:
+ nan "^2.9.2"
+ node-pre-gyp "^0.10.0"
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+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"
+ integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+generate-function@^2.0.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.3.1.tgz#f069617690c10c868e73b8465746764f97c3479f"
+ integrity sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==
+ dependencies:
+ is-property "^1.0.2"
+
+generate-object-property@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0"
+ integrity sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=
+ dependencies:
+ is-property "^1.0.0"
+
+get-stream@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
+ integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
+ dependencies:
+ pump "^3.0.0"
+
+get-value@^2.0.3, get-value@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+ integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob-base@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+ integrity sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=
+ 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"
+ integrity sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=
+ dependencies:
+ is-glob "^2.0.0"
+
+glob-parent@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+ integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=
+ dependencies:
+ is-glob "^3.1.0"
+ path-dirname "^1.0.0"
+
+glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2:
+ version "7.1.3"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
+ integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
+ 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"
+
+global-modules@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
+ integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==
+ dependencies:
+ global-prefix "^1.0.1"
+ is-windows "^1.0.1"
+ resolve-dir "^1.0.0"
+
+global-prefix@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe"
+ integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=
+ dependencies:
+ expand-tilde "^2.0.2"
+ homedir-polyfill "^1.0.1"
+ ini "^1.3.4"
+ is-windows "^1.0.1"
+ which "^1.2.14"
+
+globals@^11.0.1:
+ version "11.9.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.9.0.tgz#bde236808e987f290768a93d065060d78e6ab249"
+ integrity sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==
+
+globals@^9.18.0, globals@^9.2.0:
+ version "9.18.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+ integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==
+
+graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2:
+ version "4.1.15"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
+ integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+ integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
+
+har-validator@~5.1.0:
+ version "5.1.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+ integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+ dependencies:
+ ajv "^6.5.5"
+ har-schema "^2.0.0"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-binary2@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d"
+ integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==
+ dependencies:
+ isarray "2.0.1"
+
+has-cors@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
+ integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=
+
+has-flag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+ integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+ integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
+
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=
+ dependencies:
+ get-value "^2.0.3"
+ has-values "^0.1.4"
+ isobject "^2.0.0"
+
+has-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+ integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=
+ dependencies:
+ get-value "^2.0.6"
+ has-values "^1.0.0"
+ isobject "^3.0.0"
+
+has-values@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+ integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E=
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+has@^1.0.1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+hash-base@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918"
+ integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
+ integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.1"
+
+hmac-drbg@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+ integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
+ dependencies:
+ hash.js "^1.0.3"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.1"
+
+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"
+ integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg=
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.1"
+
+homedir-polyfill@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"
+ integrity sha1-TCu8inWJmP7r9e1oWA921GdotLw=
+ dependencies:
+ parse-passwd "^1.0.0"
+
+hosted-git-info@^2.1.4:
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
+ integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==
+
+http-errors@1.6.3, http-errors@~1.6.3:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
+ integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.3"
+ setprototypeof "1.1.0"
+ statuses ">= 1.4.0 < 2"
+
+http-proxy@^1.13.0:
+ version "1.17.0"
+ resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a"
+ integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g==
+ dependencies:
+ eventemitter3 "^3.0.0"
+ follow-redirects "^1.0.0"
+ requires-port "^1.0.0"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+https-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
+ integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
+
+https-proxy-agent@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
+ integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==
+ dependencies:
+ agent-base "^4.1.0"
+ debug "^3.1.0"
+
+iconv-lite@0.4.23:
+ version "0.4.23"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
+ integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+iconv-lite@^0.4.17, iconv-lite@^0.4.4:
+ version "0.4.24"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
+ integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3"
+
+ieee754@^1.1.4:
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b"
+ integrity sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==
+
+iferr@^0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
+ integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
+
+ignore-walk@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
+ integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==
+ dependencies:
+ minimatch "^3.0.4"
+
+ignore@^3.0.9, ignore@^3.1.2, ignore@^3.3.3:
+ version "3.3.10"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
+ integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+ integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
+
+indexof@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
+ integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+ integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+
+inherits@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+ integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
+
+ini@^1.3.4, ini@~1.3.0:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
+ integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
+
+inquirer@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e"
+ integrity sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=
+ dependencies:
+ ansi-escapes "^1.1.0"
+ ansi-regex "^2.0.0"
+ chalk "^1.0.0"
+ cli-cursor "^1.0.1"
+ cli-width "^2.0.0"
+ figures "^1.3.5"
+ lodash "^4.3.0"
+ readline2 "^1.0.1"
+ run-async "^0.1.0"
+ rx-lite "^3.1.2"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.0"
+ through "^2.3.6"
+
+inquirer@^3.0.6:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
+ integrity sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==
+ 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"
+ integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
+ dependencies:
+ loose-envify "^1.0.0"
+
+is-accessor-descriptor@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+ integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+ integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==
+ dependencies:
+ kind-of "^6.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"
+ integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
+
+is-binary-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+ integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=
+ dependencies:
+ binary-extensions "^1.0.0"
+
+is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+ integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
+
+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"
+ integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74=
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-data-descriptor@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+ integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=
+ dependencies:
+ kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+ integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==
+ dependencies:
+ kind-of "^6.0.0"
+
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==
+ dependencies:
+ is-accessor-descriptor "^0.1.6"
+ is-data-descriptor "^0.1.4"
+ kind-of "^5.0.0"
+
+is-descriptor@^1.0.0, is-descriptor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+ integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==
+ dependencies:
+ is-accessor-descriptor "^1.0.0"
+ is-data-descriptor "^1.0.0"
+ kind-of "^6.0.2"
+
+is-dotfile@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+ integrity sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=
+
+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"
+ integrity sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=
+ dependencies:
+ is-primitive "^2.0.0"
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+ integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
+ dependencies:
+ is-plain-object "^2.0.4"
+
+is-extglob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+ integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+ integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
+ 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"
+ integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
+
+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"
+ integrity sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=
+ dependencies:
+ is-extglob "^1.0.0"
+
+is-glob@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+ integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=
+ dependencies:
+ is-extglob "^2.1.0"
+
+is-glob@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0"
+ integrity sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
+ integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=
+
+is-my-ip-valid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824"
+ integrity sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==
+
+is-my-json-valid@^2.10.0:
+ version "2.19.0"
+ resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz#8fd6e40363cd06b963fa877d444bfb5eddc62175"
+ integrity sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q==
+ dependencies:
+ generate-function "^2.0.0"
+ generate-object-property "^1.1.0"
+ is-my-ip-valid "^1.0.0"
+ jsonpointer "^4.0.0"
+ xtend "^4.0.0"
+
+is-number@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806"
+ integrity sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=
+
+is-number@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=
+ 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"
+ integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=
+ 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"
+ integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==
+
+is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+ dependencies:
+ isobject "^3.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"
+ integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=
+
+is-primitive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+ integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU=
+
+is-promise@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
+ integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
+
+is-property@^1.0.0, is-property@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
+ integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=
+
+is-resolvable@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
+ integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
+
+is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+ integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+ integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+
+is-windows@^1.0.1, is-windows@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
+ integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
+
+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"
+ integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
+
+isarray@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
+ integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=
+
+isbinaryfile@^3.0.0:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.3.tgz#5d6def3edebf6e8ca8cae9c30183a804b5f8be80"
+ integrity sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==
+ dependencies:
+ buffer-alloc "^1.2.0"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+ integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=
+ dependencies:
+ isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+ integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+ integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
+
+js-reporters@1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/js-reporters/-/js-reporters-1.2.1.tgz#f88c608e324a3373a95bcc45ad305e5c979c459b"
+ integrity sha1-+IxgjjJKM3OpW8xFrTBeXJecRZs=
+
+"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"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-tokens@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+ integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
+
+js-yaml@^3.5.1, js-yaml@^3.9.1:
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1"
+ integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+ integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
+
+jsesc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+ integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s=
+
+jsesc@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+ integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
+
+json-parse-better-errors@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+ integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+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"
+ integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+ integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=
+
+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"
+ integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
+
+json-stable-stringify@^1.0.0, 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"
+ integrity sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=
+ dependencies:
+ jsonify "~0.0.0"
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+ integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
+
+json5@^0.5.0, json5@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+ integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+ integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
+
+jsonpointer@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
+ integrity sha1-T9kss04OnbPInIYi7PUfm5eMbLk=
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+karma-chrome-launcher@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz#cf1b9d07136cc18fe239327d24654c3dbc368acf"
+ integrity sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w==
+ dependencies:
+ fs-access "^1.0.0"
+ which "^1.2.1"
+
+karma-qunit@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/karma-qunit/-/karma-qunit-2.1.0.tgz#135ee8f6ceb68d6a08b2c151e9302ef615090e65"
+ integrity sha512-QFt2msjpFNx1ZqB1EcD7rXaFRa3P+kLrgm6uRDYV/1MO7qGMxnTDgsFB1KyAKCpMreOmB5MMpEm5sX52j4c0aw==
+
+karma-sauce-launcher@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/karma-sauce-launcher/-/karma-sauce-launcher-1.2.0.tgz#6f2558ddef3cf56879fa27540c8ae9f8bfd16bca"
+ integrity sha512-lEhtGRGS+3Yw6JSx/vJY9iQyHNtTjcojrSwNzqNUOaDceKDu9dPZqA/kr69bUO9G2T6GKbu8AZgXqy94qo31Jg==
+ dependencies:
+ q "^1.5.0"
+ sauce-connect-launcher "^1.2.2"
+ saucelabs "^1.4.0"
+ wd "^1.4.0"
+
+karma@^3.1.1:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/karma/-/karma-3.1.3.tgz#6e251648e3aff900927bc1126dbcbcb92d3edd61"
+ integrity sha512-JU4FYUtFEGsLZd6ZJzLrivcPj0TkteBiIRDcXWFsltPMGgZMDtby/MIzNOzgyZv/9dahs9vHpSxerC/ZfeX9Qw==
+ dependencies:
+ bluebird "^3.3.0"
+ body-parser "^1.16.1"
+ chokidar "^2.0.3"
+ colors "^1.1.0"
+ combine-lists "^1.0.0"
+ connect "^3.6.0"
+ core-js "^2.2.0"
+ di "^0.0.1"
+ dom-serialize "^2.2.0"
+ expand-braces "^0.1.1"
+ flatted "^2.0.0"
+ glob "^7.1.1"
+ graceful-fs "^4.1.2"
+ http-proxy "^1.13.0"
+ isbinaryfile "^3.0.0"
+ lodash "^4.17.5"
+ log4js "^3.0.0"
+ mime "^2.3.1"
+ minimatch "^3.0.2"
+ optimist "^0.6.1"
+ qjobs "^1.1.4"
+ range-parser "^1.2.0"
+ rimraf "^2.6.0"
+ safe-buffer "^5.0.1"
+ socket.io "2.1.1"
+ source-map "^0.6.1"
+ tmp "0.0.33"
+ useragent "2.3.0"
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=
+ 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"
+ integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc=
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+ integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+ integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
+
+lazystream@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4"
+ integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=
+ dependencies:
+ readable-stream "^2.0.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"
+ integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
+ 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"
+ integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ strip-bom "^3.0.0"
+
+loader-runner@^2.3.0:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.1.tgz#026f12fe7c3115992896ac02ba022ba92971b979"
+ integrity sha512-By6ZFY7ETWOc9RFaAIb23IjJVcM4dvJC/N57nmdz9RSkMXvAXGI7SyVlAw3v8vjtDRlqThgVDVmTnr9fqMlxkw==
+
+loader-utils@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd"
+ integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=
+ dependencies:
+ big.js "^3.1.3"
+ emojis-list "^2.0.0"
+ json5 "^0.5.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+locate-path@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
+ integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
+ dependencies:
+ p-locate "^3.0.0"
+ path-exists "^3.0.0"
+
+lodash.debounce@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
+ integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
+
+lodash@4.17.11, lodash@^4.0.0, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.8.0:
+ version "4.17.11"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
+ integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
+
+log4js@^3.0.0:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/log4js/-/log4js-3.0.6.tgz#e6caced94967eeeb9ce399f9f8682a4b2b28c8ff"
+ integrity sha512-ezXZk6oPJCWL483zj64pNkMuY/NcRX5MPiB0zE6tjZM137aeusrOnW1ecxgF9cmwMWkBMhjteQxBPoZBh9FDxQ==
+ dependencies:
+ circular-json "^0.5.5"
+ date-format "^1.2.0"
+ debug "^3.1.0"
+ rfdc "^1.1.2"
+ streamroller "0.7.0"
+
+loose-envify@^1.0.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+ integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.1.3:
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
+ integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+magic-string@^0.25.1:
+ version "0.25.1"
+ resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.1.tgz#b1c248b399cd7485da0fe7385c2fc7011843266e"
+ integrity sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg==
+ dependencies:
+ sourcemap-codec "^1.4.1"
+
+make-dir@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
+ integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==
+ dependencies:
+ pify "^3.0.0"
+
+makeerror@1.0.x:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+ integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=
+ dependencies:
+ tmpl "1.0.x"
+
+map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+ integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
+
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=
+ dependencies:
+ object-visit "^1.0.0"
+
+matcher-collection@^1.0.0:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/matcher-collection/-/matcher-collection-1.0.5.tgz#2ee095438372cb8884f058234138c05c644ec339"
+ integrity sha512-nUCmzKipcJEwYsBVAFh5P+d7JBuhJaW1xs85Hara9xuMLqtCVUrW6DSC0JVIkluxEH2W45nPBM/wjHtBXa/tYA==
+ dependencies:
+ minimatch "^3.0.2"
+
+math-random@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac"
+ integrity sha1-izqsWIuKZuSXXjzepn97sylgH6w=
+
+md5.js@^1.3.4:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
+ integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+ safe-buffer "^5.1.2"
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+ integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+
+memory-fs@^0.4.0, memory-fs@~0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
+ integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=
+ dependencies:
+ errno "^0.1.3"
+ readable-stream "^2.0.1"
+
+merge@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
+ integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
+
+micromatch@^2.3.11:
+ version "2.3.11"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+ integrity sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=
+ 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"
+
+micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8:
+ version "3.1.10"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
+ integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.1"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ extglob "^2.0.4"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.2"
+ nanomatch "^1.2.9"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.2"
+
+miller-rabin@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+ integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==
+ dependencies:
+ bn.js "^4.0.0"
+ brorand "^1.0.1"
+
+mime-db@~1.37.0:
+ version "1.37.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8"
+ integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==
+
+mime-types@^2.1.12, mime-types@~2.1.18, mime-types@~2.1.19:
+ version "2.1.21"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96"
+ integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==
+ dependencies:
+ mime-db "~1.37.0"
+
+mime@^2.3.1:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.0.tgz#e051fd881358585f3279df333fe694da0bcffdd6"
+ integrity sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==
+
+mimic-fn@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
+ integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
+
+minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
+ integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+ integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
+
+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"
+ integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+ 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"
+ integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
+
+minimist@^1.1.1, minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+ integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
+
+minimist@~0.0.1:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
+ integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
+
+minipass@^2.2.1, minipass@^2.3.4:
+ version "2.3.5"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848"
+ integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==
+ dependencies:
+ safe-buffer "^5.1.2"
+ yallist "^3.0.0"
+
+minizlib@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.1.tgz#6734acc045a46e61d596a43bb9d9cd326e19cc42"
+ integrity sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg==
+ dependencies:
+ minipass "^2.2.1"
+
+mississippi@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
+ integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==
+ dependencies:
+ concat-stream "^1.5.0"
+ duplexify "^3.4.2"
+ end-of-stream "^1.1.0"
+ flush-write-stream "^1.0.0"
+ from2 "^2.1.0"
+ parallel-transform "^1.1.0"
+ pump "^3.0.0"
+ pumpify "^1.3.3"
+ stream-each "^1.1.0"
+ through2 "^2.0.0"
+
+mixin-deep@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
+ integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
+mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
+ dependencies:
+ minimist "0.0.8"
+
+mock-socket@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-2.0.0.tgz#d2f941eb8010c2beaa97983e4827fbc62111abcb"
+ integrity sha1-0vlB64AQwr6ql5g+SCf7xiERq8s=
+ dependencies:
+ urijs "~1.17.0"
+
+move-concurrently@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
+ integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=
+ dependencies:
+ aproba "^1.1.1"
+ copy-concurrently "^1.0.0"
+ fs-write-stream-atomic "^1.0.8"
+ mkdirp "^0.5.1"
+ rimraf "^2.5.4"
+ run-queue "^1.0.3"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+ms@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+ integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+
+mute-stream@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
+ integrity sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=
+
+mute-stream@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
+ integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
+
+nan@^2.9.2:
+ version "2.11.1"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766"
+ integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==
+
+nanomatch@^1.2.9:
+ version "1.2.13"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
+ integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ fragment-cache "^0.2.1"
+ is-windows "^1.0.2"
+ kind-of "^6.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+ integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+
+needle@^2.2.1:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e"
+ integrity sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==
+ dependencies:
+ debug "^2.1.2"
+ iconv-lite "^0.4.4"
+ sax "^1.2.4"
+
+negotiator@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
+ integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=
+
+neo-async@^2.5.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.0.tgz#b9d15e4d71c6762908654b5183ed38b753340835"
+ integrity sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA==
+
+next-tick@1:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
+ integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
+
+nice-try@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+ integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+node-int64@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+ integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=
+
+node-libs-browser@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df"
+ integrity sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==
+ dependencies:
+ assert "^1.1.1"
+ browserify-zlib "^0.2.0"
+ buffer "^4.3.0"
+ console-browserify "^1.1.0"
+ constants-browserify "^1.0.0"
+ crypto-browserify "^3.11.0"
+ domain-browser "^1.1.1"
+ events "^1.0.0"
+ https-browserify "^1.0.0"
+ os-browserify "^0.3.0"
+ path-browserify "0.0.0"
+ process "^0.11.10"
+ punycode "^1.2.4"
+ querystring-es3 "^0.2.0"
+ readable-stream "^2.3.3"
+ stream-browserify "^2.0.1"
+ stream-http "^2.7.2"
+ string_decoder "^1.0.0"
+ timers-browserify "^2.0.4"
+ tty-browserify "0.0.0"
+ url "^0.11.0"
+ util "^0.10.3"
+ vm-browserify "0.0.4"
+
+node-pre-gyp@^0.10.0:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
+ integrity sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==
+ dependencies:
+ detect-libc "^1.0.2"
+ mkdirp "^0.5.1"
+ needle "^2.2.1"
+ nopt "^4.0.1"
+ npm-packlist "^1.1.6"
+ npmlog "^4.0.2"
+ rc "^1.2.7"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^4"
+
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+normalize-package-data@^2.3.2:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==
+ 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.0, normalize-path@^2.0.1, normalize-path@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+npm-bundled@^1.0.1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979"
+ integrity sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==
+
+npm-packlist@^1.1.6:
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.12.tgz#22bde2ebc12e72ca482abd67afc51eb49377243a"
+ integrity sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==
+ dependencies:
+ ignore-walk "^3.0.1"
+ npm-bundled "^1.0.1"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
+ dependencies:
+ path-key "^2.0.0"
+
+npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
+null-check@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd"
+ integrity sha1-l33/1xdgErnsMNKjnbXPcqBDnt0=
+
+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"
+ integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
+
+oauth-sign@~0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+ integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
+object-assign@^4.0.1, object-assign@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+ integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+object-component@0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
+ integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=
+
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw=
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=
+ dependencies:
+ isobject "^3.0.0"
+
+object.omit@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+ integrity sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=
+ dependencies:
+ for-own "^0.1.4"
+ is-extendable "^0.1.1"
+
+object.pick@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+ integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=
+ dependencies:
+ isobject "^3.0.1"
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+ integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+ dependencies:
+ ee-first "1.1.1"
+
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+ dependencies:
+ wrappy "1"
+
+onetime@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
+ integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=
+
+onetime@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+ integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
+ dependencies:
+ mimic-fn "^1.0.0"
+
+optimist@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+ integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY=
+ dependencies:
+ minimist "~0.0.1"
+ wordwrap "~0.0.2"
+
+optionator@^0.8.1, optionator@^0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=
+ 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-browserify@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
+ integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+ integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
+
+os-tmpdir@^1.0.0, 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"
+ integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+
+osenv@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
+ integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+ integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
+
+p-limit@^1.1.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
+ integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
+ dependencies:
+ p-try "^1.0.0"
+
+p-limit@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.0.0.tgz#e624ed54ee8c460a778b3c9f3670496ff8a57aec"
+ integrity sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==
+ dependencies:
+ p-try "^2.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"
+ integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
+ dependencies:
+ p-limit "^1.1.0"
+
+p-locate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
+ integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
+ dependencies:
+ p-limit "^2.0.0"
+
+p-try@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+ integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
+
+p-try@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
+ integrity sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==
+
+pako@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.7.tgz#2473439021b57f1516c82f58be7275ad8ef1bb27"
+ integrity sha512-3HNK5tW4x8o5mO8RuHZp3Ydw9icZXx0RANAOMzlMzx7LVXhMJ4mo3MOBpzyd7r/+RUu8BmndP47LXT+vzjtWcQ==
+
+parallel-transform@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06"
+ integrity sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=
+ dependencies:
+ cyclist "~0.2.2"
+ inherits "^2.0.3"
+ readable-stream "^2.1.5"
+
+parse-asn1@^5.0.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8"
+ integrity sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==
+ dependencies:
+ asn1.js "^4.0.0"
+ browserify-aes "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.0"
+ pbkdf2 "^3.0.3"
+
+parse-glob@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+ integrity sha1-ssN2z7EfNVE7rdFz7wu246OIORw=
+ 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"
+ integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=
+ dependencies:
+ error-ex "^1.2.0"
+
+parse-passwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
+ integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
+
+parseqs@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
+ integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=
+ dependencies:
+ better-assert "~1.0.0"
+
+parseuri@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a"
+ integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=
+ dependencies:
+ better-assert "~1.0.0"
+
+parseurl@~1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
+ integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+ integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
+
+path-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
+ integrity sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=
+
+path-dirname@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+ integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
+
+path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=
+ 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"
+ integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+
+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"
+ integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+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"
+ integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
+
+path-key@^2.0.0, path-key@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+ integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
+
+path-parse@^1.0.5:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
+ integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
+
+path-type@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+ integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=
+ dependencies:
+ pify "^2.0.0"
+
+pbkdf2@^3.0.3:
+ version "3.0.17"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6"
+ integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==
+ dependencies:
+ create-hash "^1.1.2"
+ create-hmac "^1.1.4"
+ ripemd160 "^2.0.1"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+ integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
+
+pify@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+ integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+ integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o=
+ 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"
+ integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
+
+pkg-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
+ integrity sha1-ektQio1bstYp1EcFb/TpyTFM89Q=
+ dependencies:
+ find-up "^1.0.0"
+
+pkg-dir@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
+ integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==
+ dependencies:
+ find-up "^3.0.0"
+
+pluralize@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
+ integrity sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=
+
+pluralize@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
+ integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==
+
+posix-character-classes@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+ integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+ integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=
+
+preserve@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+ integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=
+
+private@^0.1.6, private@^0.1.8:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+ integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
+
+process-nextick-args@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
+ integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==
+
+process@^0.11.10:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+ integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
+
+progress@^1.1.8:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
+ integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=
+
+progress@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.1.tgz#c9242169342b1c29d275889c95734621b1952e31"
+ integrity sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg==
+
+promise-inflight@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
+ integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
+
+prr@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
+ integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+ integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
+
+psl@^1.1.24:
+ version "1.1.29"
+ resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
+ integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==
+
+public-encrypt@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
+ integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==
+ dependencies:
+ bn.js "^4.1.0"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ parse-asn1 "^5.0.0"
+ randombytes "^2.0.1"
+ safe-buffer "^5.1.2"
+
+pump@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
+ integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+pump@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+ integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+pumpify@^1.3.3:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce"
+ integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==
+ dependencies:
+ duplexify "^3.6.0"
+ inherits "^2.0.3"
+ pump "^2.0.0"
+
+punycode@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+ integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
+
+punycode@^1.2.4, punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+ integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
+
+punycode@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+ integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+q@1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e"
+ integrity sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=
+
+q@^1.5.0:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
+ integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
+
+qjobs@^1.1.4:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
+ integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==
+
+qs@6.5.2, qs@~6.5.2:
+ version "6.5.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+ integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
+
+querystring-es3@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+ integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
+
+querystring@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+ integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
+
+qunit@^2.8.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/qunit/-/qunit-2.8.0.tgz#007474727cdba323c35f9526e21c0687f8ea04b3"
+ integrity sha512-bT7vvvE4Xvk6c/uSbvP11uZXlzPJINURQyG9zj5I0EXXycW9oeDCodvAOK3GuYZ+GoXiTAMsxVSXCPGeXlTWzg==
+ dependencies:
+ commander "2.12.2"
+ exists-stat "1.0.0"
+ findup-sync "2.0.0"
+ js-reporters "1.2.1"
+ resolve "1.5.0"
+ sane "^4.0.0"
+ walk-sync "0.3.2"
+
+randomatic@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed"
+ integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==
+ dependencies:
+ is-number "^4.0.0"
+ kind-of "^6.0.0"
+ math-random "^1.0.1"
+
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"
+ integrity sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==
+ dependencies:
+ safe-buffer "^5.1.0"
+
+randomfill@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458"
+ integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==
+ dependencies:
+ randombytes "^2.0.5"
+ safe-buffer "^5.1.0"
+
+range-parser@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
+ integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=
+
+raw-body@2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3"
+ integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==
+ dependencies:
+ bytes "3.0.0"
+ http-errors "1.6.3"
+ iconv-lite "0.4.23"
+ unpipe "1.0.0"
+
+rc@^1.2.7:
+ version "1.2.8"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
+ integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
+ dependencies:
+ deep-extend "^0.6.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+read-pkg-up@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+ integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=
+ 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"
+ integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=
+ dependencies:
+ load-json-file "^2.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^2.0.0"
+
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
+ version "2.3.6"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
+ integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
+readdirp@^2.0.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
+ integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==
+ dependencies:
+ graceful-fs "^4.1.11"
+ micromatch "^3.1.10"
+ readable-stream "^2.0.2"
+
+readline2@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35"
+ integrity sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ mute-stream "0.0.5"
+
+regenerate@^1.2.1:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
+ integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
+
+regenerator-runtime@^0.11.0:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+ integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
+
+regenerator-transform@^0.10.0:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
+ integrity sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==
+ 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"
+ integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==
+ dependencies:
+ is-equal-shallow "^0.1.3"
+
+regex-not@^1.0.0, regex-not@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
+ integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==
+ dependencies:
+ extend-shallow "^3.0.2"
+ safe-regex "^1.1.0"
+
+regexpp@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab"
+ integrity sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw==
+
+regexpu-core@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
+ integrity sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=
+ 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"
+ integrity sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=
+
+regjsparser@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
+ integrity sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=
+ 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"
+ integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8=
+
+repeat-element@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
+ integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
+
+repeat-string@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae"
+ integrity sha1-x6jTI2BoNiBZp+RlH8aITosftK4=
+
+repeat-string@^1.5.2, repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+ integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=
+ dependencies:
+ is-finite "^1.0.0"
+
+request@2.88.0:
+ version "2.88.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+ integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.8.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.6"
+ extend "~3.0.2"
+ forever-agent "~0.6.1"
+ form-data "~2.3.2"
+ har-validator "~5.1.0"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.19"
+ oauth-sign "~0.9.0"
+ performance-now "^2.1.0"
+ qs "~6.5.2"
+ safe-buffer "^5.1.2"
+ tough-cookie "~2.4.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.3.2"
+
+require-uncached@^1.0.2, require-uncached@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
+ integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=
+ dependencies:
+ caller-path "^0.1.0"
+ resolve-from "^1.0.0"
+
+requires-port@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+ integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
+
+resolve-dir@^1.0.0, resolve-dir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
+ integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=
+ dependencies:
+ expand-tilde "^2.0.0"
+ global-modules "^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"
+ integrity sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=
+
+resolve-url@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+ integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
+
+resolve@1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
+ integrity sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==
+ dependencies:
+ path-parse "^1.0.5"
+
+resolve@^0.6.3:
+ version "0.6.3"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-0.6.3.tgz#dd957982e7e736debdf53b58a4dd91754575dd46"
+ integrity sha1-3ZV5gufnNt699TtYpN2RdUV13UY=
+
+resolve@^1.1.6, resolve@^1.5.0, resolve@^1.6.0, resolve@^1.8.1:
+ version "1.8.1"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.8.1.tgz#82f1ec19a423ac1fbd080b0bab06ba36e84a7a26"
+ integrity sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==
+ dependencies:
+ path-parse "^1.0.5"
+
+restore-cursor@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
+ integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=
+ dependencies:
+ exit-hook "^1.0.0"
+ onetime "^1.0.0"
+
+restore-cursor@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+ integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
+ dependencies:
+ onetime "^2.0.0"
+ signal-exit "^3.0.2"
+
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+ integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
+rfdc@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.2.tgz#e6e72d74f5dc39de8f538f65e00c36c18018e349"
+ integrity sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA==
+
+rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
+ integrity sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==
+ dependencies:
+ glob "^7.0.5"
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
+ integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
+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"
+ integrity sha512-bVe2y0z/V5Ax1qU8NX/0idmzIwJPdUGu8Xx3vXH73h0yGjxfv2gkFI82MBVg49SlsFlLTBadBHb67zy4TWM3hA==
+ dependencies:
+ rollup-pluginutils "^1.5.0"
+
+rollup-plugin-commonjs@^9.1.0:
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-9.2.0.tgz#4604e25069e0c78a09e08faa95dc32dec27f7c89"
+ integrity sha512-0RM5U4Vd6iHjL6rLvr3lKBwnPsaVml+qxOGaaNUWN1lSq6S33KhITOfHmvxV3z2vy9Mk4t0g4rNlVaJJsNQPWA==
+ dependencies:
+ estree-walker "^0.5.2"
+ magic-string "^0.25.1"
+ resolve "^1.8.1"
+ rollup-pluginutils "^2.3.3"
+
+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"
+ integrity sha512-PJcd85dxfSBWih84ozRtBkB731OjXk0KnzN0oGp7WOWcarAFkVa71cV5hTJg2qpVsV2U8EUwrzHP3tvy9vS3qg==
+ 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"
+ integrity sha512-dehLu9eRRoV4l09aC+ySntRw1OAfoyKdbk8Nelblj03tHoynkSybqyEpgavemi1LBOH6S1vzI58/mpxkZIe1iQ==
+ 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"
+ integrity sha1-HhVud4+UtyVb+hs9AXi+j1xVJAg=
+ dependencies:
+ estree-walker "^0.2.1"
+ minimatch "^3.0.2"
+
+rollup-pluginutils@^2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.3.3.tgz#3aad9b1eb3e7fe8262820818840bf091e5ae6794"
+ integrity sha512-2XZwja7b6P5q4RZ5FhyX1+f46xi1Z3qBKigLRZ6VTZjwbN0K1IFGMlwm06Uu0Emcre2Z63l77nq/pzn+KxIEoA==
+ 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"
+ integrity sha512-RZVvCWm9BHOYloaE6LLiE/ibpjv1CmI8F8k0B0Cp+q1eezo3cswszJH1DN0djgzSlo0hjuuCmyeI+1XOYLl4wg==
+ dependencies:
+ "@types/estree" "0.0.38"
+ "@types/node" "*"
+
+rsvp@^3.3.3:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a"
+ integrity sha512-OfWGQTb9vnwRjwtA2QwpG2ICclHC3pgXZO5xt8H2EfgDquO0qVdSb5T88L4qJVAEugbS56pAuV4XZM58UX8ulw==
+
+run-async@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
+ integrity sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=
+ dependencies:
+ once "^1.3.0"
+
+run-async@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
+ integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA=
+ dependencies:
+ is-promise "^2.1.0"
+
+run-queue@^1.0.0, run-queue@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47"
+ integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=
+ dependencies:
+ aproba "^1.1.1"
+
+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"
+ integrity sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=
+ 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"
+ integrity sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=
+
+rx-lite@^3.1.2:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
+ integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-regex@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
+ integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4=
+ dependencies:
+ ret "~0.1.10"
+
+"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sane@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-4.0.2.tgz#5bd4a3f1268fd7a921a2dc657047de635c8f8f25"
+ integrity sha512-/3STCUfNSgMVpoREJc1i6ajKFlYZ5OflzZTOhlqPLa+01Ey+QR9iGZK7K5/qIRsQbEDCvqEJH/PL7yZywmnWsA==
+ dependencies:
+ anymatch "^2.0.0"
+ capture-exit "^1.2.0"
+ exec-sh "^0.3.2"
+ execa "^1.0.0"
+ fb-watchman "^2.0.0"
+ micromatch "^3.1.4"
+ minimist "^1.1.1"
+ walker "~1.0.5"
+ watch "~0.18.0"
+
+sauce-connect-launcher@^1.2.2:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sauce-connect-launcher/-/sauce-connect-launcher-1.2.4.tgz#8d38f85242a9fbede1b2303b559f7e20c5609a1c"
+ integrity sha512-X2vfwulR6brUGiicXKxPm1GJ7dBEeP1II450Uv4bHGrcGOapZNgzJvn9aioea5IC5BPp/7qjKdE3xbbTBIVXMA==
+ dependencies:
+ adm-zip "~0.4.3"
+ async "^2.1.2"
+ https-proxy-agent "^2.2.1"
+ lodash "^4.16.6"
+ rimraf "^2.5.4"
+
+saucelabs@^1.4.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/saucelabs/-/saucelabs-1.5.0.tgz#9405a73c360d449b232839919a86c396d379fd9d"
+ integrity sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==
+ dependencies:
+ https-proxy-agent "^2.2.1"
+
+sax@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+ integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+schema-utils@^0.4.4:
+ version "0.4.7"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187"
+ integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==
+ dependencies:
+ ajv "^6.1.0"
+ ajv-keywords "^3.1.0"
+
+schema-utils@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
+ integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==
+ dependencies:
+ ajv "^6.1.0"
+ ajv-errors "^1.0.0"
+ ajv-keywords "^3.1.0"
+
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0:
+ version "5.6.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
+ integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
+
+serialize-javascript@^1.4.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.5.0.tgz#1aa336162c88a890ddad5384baebc93a655161fe"
+ integrity sha512-Ga8c8NjAAp46Br4+0oZ2WxJCwIzwP60Gq1YPgU+39PiTVxyed/iKE/zyZI6+UlVYH5Q4PaQdHhcegIFPZTUfoQ==
+
+set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+ integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
+
+set-value@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
+ integrity sha1-fbCPnT0i3H945Trzw79GZuzfzPE=
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.1"
+ to-object-path "^0.3.0"
+
+set-value@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274"
+ integrity sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
+setimmediate@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+ integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
+
+setprototypeof@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
+ integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
+
+sha.js@^2.4.0, sha.js@^2.4.8:
+ version "2.4.11"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
+ integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
+ 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"
+ integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
+
+shelljs@^0.6.0:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8"
+ integrity sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+ integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
+
+slash@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+ integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=
+
+slice-ansi@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
+ integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=
+
+slice-ansi@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
+ integrity sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==
+ dependencies:
+ define-property "^1.0.0"
+ isobject "^3.0.0"
+ snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+ integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==
+ dependencies:
+ kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d"
+ integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==
+ dependencies:
+ base "^0.11.1"
+ debug "^2.2.0"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ map-cache "^0.2.2"
+ source-map "^0.5.6"
+ source-map-resolve "^0.5.0"
+ use "^3.1.0"
+
+socket.io-adapter@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b"
+ integrity sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs=
+
+socket.io-client@2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.1.1.tgz#dcb38103436ab4578ddb026638ae2f21b623671f"
+ integrity sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==
+ dependencies:
+ backo2 "1.0.2"
+ base64-arraybuffer "0.1.5"
+ component-bind "1.0.0"
+ component-emitter "1.2.1"
+ debug "~3.1.0"
+ engine.io-client "~3.2.0"
+ has-binary2 "~1.0.2"
+ has-cors "1.1.0"
+ indexof "0.0.1"
+ object-component "0.0.3"
+ parseqs "0.0.5"
+ parseuri "0.0.5"
+ socket.io-parser "~3.2.0"
+ to-array "0.1.4"
+
+socket.io-parser@~3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.2.0.tgz#e7c6228b6aa1f814e6148aea325b51aa9499e077"
+ integrity sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==
+ dependencies:
+ component-emitter "1.2.1"
+ debug "~3.1.0"
+ isarray "2.0.1"
+
+socket.io@2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.1.1.tgz#a069c5feabee3e6b214a75b40ce0652e1cfb9980"
+ integrity sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==
+ dependencies:
+ debug "~3.1.0"
+ engine.io "~3.2.0"
+ has-binary2 "~1.0.2"
+ socket.io-adapter "~1.1.0"
+ socket.io-client "2.1.1"
+ socket.io-parser "~3.2.0"
+
+source-list-map@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
+ integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
+
+source-map-resolve@^0.5.0:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259"
+ integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==
+ dependencies:
+ atob "^2.1.1"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.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"
+ integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==
+ dependencies:
+ source-map "^0.5.6"
+
+source-map-support@~0.5.6:
+ version "0.5.9"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f"
+ integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==
+ dependencies:
+ buffer-from "^1.0.0"
+ source-map "^0.6.0"
+
+source-map-url@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+ integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=
+
+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"
+ integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+sourcemap-codec@^1.4.1:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.4.tgz#c63ea927c029dd6bd9a2b7fa03b3fec02ad56e9f"
+ integrity sha512-CYAPYdBu34781kLHkaW3m6b/uUSyMOC2R61gcYMWooeuaGtjof86ZA/8T+qVPPt7np1085CR9hmMGrySwEc8Xg==
+
+spark-md5@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spark-md5/-/spark-md5-3.0.0.tgz#3722227c54e2faf24b1dc6d933cc144e6f71bfef"
+ integrity sha1-NyIifFTi+vJLHcbZM8wUTm9xv+8=
+
+spdx-correct@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.2.tgz#19bb409e91b47b1ad54159243f7312a858db3c2e"
+ integrity sha512-q9hedtzyXHr5S0A1vEPoK/7l8NpfkFYTq6iCY+Pno2ZbdZR6WexZFtqeVGkGxW3TEJMN914Z55EnAGMmenlIQQ==
+ dependencies:
+ spdx-expression-parse "^3.0.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-exceptions@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977"
+ integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==
+
+spdx-expression-parse@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
+ integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==
+ dependencies:
+ spdx-exceptions "^2.1.0"
+ spdx-license-ids "^3.0.0"
+
+spdx-license-ids@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz#a59efc09784c2a5bada13cfeaf5c75dd214044d2"
+ integrity sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg==
+
+split-string@^3.0.1, split-string@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+ integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==
+ dependencies:
+ extend-shallow "^3.0.0"
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+ integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+
+sshpk@^1.7.0:
+ version "1.15.2"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.2.tgz#c946d6bd9b1a39d0e8635763f5242d6ed6dcb629"
+ integrity sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ bcrypt-pbkdf "^1.0.0"
+ dashdash "^1.12.0"
+ ecc-jsbn "~0.1.1"
+ getpass "^0.1.1"
+ jsbn "~0.1.0"
+ safer-buffer "^2.0.2"
+ tweetnacl "~0.14.0"
+
+ssri@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8"
+ integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==
+ dependencies:
+ figgy-pudding "^3.5.1"
+
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=
+ dependencies:
+ define-property "^0.2.5"
+ object-copy "^0.1.0"
+
+"statuses@>= 1.4.0 < 2":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+ integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+
+statuses@~1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
+ integrity sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=
+
+stream-browserify@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
+ integrity sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "^2.0.2"
+
+stream-each@^1.1.0:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
+ integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==
+ dependencies:
+ end-of-stream "^1.1.0"
+ stream-shift "^1.0.0"
+
+stream-http@^2.7.2:
+ version "2.8.3"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc"
+ integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==
+ dependencies:
+ builtin-status-codes "^3.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.3.6"
+ to-arraybuffer "^1.0.0"
+ xtend "^4.0.0"
+
+stream-shift@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
+ integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
+
+streamroller@0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.7.0.tgz#a1d1b7cf83d39afb0d63049a5acbf93493bdf64b"
+ integrity sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ==
+ dependencies:
+ date-format "^1.2.0"
+ debug "^3.1.0"
+ mkdirp "^0.5.1"
+ readable-stream "^2.3.0"
+
+string-width@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+"string-width@^1.0.2 || 2", string-width@^2.0.0, 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"
+ integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string_decoder@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
+ integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+ integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
+ 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"
+ integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
+ 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"
+ integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+ integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
+
+strip-json-comments@^1.0.2, strip-json-comments@~1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
+ integrity sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=
+
+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"
+ integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+ integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
+
+supports-color@^5.3.0:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
+ integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
+ dependencies:
+ has-flag "^3.0.0"
+
+table@4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
+ integrity sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==
+ 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"
+
+table@^3.7.8:
+ version "3.8.3"
+ resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f"
+ integrity sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=
+ 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"
+
+tapable@^1.0.0, tapable@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.1.tgz#4d297923c5a72a42360de2ab52dadfaaec00018e"
+ integrity sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==
+
+tar-stream@^1.5.0:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
+ integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
+ dependencies:
+ bl "^1.0.0"
+ buffer-alloc "^1.2.0"
+ end-of-stream "^1.0.0"
+ fs-constants "^1.0.0"
+ readable-stream "^2.3.0"
+ to-buffer "^1.1.1"
+ xtend "^4.0.0"
+
+tar@^4:
+ version "4.4.8"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d"
+ integrity sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==
+ dependencies:
+ chownr "^1.1.1"
+ fs-minipass "^1.2.5"
+ minipass "^2.3.4"
+ minizlib "^1.1.1"
+ mkdirp "^0.5.0"
+ safe-buffer "^5.1.2"
+ yallist "^3.0.2"
+
+terser-webpack-plugin@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.1.0.tgz#cf7c25a1eee25bf121f4a587bb9e004e3f80e528"
+ integrity sha512-61lV0DSxMAZ8AyZG7/A4a3UPlrbOBo8NIQ4tJzLPAdGOQ+yoNC7l5ijEow27lBAL2humer01KLS6bGIMYQxKoA==
+ dependencies:
+ cacache "^11.0.2"
+ find-cache-dir "^2.0.0"
+ schema-utils "^1.0.0"
+ serialize-javascript "^1.4.0"
+ source-map "^0.6.1"
+ terser "^3.8.1"
+ webpack-sources "^1.1.0"
+ worker-farm "^1.5.2"
+
+terser@^3.8.1:
+ version "3.10.13"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-3.10.13.tgz#00a8b2e9c1bec3f681257d90f96c3b1292ada97a"
+ integrity sha512-AgdHqw2leuADuHiP4Kkk1i40m10RMGguPaiCw6MVD6jtDR7N94zohGqAS2lkDXIS7eIkGit3ief3eQGh/Md+GQ==
+ dependencies:
+ commander "~2.17.1"
+ source-map "~0.6.1"
+ source-map-support "~0.5.6"
+
+text-table@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+ integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
+
+through2@^2.0.0:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
+ integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==
+ dependencies:
+ readable-stream "~2.3.6"
+ xtend "~4.0.1"
+
+through@^2.3.6:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+ integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+
+timers-browserify@^2.0.4:
+ version "2.0.10"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae"
+ integrity sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==
+ dependencies:
+ setimmediate "^1.0.4"
+
+tmp@0.0.33, tmp@0.0.x, tmp@^0.0.33:
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+ integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
+ dependencies:
+ os-tmpdir "~1.0.2"
+
+tmpl@1.0.x:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+ integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
+
+to-array@0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
+ integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA=
+
+to-arraybuffer@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+ integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
+
+to-buffer@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80"
+ integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==
+
+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"
+ integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=
+
+to-object-path@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+ integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=
+ dependencies:
+ kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+ integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
+to-regex@^3.0.1, to-regex@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
+ integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==
+ dependencies:
+ define-property "^2.0.2"
+ extend-shallow "^3.0.2"
+ regex-not "^1.0.2"
+ safe-regex "^1.1.0"
+
+tough-cookie@~2.4.3:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
+ integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
+ dependencies:
+ psl "^1.1.24"
+ punycode "^1.4.1"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+ integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
+
+trix@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/trix/-/trix-1.0.0.tgz#e9cc98cf6030c908f8d54e317b5b072f927b0c6b"
+ integrity sha512-feli9QVXe6gzZOCUfpPGpNDURW9jMciIRVQ5gkDudOctcA1oMtI5K/qEbsL2rFCoGl1rSoeRt+HPhIFGyQscKg==
+
+tslib@^1.9.0:
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
+ integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
+
+tty-browserify@0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
+ integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+ integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=
+ dependencies:
+ prelude-ls "~1.1.2"
+
+type-is@~1.6.16:
+ version "1.6.16"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
+ integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.18"
+
+typedarray@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+ integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
+
+uglify-es@^3.3.7:
+ version "3.3.9"
+ resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
+ integrity sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==
+ dependencies:
+ commander "~2.13.0"
+ source-map "~0.6.1"
+
+ultron@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
+ integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
+
+union-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
+ integrity sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^0.4.3"
+
+unique-filename@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"
+ integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==
+ dependencies:
+ unique-slug "^2.0.0"
+
+unique-slug@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.1.tgz#5e9edc6d1ce8fb264db18a507ef9bd8544451ca6"
+ integrity sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg==
+ dependencies:
+ imurmurhash "^0.1.4"
+
+unpipe@1.0.0, unpipe@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+ integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
+
+unset-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+ integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=
+ dependencies:
+ has-value "^0.3.1"
+ isobject "^3.0.0"
+
+upath@^1.0.5:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd"
+ integrity sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw==
+
+uri-js@^4.2.2:
+ version "4.2.2"
+ resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
+ integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==
+ dependencies:
+ punycode "^2.1.0"
+
+urijs@~1.17.0:
+ version "1.17.1"
+ resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.17.1.tgz#0a28bf2e00dfc24eeb0974feb8268a238c7bac2d"
+ integrity sha1-Cii/LgDfwk7rCXT+uCaKI4x7rC0=
+
+urix@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+ integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=
+
+url@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+ integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
+ dependencies:
+ punycode "1.3.2"
+ querystring "0.2.0"
+
+use@^3.1.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
+ integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
+
+user-home@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f"
+ integrity sha1-nHC/2Babwdy/SGBODwS4tJzenp8=
+ dependencies:
+ os-homedir "^1.0.0"
+
+useragent@2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972"
+ integrity sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==
+ dependencies:
+ lru-cache "4.1.x"
+ tmp "0.0.x"
+
+util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+ integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+util@0.10.3:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+ integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk=
+ dependencies:
+ inherits "2.0.1"
+
+util@^0.10.3:
+ version "0.10.4"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
+ integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
+ dependencies:
+ inherits "2.0.3"
+
+utils-merge@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
+ integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
+
+uuid@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+ integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
+ integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
+ dependencies:
+ spdx-correct "^3.0.0"
+ spdx-expression-parse "^3.0.0"
+
+vargs@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/vargs/-/vargs-0.1.0.tgz#6b6184da6520cc3204ce1b407cac26d92609ebff"
+ integrity sha1-a2GE2mUgzDIEzhtAfKwm2SYJ6/8=
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+vm-browserify@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
+ integrity sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=
+ dependencies:
+ indexof "0.0.1"
+
+void-elements@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+ integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
+
+walk-sync@0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.3.2.tgz#4827280afc42d0e035367c4a4e31eeac0d136f75"
+ integrity sha512-FMB5VqpLqOCcqrzA9okZFc0wq0Qbmdm396qJxvQZhDpyu0W95G9JCmp74tx7iyYnyOcBtUuKJsgIKAqjozvmmQ==
+ dependencies:
+ ensure-posix-path "^1.0.0"
+ matcher-collection "^1.0.0"
+
+walker@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+ integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=
+ dependencies:
+ makeerror "1.0.x"
+
+watch@~0.18.0:
+ version "0.18.0"
+ resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986"
+ integrity sha1-KAlUdsbffJDJYxOJkMClQj60uYY=
+ dependencies:
+ exec-sh "^0.2.0"
+ minimist "^1.2.0"
+
+watchpack@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"
+ integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==
+ dependencies:
+ chokidar "^2.0.2"
+ graceful-fs "^4.1.2"
+ neo-async "^2.5.0"
+
+wd@^1.4.0:
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/wd/-/wd-1.11.1.tgz#21a33e21977ad20522bb189f6529c3b55ac3862c"
+ integrity sha512-XNK6EbOrXF7cG8f3pbps6mb/+xPGZH2r1AL1zGJluGynA/Xt6ip1Tvqj2AkavyDFworreaGXoe+0AP/r7EX9pg==
+ dependencies:
+ archiver "2.1.1"
+ async "2.0.1"
+ lodash "4.17.11"
+ mkdirp "^0.5.1"
+ q "1.4.1"
+ request "2.88.0"
+ vargs "0.1.0"
+
+webpack-sources@^1.1.0, webpack-sources@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85"
+ integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==
+ dependencies:
+ source-list-map "^2.0.0"
+ source-map "~0.6.1"
+
+webpack@^4.17.1:
+ version "4.26.1"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.26.1.tgz#ff3a9283d363c07b3494dfa702d08f4f2ef6cb39"
+ integrity sha512-i2oOvEvuvLLSuSCkdVrknaxAhtUZ9g+nLSoHCWV0gDzqGX2DXaCrMmMUpbRsTSSLrUqAI56PoEiyMUZIZ1msug==
+ dependencies:
+ "@webassemblyjs/ast" "1.7.11"
+ "@webassemblyjs/helper-module-context" "1.7.11"
+ "@webassemblyjs/wasm-edit" "1.7.11"
+ "@webassemblyjs/wasm-parser" "1.7.11"
+ acorn "^5.6.2"
+ acorn-dynamic-import "^3.0.0"
+ ajv "^6.1.0"
+ ajv-keywords "^3.1.0"
+ chrome-trace-event "^1.0.0"
+ enhanced-resolve "^4.1.0"
+ eslint-scope "^4.0.0"
+ json-parse-better-errors "^1.0.2"
+ loader-runner "^2.3.0"
+ loader-utils "^1.1.0"
+ memory-fs "~0.4.1"
+ micromatch "^3.1.8"
+ mkdirp "~0.5.0"
+ neo-async "^2.5.0"
+ node-libs-browser "^2.0.0"
+ schema-utils "^0.4.4"
+ tapable "^1.1.0"
+ terser-webpack-plugin "^1.1.0"
+ watchpack "^1.5.0"
+ webpack-sources "^1.3.0"
+
+which@^1.2.1, which@^1.2.14, which@^1.2.9:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
+ integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
+ dependencies:
+ string-width "^1.0.2 || 2"
+
+wordwrap@~0.0.2:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+ integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc=
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+ integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
+
+worker-farm@^1.5.2:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0"
+ integrity sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==
+ dependencies:
+ errno "~0.1.7"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+ integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+write@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
+ integrity sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=
+ dependencies:
+ mkdirp "^0.5.1"
+
+ws@~3.3.1:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
+ integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==
+ dependencies:
+ async-limiter "~1.0.0"
+ safe-buffer "~5.1.0"
+ ultron "~1.1.0"
+
+xmlhttprequest-ssl@~1.5.4:
+ version "1.5.5"
+ resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
+ integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=
+
+xtend@^4.0.0, xtend@~4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+ integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
+
+y18n@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
+ integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+ integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
+
+yallist@^3.0.0, yallist@^3.0.2:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"
+ integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==
+
+yeast@0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
+ integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
+
+zip-stream@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04"
+ integrity sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=
+ dependencies:
+ archiver-utils "^1.3.0"
+ compress-commons "^1.2.0"
+ lodash "^4.8.0"
+ readable-stream "^2.0.0"