aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml4
-rw-r--r--.travis.yml3
-rw-r--r--Gemfile10
-rw-r--r--Gemfile.lock114
-rw-r--r--Rakefile4
-rw-r--r--actioncable/CHANGELOG.md13
-rw-r--r--actioncable/lib/action_cable/channel/base.rb38
-rw-r--r--actioncable/lib/action_cable/channel/streams.rb10
-rw-r--r--actioncable/lib/action_cable/connection.rb2
-rw-r--r--actioncable/lib/action_cable/connection/base.rb4
-rw-r--r--actioncable/lib/action_cable/connection/faye_client_socket.rb48
-rw-r--r--actioncable/lib/action_cable/connection/faye_event_loop.rb44
-rw-r--r--actioncable/lib/action_cable/connection/stream.rb57
-rw-r--r--actioncable/lib/action_cable/connection/stream_event_loop.rb46
-rw-r--r--actioncable/lib/action_cable/connection/subscriptions.rb6
-rw-r--r--actioncable/lib/action_cable/connection/web_socket.rb4
-rw-r--r--actioncable/lib/action_cable/server/base.rb12
-rw-r--r--actioncable/lib/action_cable/server/configuration.rb18
-rw-r--r--actioncable/lib/action_cable/server/worker.rb2
-rw-r--r--actioncable/test/channel/base_test.rb15
-rw-r--r--actioncable/test/channel/periodic_timers_test.rb1
-rw-r--r--actioncable/test/channel/rejection_test.rb2
-rw-r--r--actioncable/test/channel/stream_test.rb31
-rw-r--r--actioncable/test/client_test.rb139
-rw-r--r--actioncable/test/connection/client_socket_test.rb4
-rw-r--r--actioncable/test/connection/stream_test.rb2
-rw-r--r--actioncable/test/server/base_test.rb33
-rw-r--r--actioncable/test/stubs/test_server.rb12
-rw-r--r--actioncable/test/subscription_adapter/common.rb2
-rw-r--r--actioncable/test/subscription_adapter/evented_redis_test.rb8
-rw-r--r--actioncable/test/test_helper.rb48
-rw-r--r--actionmailer/lib/action_mailer/base.rb30
-rw-r--r--actionpack/CHANGELOG.md8
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb12
-rw-r--r--actionpack/lib/action_controller/metal/etag_with_template_digest.rb8
-rw-r--r--actionpack/lib/action_controller/metal/helpers.rb2
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb14
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb2
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb8
-rw-r--r--actionpack/lib/action_controller/test_case.rb10
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb4
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.rb22
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb26
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb7
-rw-r--r--actionpack/lib/action_dispatch/testing/test_process.rb3
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.rb2
-rw-r--r--actionpack/test/abstract_unit.rb2
-rw-r--r--actionpack/test/assertions/response_assertions_test.rb24
-rw-r--r--actionpack/test/controller/base_test.rb2
-rw-r--r--actionpack/test/controller/flash_test.rb2
-rw-r--r--actionpack/test/controller/helper_test.rb24
-rw-r--r--actionpack/test/controller/new_base/render_context_test.rb2
-rw-r--r--actionpack/test/controller/render_test.rb2
-rw-r--r--actionpack/test/controller/request/test_request_test.rb5
-rw-r--r--actionpack/test/controller/resources_test.rb2
-rw-r--r--actionpack/test/controller/test_case_test.rb12
-rw-r--r--actionpack/test/dispatch/cookies_test.rb6
-rw-r--r--actionpack/test/dispatch/header_test.rb4
-rw-r--r--actionpack/test/dispatch/test_request_test.rb7
-rw-r--r--actionview/CHANGELOG.md18
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/date_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_helpers.rb4
-rw-r--r--actionview/lib/action_view/helpers/tags/select.rb8
-rw-r--r--actionview/lib/action_view/layouts.rb22
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb28
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb4
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb12
-rw-r--r--actionview/lib/action_view/template.rb8
-rw-r--r--actionview/lib/action_view/template/html.rb4
-rw-r--r--actionview/lib/action_view/template/resolver.rb10
-rw-r--r--actionview/lib/action_view/template/text.rb4
-rw-r--r--actionview/lib/action_view/view_paths.rb4
-rw-r--r--actionview/test/abstract_unit.rb2
-rw-r--r--actionview/test/actionpack/abstract/abstract_controller_test.rb2
-rw-r--r--actionview/test/activerecord/polymorphic_routes_test.rb2
-rw-r--r--actionview/test/fixtures/test/render_file_inspect_local_assigns.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/template/compiled_templates_test.rb19
-rw-r--r--actionview/test/template/form_helper_test.rb4
-rw-r--r--actionview/test/template/lookup_context_test.rb4
-rw-r--r--actionview/test/template/render_test.rb2
-rw-r--r--actionview/test/template/test_case_test.rb14
-rw-r--r--activejob/lib/active_job/queue_adapters.rb2
-rw-r--r--activemodel/lib/active_model/conversion.rb13
-rw-r--r--activemodel/lib/active_model/dirty.rb18
-rw-r--r--activemodel/lib/active_model/errors.rb2
-rw-r--r--activemodel/lib/active_model/type/date_time.rb4
-rw-r--r--activemodel/lib/active_model/type/helpers/time_value.rb2
-rw-r--r--activemodel/lib/active_model/type/value.rb6
-rw-r--r--activemodel/test/cases/errors_test.rb14
-rw-r--r--activemodel/test/cases/serializers/json_serialization_test.rb12
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb2
-rw-r--r--activemodel/test/cases/validations/validations_context_test.rb10
-rw-r--r--activemodel/test/cases/validations/with_validation_test.rb12
-rw-r--r--activemodel/test/cases/validations_test.rb12
-rw-r--r--activerecord/CHANGELOG.md38
-rw-r--r--activerecord/lib/active_record/aggregations.rb310
-rw-r--r--activerecord/lib/active_record/associations.rb1752
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb16
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb34
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb4
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb14
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb10
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb36
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb2
-rw-r--r--activerecord/lib/active_record/autosave_association.rb22
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb84
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb92
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb8
-rw-r--r--activerecord/lib/active_record/core.rb2
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb8
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb7
-rw-r--r--activerecord/lib/active_record/fixtures.rb2
-rw-r--r--activerecord/lib/active_record/inheritance.rb24
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb8
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb1
-rw-r--r--activerecord/lib/active_record/migration.rb11
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb2
-rw-r--r--activerecord/lib/active_record/model_schema.rb4
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb142
-rw-r--r--activerecord/lib/active_record/persistence.rb9
-rw-r--r--activerecord/lib/active_record/relation.rb1
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb32
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb32
-rw-r--r--activerecord/lib/active_record/relation/record_fetch_warning.rb6
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb4
-rw-r--r--activerecord/lib/active_record/sanitization.rb160
-rw-r--r--activerecord/lib/active_record/scoping/default.rb88
-rw-r--r--activerecord/lib/active_record/statement_cache.rb4
-rw-r--r--activerecord/lib/active_record/transactions.rb46
-rw-r--r--activerecord/lib/active_record/type/internal/abstract_json.rb6
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb6
-rw-r--r--activerecord/test/cases/adapter_test.rb16
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb21
-rw-r--r--activerecord/test/cases/adapters/mysql2/json_test.rb16
-rw-r--r--activerecord/test/cases/adapters/mysql2/reserved_word_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb16
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/transaction_test.rb21
-rw-r--r--activerecord/test/cases/associations/callbacks_test.rb6
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb4
-rw-r--r--activerecord/test/cases/associations/eager_test.rb22
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb20
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb36
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb48
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb10
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb26
-rw-r--r--activerecord/test/cases/associations_test.rb6
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb5
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb6
-rw-r--r--activerecord/test/cases/attributes_test.rb4
-rw-r--r--activerecord/test/cases/autosave_association_test.rb16
-rw-r--r--activerecord/test/cases/base_test.rb2
-rw-r--r--activerecord/test/cases/calculations_test.rb8
-rw-r--r--activerecord/test/cases/dirty_test.rb6
-rw-r--r--activerecord/test/cases/enum_test.rb40
-rw-r--r--activerecord/test/cases/finder_test.rb6
-rw-r--r--activerecord/test/cases/fixtures_test.rb6
-rw-r--r--activerecord/test/cases/hot_compatibility_test.rb10
-rw-r--r--activerecord/test/cases/json_serialization_test.rb10
-rw-r--r--activerecord/test/cases/locking_test.rb1
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb2
-rw-r--r--activerecord/test/cases/migration/columns_test.rb8
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb2
-rw-r--r--activerecord/test/cases/persistence_test.rb6
-rw-r--r--activerecord/test/cases/quoting_test.rb8
-rw-r--r--activerecord/test/cases/reflection_test.rb6
-rw-r--r--activerecord/test/cases/relation/merging_test.rb6
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb2
-rw-r--r--activerecord/test/cases/relations_test.rb8
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb4
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb4
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb14
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb2
-rw-r--r--activerecord/test/cases/test_case.rb7
-rw-r--r--activerecord/test/cases/transactions_test.rb11
-rw-r--r--activerecord/test/cases/validations/i18n_validation_test.rb2
-rw-r--r--activerecord/test/cases/view_test.rb2
-rw-r--r--activerecord/test/fixtures/all/namespaced/accounts.yml2
-rw-r--r--activerecord/test/fixtures/books.yml1
-rw-r--r--activerecord/test/models/book.rb1
-rw-r--r--activerecord/test/models/bulb.rb6
-rw-r--r--activerecord/test/schema/schema.rb1
-rw-r--r--activesupport/CHANGELOG.md19
-rw-r--r--activesupport/lib/active_support/cache.rb11
-rw-r--r--activesupport/lib/active_support/callbacks.rb348
-rw-r--r--activesupport/lib/active_support/concurrency/share_lock.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/calculations.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/load_error.rb1
-rw-r--r--activesupport/lib/active_support/duration/iso8601_parser.rb4
-rw-r--r--activesupport/lib/active_support/file_update_checker.rb14
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb18
-rw-r--r--activesupport/lib/active_support/key_generator.rb4
-rw-r--r--activesupport/lib/active_support/lazy_load_hooks.rb4
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb30
-rw-r--r--activesupport/lib/active_support/testing/autorun.rb11
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb5
-rw-r--r--activesupport/lib/active_support/xml_mini.rb2
-rw-r--r--activesupport/lib/active_support/xml_mini/jdom.rb80
-rw-r--r--activesupport/test/autoload_test.rb12
-rw-r--r--activesupport/test/caching_test.rb6
-rw-r--r--activesupport/test/callbacks_test.rb73
-rw-r--r--activesupport/test/concern_test.rb4
-rw-r--r--activesupport/test/configurable_test.rb4
-rw-r--r--activesupport/test/core_ext/array/conversions_test.rb46
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb68
-rw-r--r--activesupport/test/core_ext/module/concerning_test.rb6
-rw-r--r--activesupport/test/core_ext/range_ext_test.rb6
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb32
-rw-r--r--activesupport/test/dependencies_test.rb18
-rw-r--r--activesupport/test/inflector_test.rb12
-rw-r--r--activesupport/test/logger_test.rb34
-rw-r--r--activesupport/test/multibyte_chars_test.rb10
-rw-r--r--activesupport/test/ordered_hash_test.rb12
-rw-r--r--activesupport/test/time_zone_test.rb8
-rw-r--r--guides/bug_report_templates/active_record_migrations_gem.rb65
-rw-r--r--guides/bug_report_templates/active_record_migrations_master.rb64
-rw-r--r--guides/rails_guides/levenshtein.rb4
-rw-r--r--guides/source/2_2_release_notes.md1
-rw-r--r--guides/source/5_0_release_notes.md8
-rw-r--r--guides/source/action_view_overview.md14
-rw-r--r--guides/source/active_record_querying.md5
-rw-r--r--guides/source/active_support_instrumentation.md13
-rw-r--r--guides/source/configuring.md2
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md1
-rw-r--r--guides/source/engines.md2
-rw-r--r--guides/source/layouts_and_rendering.md4
-rw-r--r--guides/source/testing.md12
-rw-r--r--railties/CHANGELOG.md4
-rw-r--r--railties/lib/rails.rb2
-rw-r--r--railties/lib/rails/cli.rb6
-rw-r--r--railties/lib/rails/command.rb99
-rw-r--r--railties/lib/rails/command/actions.rb42
-rw-r--r--railties/lib/rails/command/base.rb135
-rw-r--r--railties/lib/rails/command/behavior.rb123
-rw-r--r--railties/lib/rails/command/environment_argument.rb34
-rw-r--r--railties/lib/rails/commands.rb6
-rw-r--r--railties/lib/rails/commands/application/application_command.rb (renamed from railties/lib/rails/commands/application.rb)18
-rw-r--r--railties/lib/rails/commands/commands_tasks.rb136
-rw-r--r--railties/lib/rails/commands/common_commands_tasks.rb68
-rw-r--r--railties/lib/rails/commands/console/console_command.rb (renamed from railties/lib/rails/commands/console.rb)51
-rw-r--r--railties/lib/rails/commands/console_helper.rb34
-rw-r--r--railties/lib/rails/commands/dbconsole/dbconsole_command.rb (renamed from railties/lib/rails/commands/dbconsole.rb)87
-rw-r--r--railties/lib/rails/commands/destroy.rb11
-rw-r--r--railties/lib/rails/commands/destroy/destroy_command.rb21
-rw-r--r--railties/lib/rails/commands/generate.rb13
-rw-r--r--railties/lib/rails/commands/generate/generate_command.rb21
-rw-r--r--railties/lib/rails/commands/help/USAGE27
-rw-r--r--railties/lib/rails/commands/help/help_command.rb13
-rw-r--r--railties/lib/rails/commands/new/new_command.rb15
-rw-r--r--railties/lib/rails/commands/plugin.rb24
-rw-r--r--railties/lib/rails/commands/plugin/plugin_command.rb43
-rw-r--r--railties/lib/rails/commands/rake/rake_command.rb51
-rw-r--r--railties/lib/rails/commands/rake_proxy.rb41
-rw-r--r--railties/lib/rails/commands/runner.rb71
-rw-r--r--railties/lib/rails/commands/runner/USAGE17
-rw-r--r--railties/lib/rails/commands/runner/runner_command.rb45
-rw-r--r--railties/lib/rails/commands/server/server_command.rb (renamed from railties/lib/rails/commands/server.rb)75
-rw-r--r--railties/lib/rails/commands/test.rb9
-rw-r--r--railties/lib/rails/commands/test/test_command.rb20
-rw-r--r--railties/lib/rails/commands/version/version_command.rb9
-rw-r--r--railties/lib/rails/engine.rb2
-rw-r--r--railties/lib/rails/engine/commands.rb6
-rw-r--r--railties/lib/rails/engine/commands_tasks.rb62
-rw-r--r--railties/lib/rails/generators.rb216
-rw-r--r--railties/lib/rails/generators/app_base.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/puma.rb9
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/test_helper.rb1
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/bin/test.tt4
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb3
-rw-r--r--railties/lib/rails/paths.rb2
-rw-r--r--railties/lib/rails/test_unit/minitest_plugin.rb26
-rw-r--r--railties/test/application/bin_setup_test.rb18
-rw-r--r--railties/test/application/console_test.rb2
-rw-r--r--railties/test/application/generators_test.rb8
-rw-r--r--railties/test/application/initializers/i18n_test.rb10
-rw-r--r--railties/test/application/initializers/load_path_test.rb4
-rw-r--r--railties/test/application/middleware/session_test.rb2
-rw-r--r--railties/test/application/middleware_test.rb24
-rw-r--r--railties/test/application/paths_test.rb6
-rw-r--r--railties/test/application/rake/restart_test.rb14
-rw-r--r--railties/test/application/runner_test.rb2
-rw-r--r--railties/test/application/test_test.rb38
-rw-r--r--railties/test/commands/console_test.rb36
-rw-r--r--railties/test/commands/dbconsole_test.rb77
-rw-r--r--railties/test/commands/server_test.rb3
-rw-r--r--railties/test/generators/app_generator_test.rb2
-rw-r--r--railties/test/generators/named_base_test.rb4
-rw-r--r--railties/test/generators/plugin_generator_test.rb2
-rw-r--r--railties/test/generators/plugin_test_runner_test.rb6
-rw-r--r--railties/test/generators_test.rb6
-rw-r--r--railties/test/isolation/abstract_unit.rb5
-rw-r--r--railties/test/paths_test.rb24
-rw-r--r--railties/test/rails_info_test.rb4
-rw-r--r--railties/test/railties/railtie_test.rb2
311 files changed, 4473 insertions, 3516 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 8f619b7956..139e6a7efb 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -20,6 +20,10 @@ Style/BracesAroundHashParameters:
Style/CaseIndentation:
Enabled: true
+# Align comments with method definitions.
+Style/CommentIndentation:
+ Enabled: true
+
# No extra empty lines.
Style/EmptyLines:
Enabled: true
diff --git a/.travis.yml b/.travis.yml
index f01b58ecb3..8bebd3f484 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -36,9 +36,7 @@ env:
- "GEM=railties"
- "GEM=ap"
- "GEM=ac"
- - "GEM=ac FAYE=1"
- "GEM=ac:integration"
- - "GEM=ac:integration FAYE=1"
- "GEM=am,amo,as,av,aj"
- "GEM=as PRESERVE_TIMEZONES=1"
- "GEM=ar:mysql2"
@@ -69,7 +67,6 @@ matrix:
- rvm: ruby-head
- rvm: jruby-9.0.5.0
- env: "GEM=ac:integration"
- - env: "GEM=ac:integration FAYE=1"
fast_finish: true
notifications:
diff --git a/Gemfile b/Gemfile
index 83276212f7..5b0ecd7b3d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,5 +1,10 @@
source "https://rubygems.org"
+git_source(:github) do |repo_name|
+ repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
+ "https://github.com/#{repo_name}.git"
+end
+
gemspec
# We need a newish Rake since Active Job sets its test tasks' descriptions.
@@ -67,10 +72,7 @@ group :cable do
gem "hiredis", require: false
gem "redis", require: false
- gem "faye-websocket", require: false
-
- # Lock to 1.1.1 until the fix for https://github.com/faye/faye/issues/394 is released
- gem "faye", "1.1.1", require: false
+ gem "websocket-client-simple", require: false
gem "blade", require: false, platforms: [:ruby]
gem "blade-sauce_labs_plugin", require: false, platforms: [:ruby]
diff --git a/Gemfile.lock b/Gemfile.lock
index cf8bb0fec9..46a50ef39e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,28 +1,28 @@
GIT
- remote: git://github.com/QueueClassic/queue_classic.git
- revision: c26f2c9f6f6133b946fbcdd7b7ec905a4aca9f94
+ remote: https://github.com/QueueClassic/queue_classic.git
+ revision: 1ef197b9db8149a895e59077badcb5b94d4c8b44
branch: master
specs:
queue_classic (3.2.0.RC1)
pg (>= 0.17, < 0.19)
GIT
- remote: git://github.com/collectiveidea/delayed_job.git
- revision: 71f1d5faf934d3057abca942f0d410327bc69087
+ remote: https://github.com/collectiveidea/delayed_job.git
+ revision: e3772d4f0c8470d0fcba00c86ca3bc4f5e876830
specs:
- delayed_job (4.1.1)
+ delayed_job (4.1.2)
activesupport (>= 3.0, < 5.1)
GIT
- remote: git://github.com/collectiveidea/delayed_job_active_record.git
- revision: 61e688e03b2ef4004b08de6d1e0a123fda8fffad
+ remote: https://github.com/collectiveidea/delayed_job_active_record.git
+ revision: 36f434c4fd660e8f11ce932be117e9c71dde7212
specs:
- delayed_job_active_record (4.1.0)
+ delayed_job_active_record (4.1.1)
activerecord (>= 3.0, < 5.1)
delayed_job (>= 3.0, < 5)
GIT
- remote: git://github.com/matthewd/rb-inotify.git
+ remote: https://github.com/matthewd/rb-inotify.git
revision: 90553518d1fb79aedc98a3036c59bd2b6731ac40
branch: close-handling
specs:
@@ -30,8 +30,8 @@ GIT
ffi (>= 0.5.0)
GIT
- remote: git://github.com/resque/resque.git
- revision: 06036388ec61e573c761ac5a25a2ef3c76537ec7
+ remote: https://github.com/resque/resque.git
+ revision: a3a66389618b830de0e6acf862b0dc9fde05cf49
specs:
resque (1.27.0)
mono_logger (~> 1.0)
@@ -41,8 +41,8 @@ GIT
vegas (~> 0.1.2)
GIT
- remote: git://github.com/sass/sass.git
- revision: 3fda1cbe70d615e7ef96e28db4fd1f8a3ebb5505
+ remote: https://github.com/sass/sass.git
+ revision: 7716e67f3507c6f65878c69aa49ec358ebf675c7
branch: stable
specs:
sass (3.4.22)
@@ -111,7 +111,7 @@ GEM
specs:
addressable (2.4.0)
amq-protocol (2.0.1)
- arel (7.1.1)
+ arel (7.1.2)
backburner (1.3.0)
beaneater (~> 1.0)
dante (> 0.1.5)
@@ -119,7 +119,7 @@ GEM
bcrypt (3.1.11-x64-mingw32)
bcrypt (3.1.11-x86-mingw32)
beaneater (1.0.0)
- benchmark-ips (2.6.1)
+ benchmark-ips (2.7.2)
blade (0.5.6)
activesupport (>= 3.0.0)
blade-qunit_adapter (~> 1.20.0)
@@ -134,14 +134,14 @@ GEM
thor (~> 0.19.1)
useragent (~> 0.16.7)
blade-qunit_adapter (1.20.0)
- blade-sauce_labs_plugin (0.5.2)
+ blade-sauce_labs_plugin (0.5.3)
childprocess
faraday
selenium-webdriver
builder (3.2.2)
bunny (2.2.2)
amq-protocol (>= 2.0.1)
- byebug (8.2.5)
+ byebug (9.0.5)
childprocess (0.5.9)
ffi (~> 1.0, >= 1.0.11)
coffee-rails (4.2.1)
@@ -153,23 +153,24 @@ GEM
coffee-script-source (1.10.0)
concurrent-ruby (1.0.2)
connection_pool (2.2.0)
- cookiejar (0.3.0)
+ cookiejar (0.3.3)
curses (1.0.2)
- daemons (1.2.3)
+ daemons (1.2.4)
dalli (2.7.6)
dante (0.2.0)
em-hiredis (0.3.1)
eventmachine (~> 1.0)
hiredis (~> 0.6.0)
- em-http-request (1.1.3)
+ em-http-request (1.1.5)
addressable (>= 2.3.4)
- cookiejar (<= 0.3.0)
+ cookiejar (!= 0.3.1)
em-socksify (>= 0.3)
eventmachine (>= 1.0.3)
http_parser.rb (>= 0.6.0)
em-socksify (0.3.1)
eventmachine (>= 1.0.0.beta.4)
erubis (2.7.0)
+ event_emitter (0.2.5)
eventmachine (1.2.0.1)
eventmachine (1.2.0.1-x64-mingw32)
eventmachine (1.2.0.1-x86-mingw32)
@@ -187,15 +188,15 @@ GEM
faye-websocket (0.10.4)
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)
+ ffi (1.9.14)
+ ffi (1.9.14-x64-mingw32)
+ ffi (1.9.14-x86-mingw32)
+ globalid (0.3.7)
activesupport (>= 4.1.0)
hiredis (0.6.1)
http_parser.rb (0.6.0)
i18n (0.7.0)
- jquery-rails (4.1.1)
+ jquery-rails (4.2.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
@@ -203,18 +204,18 @@ GEM
kindlerb (0.1.1)
mustache
nokogiri
- listen (3.0.7)
- rb-fsevent (>= 0.9.3)
- rb-inotify (>= 0.9.7)
+ listen (3.0.8)
+ rb-fsevent (~> 0.9, >= 0.9.4)
+ rb-inotify (~> 0.9, >= 0.9.7)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.4)
mime-types (>= 1.16, < 4)
metaclass (0.0.4)
method_source (0.8.2)
- mime-types (3.0)
+ mime-types (3.1)
mime-types-data (~> 3.2015)
- mime-types-data (3.2016.0221)
+ mime-types-data (3.2016.0521)
mini_portile2 (2.1.0)
minitest (5.3.3)
mocha (0.14.0)
@@ -240,42 +241,44 @@ GEM
pg (0.18.4-x64-mingw32)
pg (0.18.4-x86-mingw32)
pkg-config (1.1.7)
- psych (2.1.0)
- puma (3.4.0)
+ psych (2.1.1)
+ puma (3.6.0)
qu (0.2.0)
multi_json
qu-redis (0.2.0)
qu (= 0.2.0)
redis-namespace
simple_uuid
- que (0.11.4)
+ que (0.12.0)
racc (1.4.14)
rack (2.0.1)
rack-cache (1.6.1)
rack (>= 0.4)
+ rack-protection (1.5.3)
+ rack
rack-test (0.6.3)
rack (>= 1.0)
- rails-dom-testing (2.0.0)
+ rails-dom-testing (2.0.1)
activesupport (>= 4.2.0, < 6.0)
nokogiri (~> 1.6.0)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
- rake (11.1.2)
+ rake (11.2.2)
rb-fsevent (0.9.7)
rdoc (4.2.2)
json (~> 1.4)
redcarpet (3.2.3)
- redis (3.3.0)
+ redis (3.3.1)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
- resque-scheduler (4.2.0)
+ resque-scheduler (4.3.0)
mono_logger (~> 1.0)
- redis (~> 3.0)
- resque (~> 1.25)
+ redis (~> 3.3)
+ resque (~> 1.26)
rufus-scheduler (~> 3.2)
rubyzip (1.2.0)
- rufus-scheduler (3.2.1)
- sass-rails (5.0.5)
+ rufus-scheduler (3.2.2)
+ sass-rails (5.0.6)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
@@ -284,16 +287,17 @@ GEM
sdoc (0.4.1)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
- selenium-webdriver (2.53.0)
+ selenium-webdriver (2.53.4)
childprocess (~> 0.5)
rubyzip (~> 1.0)
websocket (~> 1.0)
- sequel (4.34.0)
+ sequel (4.38.0)
serverengine (1.5.11)
sigdump (~> 0.2.2)
- sidekiq (4.1.2)
+ sidekiq (4.2.0)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
+ rack-protection (~> 1.5)
redis (~> 3.2, >= 3.2.1)
sigdump (0.2.4)
simple_uuid (0.4.0)
@@ -304,11 +308,11 @@ GEM
serverengine (~> 1.5.11)
thor
thread (~> 0.1.7)
- sprockets (3.6.1)
+ sprockets (3.7.0)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-export (0.9.1)
- sprockets-rails (3.1.1)
+ sprockets-rails (3.2.0)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
@@ -326,16 +330,16 @@ GEM
thread (0.1.7)
thread_safe (0.3.5)
tilt (2.0.5)
- turbolinks (5.0.0)
+ turbolinks (5.0.1)
turbolinks-source (~> 5)
turbolinks-source (5.0.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
- tzinfo-data (1.2016.4)
+ tzinfo-data (1.2016.6)
tzinfo (>= 1.0.0)
- uglifier (3.0.0)
+ uglifier (3.0.2)
execjs (>= 0.3.0, < 3)
- useragent (0.16.7)
+ useragent (0.16.8)
vegas (0.1.11)
rack (>= 1.0.0)
w3c_validators (1.2)
@@ -343,6 +347,9 @@ GEM
nokogiri
wdm (0.1.1)
websocket (1.2.3)
+ websocket-client-simple (0.3.0)
+ event_emitter
+ websocket
websocket-driver (0.6.4)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
@@ -367,8 +374,6 @@ DEPENDENCIES
delayed_job!
delayed_job_active_record!
em-hiredis
- faye (= 1.1.1)
- faye-websocket
hiredis
jquery-rails
kindlerb (= 0.1.1)
@@ -406,6 +411,7 @@ DEPENDENCIES
uglifier (>= 1.3.0)
w3c_validators
wdm (>= 0.1.0)
+ websocket-client-simple
BUNDLED WITH
- 1.12.5
+ 1.13.1
diff --git a/Rakefile b/Rakefile
index 3bc4cd19fb..202eb5e6fc 100644
--- a/Rakefile
+++ b/Rakefile
@@ -23,9 +23,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,7 +33,6 @@ 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."
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md
index dec6f7c027..137c88d91b 100644
--- a/actioncable/CHANGELOG.md
+++ b/actioncable/CHANGELOG.md
@@ -1,3 +1,16 @@
+* Prevent race where the client could receive and act upon a
+ subscription confirmation before the channel's `subscribed` method
+ completed.
+
+ Fixes #25381.
+
+ *Vladimir Dementyev*
+
+* Buffer writes to websocket connections, to avoid blocking threads
+ that could be doing more useful things.
+
+ *Matthew Draper*, *Tinco Andringa*
+
* Protect against concurrent writes to a websocket connection from
multiple threads; the underlying OS write is not always threadsafe.
diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb
index 2e589a2cfa..a866044f95 100644
--- a/actioncable/lib/action_cable/channel/base.rb
+++ b/actioncable/lib/action_cable/channel/base.rb
@@ -144,13 +144,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 +170,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:
@@ -201,12 +213,18 @@ module ActionCable
end
end
+ def ensure_confirmation_sent
+ return if subscription_rejected?
+ @defer_subscription_confirmation_counter.decrement
+ transmit_subscription_confirmation unless defer_subscription_confirmation?
+ end
+
def defer_subscription_confirmation!
- @defer_subscription_confirmation = true
+ @defer_subscription_confirmation_counter.increment
end
def defer_subscription_confirmation?
- @defer_subscription_confirmation
+ @defer_subscription_confirmation_counter.value > 0
end
def subscription_confirmation_sent?
@@ -230,18 +248,6 @@ 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
end
diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb
index 561750d713..dbba333353 100644
--- a/actioncable/lib/action_cable/channel/streams.rb
+++ b/actioncable/lib/action_cable/channel/streams.rb
@@ -69,8 +69,8 @@ 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)
@@ -84,7 +84,7 @@ module ActionCable
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
@@ -94,8 +94,8 @@ 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)
end
diff --git a/actioncable/lib/action_cable/connection.rb b/actioncable/lib/action_cable/connection.rb
index 5f813cf8e0..902efb07e2 100644
--- a/actioncable/lib/action_cable/connection.rb
+++ b/actioncable/lib/action_cable/connection.rb
@@ -8,8 +8,6 @@ module ActionCable
autoload :ClientSocket
autoload :Identification
autoload :InternalChannel
- autoload :FayeClientSocket
- autoload :FayeEventLoop
autoload :MessageBuffer
autoload :Stream
autoload :StreamEventLoop
diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb
index b0615b08a1..06f4f5edd3 100644
--- a/actioncable/lib/action_cable/connection/base.rb
+++ b/actioncable/lib/action_cable/connection/base.rb
@@ -57,7 +57,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)
@@ -105,7 +105,7 @@ 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
{
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 06e92c5d52..0000000000
--- a/actioncable/lib/action_cable/connection/faye_client_socket.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require "faye/websocket"
-
-module ActionCable
- module Connection
- class FayeClientSocket
- def initialize(env, event_target, stream_event_loop, protocols)
- @env = env
- @event_target = event_target
- @protocols = protocols
-
- @faye = nil
- end
-
- def alive?
- @faye && @faye.ready_state == Faye::WebSocket::API::OPEN
- end
-
- def transmit(data)
- connect
- @faye.send data
- end
-
- def close
- @faye && @faye.close
- end
-
- def protocol
- @faye && @faye.protocol
- end
-
- def rack_response
- connect
- @faye.rack_response
- end
-
- private
- def connect
- return if @faye
- @faye = Faye::WebSocket.new(@env, @protocols)
-
- @faye.on(:open) { |event| @event_target.on_open }
- @faye.on(:message) { |event| @event_target.on_message(event.data) }
- @faye.on(:close) { |event| @event_target.on_close(event.reason, event.code) }
- @faye.on(:error) { |event| @event_target.on_error(event.message) }
- end
- end
- end
-end
diff --git a/actioncable/lib/action_cable/connection/faye_event_loop.rb b/actioncable/lib/action_cable/connection/faye_event_loop.rb
deleted file mode 100644
index cfbe26ee6a..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/stream.rb b/actioncable/lib/action_cable/connection/stream.rb
index 5a2aace0ba..d66e1b4e41 100644
--- a/actioncable/lib/action_cable/connection/stream.rb
+++ b/actioncable/lib/action_cable/connection/stream.rb
@@ -14,6 +14,9 @@ module ActionCable
@rack_hijack_io = nil
@write_lock = Mutex.new
+
+ @write_head = nil
+ @write_buffer = Queue.new
end
def each(&callback)
@@ -30,14 +33,62 @@ module ActionCable
end
def write(data)
- @write_lock.synchronize do
- 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
diff --git a/actioncable/lib/action_cable/connection/stream_event_loop.rb b/actioncable/lib/action_cable/connection/stream_event_loop.rb
index 106b948c45..eec24638b6 100644
--- a/actioncable/lib/action_cable/connection/stream_event_loop.rb
+++ b/actioncable/lib/action_cable/connection/stream_event_loop.rb
@@ -5,7 +5,7 @@ module ActionCable
module Connection
class StreamEventLoop
def initialize
- @nio = @thread = nil
+ @nio = @executor = @thread = nil
@map = {}
@stopping = false
@todo = Queue.new
@@ -20,13 +20,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
@@ -39,6 +40,15 @@ module ActionCable
wakeup
end
+ def writes_pending(io)
+ @todo << lambda do
+ if monitor = @map[io]
+ monitor.interests = :rw
+ end
+ end
+ wakeup
+ end
+
def stop
@stopping = true
wakeup if @nio
@@ -52,6 +62,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 +94,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 9060183249..00511aead5 100644
--- a/actioncable/lib/action_cable/connection/subscriptions.rb
+++ b/actioncable/lib/action_cable/connection/subscriptions.rb
@@ -26,10 +26,14 @@ module ActionCable
id_key = data["identifier"]
id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
+ return if subscriptions.key?(id_key)
+
subscription_klass = id_options[:channel].safe_constantize
if subscription_klass && ActionCable::Channel::Base >= subscription_klass
- subscriptions[id_key] ||= subscription_klass.new(connection, id_key, id_options)
+ subscription = subscription_klass.new(connection, id_key, id_options)
+ subscriptions[id_key] = subscription
+ subscription.subscribe_to_channel
else
logger.error "Subscription class not found: #{id_options[:channel].inspect}"
end
diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb
index 52d8daad4b..382141b89f 100644
--- a/actioncable/lib/action_cable/connection/web_socket.rb
+++ b/actioncable/lib/action_cable/connection/web_socket.rb
@@ -4,8 +4,8 @@ module ActionCable
module Connection
# Wrap the real socket to minimize the externally-presented API
class WebSocket
- def initialize(env, event_target, event_loop, client_socket_class, protocols: ActionCable::INTERNAL[:protocols])
- @websocket = ::WebSocket::Driver.websocket?(env) ? client_socket_class.new(env, event_target, event_loop, protocols) : nil
+ 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?
diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb
index c700297a8d..419eccd73c 100644
--- a/actioncable/lib/action_cable/server/base.rb
+++ b/actioncable/lib/action_cable/server/base.rb
@@ -37,9 +37,13 @@ module ActionCable
connections.each(&:close)
@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,12 +53,12 @@ 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
diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb
index 7153593d4c..dc146f07b0 100644
--- a/actioncable/lib/action_cable/server/configuration.rb
+++ b/actioncable/lib/action_cable/server/configuration.rb
@@ -4,7 +4,7 @@ module ActionCable
# in a Rails config initializer.
class Configuration
attr_accessor :logger, :log_tags
- attr_accessor :use_faye, :connection_class, :worker_pool_size
+ attr_accessor :connection_class, :worker_pool_size
attr_accessor :disable_request_forgery_protection, :allowed_request_origins
attr_accessor :cable, :url, :mount_path
@@ -35,22 +35,6 @@ module ActionCable
adapter = "PostgreSQL" if adapter == "Postgresql"
"ActionCable::SubscriptionAdapter::#{adapter}".constantize
end
-
- def event_loop_class
- if use_faye
- ActionCable::Connection::FayeEventLoop
- else
- ActionCable::Connection::StreamEventLoop
- end
- end
-
- def client_socket_class
- if use_faye
- ActionCable::Connection::FayeClientSocket
- else
- ActionCable::Connection::ClientSocket
- end
- end
end
end
end
diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb
index 7460472551..43639c27af 100644
--- a/actioncable/lib/action_cable/server/worker.rb
+++ b/actioncable/lib/action_cable/server/worker.rb
@@ -25,7 +25,7 @@ module ActionCable
# Stop processing work: any work that has not already started
# running will be discarded from the queue
def halt
- @executor.kill
+ @executor.shutdown
end
def stopping?
diff --git a/actioncable/test/channel/base_test.rb b/actioncable/test/channel/base_test.rb
index 2bb3214f74..9a3a3581e6 100644
--- a/actioncable/test/channel/base_test.rb
+++ b/actioncable/test/channel/base_test.rb
@@ -77,11 +77,13 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
@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,6 +92,8 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
end
test "unsubscribing from a channel" do
+ @channel.subscribe_to_channel
+
assert @channel.room
assert @channel.subscribed?
@@ -150,8 +154,13 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
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
@@ -208,6 +217,8 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
test "notification for transmit_subscription_confirmation" do
begin
+ @channel.subscribe_to_channel
+
events = []
ActiveSupport::Notifications.subscribe "transmit_subscription_confirmation.action_cable" do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
diff --git a/actioncable/test/channel/periodic_timers_test.rb b/actioncable/test/channel/periodic_timers_test.rb
index 529abb9535..2ee711fd29 100644
--- a/actioncable/test/channel/periodic_timers_test.rb
+++ b/actioncable/test/channel/periodic_timers_test.rb
@@ -62,6 +62,7 @@ class ActionCable::Channel::PeriodicTimersTest < ActiveSupport::TestCase
@connection.server.event_loop.expects(:timer).times(3).returns(stub(shutdown: nil))
channel = ChatChannel.new @connection, "{id: 1}", id: 1
+ channel.subscribe_to_channel
channel.unsubscribe_from_channel
assert_equal [], channel.send(:active_periodic_timers)
end
diff --git a/actioncable/test/channel/rejection_test.rb b/actioncable/test/channel/rejection_test.rb
index faf35ad048..99c4a7603a 100644
--- a/actioncable/test/channel/rejection_test.rb
+++ b/actioncable/test/channel/rejection_test.rb
@@ -20,6 +20,7 @@ class ActionCable::Channel::RejectionTest < ActiveSupport::TestCase
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
+ @channel.subscribe_to_channel
expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" }
assert_equal expected, @connection.last_transmission
@@ -28,6 +29,7 @@ class ActionCable::Channel::RejectionTest < ActiveSupport::TestCase
test "does not execute action if subscription is rejected" do
@connection.expects(:subscriptions).returns mock().tap { |m| m.expects(:remove_subscription).with instance_of(SecretChannel) }
@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
diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb
index da26f81a5c..31dcde2e29 100644
--- a/actioncable/test/channel/stream_test.rb
+++ b/actioncable/test/channel/stream_test.rb
@@ -53,6 +53,7 @@ module ActionCable::StreamTests
connection = TestConnection.new
connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("test_room_1", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
channel = ChatChannel.new connection, "{id: 1}", id: 1
+ channel.subscribe_to_channel
connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
channel.unsubscribe_from_channel
@@ -64,6 +65,7 @@ module ActionCable::StreamTests
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, ""
+ channel.subscribe_to_channel
connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
channel.unsubscribe_from_channel
@@ -76,6 +78,7 @@ module ActionCable::StreamTests
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)
end
end
@@ -84,7 +87,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,7 +119,7 @@ module ActionCable::StreamTests
end
end
- require "action_cable/subscription_adapter/inline"
+ require "action_cable/subscription_adapter/async"
class UserCallbackChannel < ActionCable::Channel::Base
def subscribed
@@ -124,9 +129,16 @@ module ActionCable::StreamTests
end
end
- class StreamEncodingTest < ActionCable::TestCase
+ 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 )
end
@@ -153,6 +165,17 @@ module ActionCable::StreamTests
end
end
+ test "subscription confirmation should only be sent out once with muptiple stream_from" do
+ run_in_eventmachine do
+ connection = open_connection
+ expected = { "identifier" => { "channel" => MultiChatChannel.name }.to_json, "type" => "confirm_subscription" }
+ connection.websocket.expects(:transmit).with(expected.to_json)
+ receive(connection, command: "subscribe", channel: MultiChatChannel.name, identifiers: {})
+
+ wait_for_async
+ end
+ end
+
private
def subscribe_to(connection, identifiers:)
receive connection, command: "subscribe", identifiers: identifiers
diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb
index 2e3821828f..f6d4ab3202 100644
--- a/actioncable/test/client_test.rb
+++ b/actioncable/test/client_test.rb
@@ -1,13 +1,38 @@
require "test_helper"
require "concurrent"
-require "faye/websocket"
+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
+}
+
+WebSocket::Client::Simple::Client.prepend Module.new {
+ def initialize(*)
+ @socket = 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
@@ -39,20 +64,9 @@ class ClientTest < ActionCable::TestCase
server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
server.config.cable = ActiveSupport::HashWithIndifferentAccess.new(adapter: "async")
- server.config.use_faye = ENV["FAYE"].present?
# and now the "real" setup for our test:
server.config.disable_request_forgery_protection = true
-
- 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)
@@ -73,44 +87,49 @@ class ClientTest < ActionCable::TestCase
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 |event|
+ closed.set
+ end
end
- open.wait(WAIT_WHEN_EXPECTING_EVENT)
- raise error if error
+ open.wait!(WAIT_WHEN_EXPECTING_EVENT)
end
def read_message
@@ -161,13 +180,17 @@ 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)
+ 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)
@@ -179,12 +202,12 @@ class ClientTest < ActionCable::TestCase
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 {
+ 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)
@@ -194,38 +217,38 @@ class ClientTest < ActionCable::TestCase
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 = concurrently(100.times) { websocket_client(port) }
- clients.map { |c| Concurrent::Future.execute {
+ 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)
- } }.each(&:wait!)
+ 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)
+ 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)
+ 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)
@@ -240,7 +263,7 @@ class ClientTest < ActionCable::TestCase
app = ActionCable.server
identifier = JSON.generate(channel: "ClientTest::EchoChannel")
- c = faye_client(port)
+ 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)
@@ -261,7 +284,7 @@ class ClientTest < ActionCable::TestCase
def test_server_restart
with_puma_server do |port|
- c = faye_client(port)
+ c = websocket_client(port)
assert_equal({ "type" => "welcome" }, c.read_message)
c.send_message command: "subscribe", identifier: JSON.generate(channel: "ClientTest::EchoChannel")
assert_equal({ "identifier"=>"{\"channel\":\"ClientTest::EchoChannel\"}", "type"=>"confirm_subscription" }, c.read_message)
diff --git a/actioncable/test/connection/client_socket_test.rb b/actioncable/test/connection/client_socket_test.rb
index 5043a63370..dff7fefbfb 100644
--- a/actioncable/test/connection/client_socket_test.rb
+++ b/actioncable/test/connection/client_socket_test.rb
@@ -33,8 +33,6 @@ class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase
end
test "delegate socket errors to on_error handler" do
- skip if ENV["FAYE"].present?
-
run_in_eventmachine do
connection = open_connection
@@ -49,8 +47,6 @@ class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase
end
test "closes hijacked i/o socket at shutdown" do
- skip if ENV["FAYE"].present?
-
run_in_eventmachine do
connection = open_connection
diff --git a/actioncable/test/connection/stream_test.rb b/actioncable/test/connection/stream_test.rb
index 4128b32f15..36e1d3c095 100644
--- a/actioncable/test/connection/stream_test.rb
+++ b/actioncable/test/connection/stream_test.rb
@@ -34,8 +34,6 @@ 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
diff --git a/actioncable/test/server/base_test.rb b/actioncable/test/server/base_test.rb
new file mode 100644
index 0000000000..f0a51c5a7d
--- /dev/null
+++ b/actioncable/test/server/base_test.rb
@@ -0,0 +1,33 @@
+require "test_helper"
+require "stubs/test_server"
+require "active_support/core_ext/hash/indifferent_access"
+
+class BaseTest < ActiveSupport::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)
+
+ conn.expects(:close)
+ @server.restart
+ end
+
+ test "#restart shuts down worker pool" do
+ @server.worker_pool.expects(:halt)
+ @server.restart
+ end
+
+ test "#restart shuts down pub/sub adapter" do
+ @server.pubsub.expects(:shutdown)
+ @server.restart
+ end
+end
diff --git a/actioncable/test/stubs/test_server.rb b/actioncable/test/stubs/test_server.rb
index b64ff33789..5bf2a151dc 100644
--- a/actioncable/test/stubs/test_server.rb
+++ b/actioncable/test/stubs/test_server.rb
@@ -10,12 +10,6 @@ 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
end
@@ -25,10 +19,8 @@ class TestServer
end
def event_loop
- @event_loop ||= if @config.use_faye
- ActionCable::Connection::FayeEventLoop.new
- else
- ActionCable::Connection::StreamEventLoop.new
+ @event_loop ||= ActionCable::Connection::StreamEventLoop.new.tap do |loop|
+ loop.instance_variable_set(:@executor, Concurrent.global_io_executor)
end
end
diff --git a/actioncable/test/subscription_adapter/common.rb b/actioncable/test/subscription_adapter/common.rb
index 1538157995..8cf0b5c684 100644
--- a/actioncable/test/subscription_adapter/common.rb
+++ b/actioncable/test/subscription_adapter/common.rb
@@ -11,7 +11,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
diff --git a/actioncable/test/subscription_adapter/evented_redis_test.rb b/actioncable/test/subscription_adapter/evented_redis_test.rb
index d6ca0e77cb..f316bc46ef 100644
--- a/actioncable/test/subscription_adapter/evented_redis_test.rb
+++ b/actioncable/test/subscription_adapter/evented_redis_test.rb
@@ -12,6 +12,14 @@ class EventedRedisAdapterTest < ActionCable::TestCase
end
def teardown
+ super
+
+ # Ensure EM is shut down before we re-enable warnings
+ EventMachine.reactor_thread.tap do |thread|
+ EventMachine.stop
+ thread.join
+ end
+
$VERBOSE = @previous_verbose
end
diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb
index 39855ea252..a47032753b 100644
--- a/actioncable/test/test_helper.rb
+++ b/actioncable/test/test_helper.rb
@@ -13,41 +13,7 @@ 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
-
-module EventMachineConcurrencyHelpers
- def wait_for_async
- EM.run_deferred_callbacks
- end
-
- def run_in_eventmachine
- failure = nil
- EM.run do
- begin
- yield
- rescue => ex
- failure = ex
- ensure
- wait_for_async
- EM.stop if EM.reactor_running?
- end
- end
- raise failure if failure
- end
-end
-
-module ConcurrentRubyConcurrencyHelpers
+class ActionCable::TestCase < ActiveSupport::TestCase
def wait_for_async
wait_for_executor Concurrent.global_io_executor
end
@@ -56,18 +22,14 @@ module ConcurrentRubyConcurrencyHelpers
yield
wait_for_async
end
-end
-
-class ActionCable::TestCase < ActiveSupport::TestCase
- if ENV["FAYE"].present?
- include EventMachineConcurrencyHelpers
- else
- include ConcurrentRubyConcurrencyHelpers
- end
def wait_for_executor(executor)
+ # do not wait forever, wait 2s
+ timeout = 2
until executor.completed_task_count == executor.scheduled_task_count
sleep 0.1
+ timeout -= 0.1
+ raise "Executor could not complete all tasks in 2 seconds" unless timeout > 0
end
end
end
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index fb6ff819c9..e9966c7ff5 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -832,15 +832,15 @@ module ActionMailer
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.
+ # 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
@@ -859,16 +859,16 @@ module ActionMailer
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.
+ # 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
- # Emails do not support relative path links.
+ # Emails do not support relative path links.
def self.supports_path?
false
end
@@ -950,7 +950,7 @@ module ActionMailer
container.add_part(part)
end
- # This and #instrument_name is for caching instrument
+ # This and #instrument_name is for caching instrument
def instrument_payload(key)
{
mailer: mailer_name,
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 96af4d9397..e7b8e1b628 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,11 @@
+* Fix adding implicitly rendered template digests to ETags.
+
+ Fixes a case when modifying an implicitly rendered template for a
+ controller action using `fresh_when` or `stale?` would not result in a new
+ `ETag` value.
+
+ *Javan Makhmali*
+
* Make `fixture_file_upload` work in integration tests.
*Yuji Yaginuma*
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb
index cb76898f59..ef3be7af83 100644
--- a/actionpack/lib/abstract_controller/helpers.rb
+++ b/actionpack/lib/abstract_controller/helpers.rb
@@ -171,12 +171,12 @@ 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
+ # 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
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 49b5f1090e..6c103bb042 100644
--- a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
+++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
@@ -39,10 +39,10 @@ module ActionController
end
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.
+ # 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}"
diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb
index 7257bbfa95..476d081239 100644
--- a/actionpack/lib/action_controller/metal/helpers.rb
+++ b/actionpack/lib/action_controller/metal/helpers.rb
@@ -108,7 +108,7 @@ module ActionController
end
private
- # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt>
+ # Extract helper names from files in <tt>app/helpers/**/*_helper.rb</tt>
def all_application_helpers
all_helpers_from_path(helpers_path)
end
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
index 745f449a05..9d1b740025 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -128,13 +128,13 @@ 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.
+ # 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
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index 15377ddcb9..f8a037189c 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -71,8 +71,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
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index ac17d61b96..f8f91ed41c 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -72,14 +72,14 @@ module ActionController
end
end
- # Normalize arguments by catching blocks and setting them on :update.
+ # 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.
+ # Normalize both text and status options.
def _normalize_options(options) #:nodoc:
_normalize_text(options)
@@ -90,7 +90,7 @@ module ActionController
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`.
+ `text/html`.
WARNING
end
@@ -118,7 +118,7 @@ module ActionController
end
end
- # Process controller specific options, as status, content-type and location.
+ # 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)
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index 16ddd3b304..f4ec13c831 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -51,7 +51,7 @@ module ActionController
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"] }
@@ -498,10 +498,6 @@ module ActionController
parameters ||= {}
- if format
- parameters[:format] = format
- end
-
@html_document = nil
cookies.update(@request.cookies)
@@ -521,6 +517,10 @@ module ActionController
format ||= as
end
+ if format
+ parameters[:format] = format
+ end
+
parameters = parameters.symbolize_keys
generated_extras = @routes.generate_extras(parameters.merge(controller: controller_class_name, action: action.to_s))
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index 0528a6ad08..3c03976f03 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -115,8 +115,8 @@ module ActionDispatch
private
- # Converts an HTTP header name to an environment variable name if it is
- # not contained within the headers hash.
+ # 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
diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb
index 7ec9d63859..01ff2109cb 100644
--- a/actionpack/lib/action_dispatch/journey/parser.rb
+++ b/actionpack/lib/action_dispatch/journey/parser.rb
@@ -10,7 +10,7 @@ require "action_dispatch/journey/parser_extras"
module ActionDispatch
module Journey
class Parser < Racc::Parser
-##### State transition tables begin ###
+ ##### State transition tables begin ###
racc_action_table = [
13, 15, 14, 7, 21, 16, 8, 19, 13, 15,
@@ -128,9 +128,9 @@ module ActionDispatch
Racc_debug_parser = false
-##### State transition tables end #####
+ ##### State transition tables end #####
-# reduce 0 omitted
+ # reduce 0 omitted
def _reduce_1(val, _values)
Cat.new(val.first, val.last)
@@ -140,13 +140,13 @@ module ActionDispatch
val.first
end
-# reduce 3 omitted
+ # reduce 3 omitted
-# reduce 4 omitted
+ # reduce 4 omitted
-# reduce 5 omitted
+ # reduce 5 omitted
-# reduce 6 omitted
+ # reduce 6 omitted
def _reduce_7(val, _values)
Group.new(val[1])
@@ -164,13 +164,13 @@ module ActionDispatch
Star.new(Symbol.new(val.last))
end
-# reduce 11 omitted
+ # reduce 11 omitted
-# reduce 12 omitted
+ # reduce 12 omitted
-# reduce 13 omitted
+ # reduce 13 omitted
-# reduce 14 omitted
+ # reduce 14 omitted
def _reduce_15(val, _values)
Slash.new("/")
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 8d0bf680c8..5abf59402d 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -264,19 +264,19 @@ 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')
- #
+ # 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
diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb
index a00602ed70..a2eaccd9ef 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -79,7 +79,12 @@ module ActionDispatch
def generate_response_message(expected, actual = @response.response_code)
"Expected response to be a <#{code_with_name(expected)}>,"\
" but was a <#{code_with_name(actual)}>"
- .concat location_if_redirected
+ .concat(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
diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb
index 1456a0afcf..8b03b776fa 100644
--- a/actionpack/lib/action_dispatch/testing/test_process.rb
+++ b/actionpack/lib/action_dispatch/testing/test_process.rb
@@ -34,7 +34,8 @@ module ActionDispatch
#
# 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
+ 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)
diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb
index d0beb72a41..91b25ec155 100644
--- a/actionpack/lib/action_dispatch/testing/test_request.rb
+++ b/actionpack/lib/action_dispatch/testing/test_request.rb
@@ -22,7 +22,7 @@ module ActionDispatch
private_class_method :default_env
def request_method=(method)
- set_header("REQUEST_METHOD", method.to_s.upcase)
+ super(method.to_s.upcase)
end
def host=(host)
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index c5b2493e06..6d28753947 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -246,7 +246,7 @@ module ActionDispatch
class DebugExceptions
private
remove_method :stderr_logger
- # Silence logger
+ # Silence logger
def stderr_logger
nil
end
diff --git a/actionpack/test/assertions/response_assertions_test.rb b/actionpack/test/assertions/response_assertions_test.rb
index 17517f8965..14a04ccdb1 100644
--- a/actionpack/test/assertions/response_assertions_test.rb
+++ b/actionpack/test/assertions/response_assertions_test.rb
@@ -6,10 +6,11 @@ module ActionDispatch
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|
@@ -112,6 +113,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/base_test.rb b/actionpack/test/controller/base_test.rb
index 2edcbc7433..42a5157010 100644
--- a/actionpack/test/controller/base_test.rb
+++ b/actionpack/test/controller/base_test.rb
@@ -194,7 +194,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
diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb
index e5f24f1a3a..dc641c19ab 100644
--- a/actionpack/test/controller/flash_test.rb
+++ b/actionpack/test/controller/flash_test.rb
@@ -227,7 +227,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
diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb
index 981b67f685..4c6a772062 100644
--- a/actionpack/test/controller/helper_test.rb
+++ b/actionpack/test/controller/helper_test.rb
@@ -125,13 +125,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)
@@ -168,13 +168,13 @@ 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
@@ -185,26 +185,26 @@ class HelperTest < ActiveSupport::TestCase
@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 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_in_instance
diff --git a/actionpack/test/controller/new_base/render_context_test.rb b/actionpack/test/controller/new_base/render_context_test.rb
index 40149a41d3..b64468a94e 100644
--- a/actionpack/test/controller/new_base/render_context_test.rb
+++ b/actionpack/test/controller/new_base/render_context_test.rb
@@ -28,7 +28,7 @@ module RenderContext
protected
- # 3) Set view_context to self
+ # 3) Set view_context to self
def view_context
self
end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index 399c7489b7..70e5760764 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -746,7 +746,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|
diff --git a/actionpack/test/controller/request/test_request_test.rb b/actionpack/test/controller/request/test_request_test.rb
index 6e0ca957c3..fa049fbc9f 100644
--- a/actionpack/test/controller/request/test_request_test.rb
+++ b/actionpack/test/controller/request/test_request_test.rb
@@ -6,6 +6,11 @@ class ActionController::TestRequestTest < ActionController::TestCase
assert @request.session_options
end
+ def test_mutating_session_options_does_not_affect_default_options
+ @request.session_options[:myparam] = 123
+ assert_equal nil, ActionController::TestSession::DEFAULT_OPTIONS[:myparam]
+ 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],
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
index 29471939d0..b572e7e8d5 100644
--- a/actionpack/test/controller/resources_test.rb
+++ b/actionpack/test/controller/resources_test.rb
@@ -1313,7 +1313,7 @@ class ResourcesTest < ActionController::TestCase
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
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index 696794a3eb..d929885aea 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -646,6 +646,11 @@ XML
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" }
@@ -962,6 +967,13 @@ XML
end
end
+ def test_fixture_file_upload_ignores_fixture_path_given_full_path
+ TestCaseTest.stub :fixture_path, File.dirname(__FILE__) do
+ 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
+ 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
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index 38cf0a2346..6dcd62572a 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -68,7 +68,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
@@ -1115,11 +1115,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
diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb
index 374e618b42..6febd5cb68 100644
--- a/actionpack/test/dispatch/header_test.rb
+++ b/actionpack/test/dispatch/header_test.rb
@@ -76,9 +76,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
diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb
index 35af3076ba..b479af781d 100644
--- a/actionpack/test/dispatch/test_request_test.rb
+++ b/actionpack/test/dispatch/test_request_test.rb
@@ -88,6 +88,13 @@ class TestRequestTest < ActiveSupport::TestCase
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"
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 8bd4e1e56c..e93745c3bf 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,21 @@
+* Render now accepts any keys for locals, including reserved words
+
+ Only locals with valid variable names get set directly. Others
+ will still be available in local_assigns.
+
+ Example of render with reserved words:
+
+ ```erb
+ <%= render "example", class: "text-center", message: "Hello world!" %>
+
+ <!-- _example.html.erb: -->
+ <%= tag.div class: local_assigns[:class] do %>
+ <p><%= message %></p>
+ <% end %>
+ ```
+
+ *Peter Schilling*, *Matthew Draper*
+
* Show cache hits and misses when rendering partials.
Partials using the `cache` helper will show whether a render hit or missed
diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb
index 0967245855..e0de2ff4d6 100644
--- a/actionview/lib/action_view/helpers/asset_url_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_url_helper.rb
@@ -37,7 +37,7 @@ 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")
diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb
index 61ce1d36e0..04c5fd4218 100644
--- a/actionview/lib/action_view/helpers/date_helper.rb
+++ b/actionview/lib/action_view/helpers/date_helper.rb
@@ -303,7 +303,7 @@ 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.
+ # # 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.
diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
index 70d7c484eb..36575b2fd0 100644
--- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
@@ -41,8 +41,8 @@ module ActionView
sanitize_attribute_name(value), text, value, html_options)
end
- # Generate default options for collection helpers, such as :checked and
- # :disabled.
+ # 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
diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb
index 8cc34e3180..667c7e945a 100644
--- a/actionview/lib/action_view/helpers/tags/select.rb
+++ b/actionview/lib/action_view/helpers/tags/select.rb
@@ -28,10 +28,10 @@ module ActionView
private
- # Grouped choices look like this:
- #
- # [nil, []]
- # { nil => [] }
+ # Grouped choices look like this:
+ #
+ # [nil, []]
+ # { nil => [] }
def grouped_choices?
!@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last
end
diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb
index dd5f6e7300..344893f41a 100644
--- a/actionview/lib/action_view/layouts.rb
+++ b/actionview/lib/action_view/layouts.rb
@@ -224,12 +224,12 @@ 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.
+ # 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
@@ -334,11 +334,11 @@ 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
+ # 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
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index 4b6aecd187..dfe38c488f 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -350,13 +350,13 @@ module ActionView
end
end
- # Sets up instance variables needed for rendering a partial. This method
- # finds the options and details and extracts them. The method also contains
- # logic that handles the type of object passed in as the partial.
- #
- # If +options[:partial]+ is a string, then the +@path+ instance variable is
- # set to that string. Otherwise, the +options[:partial]+ object must
- # respond to +to_partial_path+ in order to setup the path.
+ # 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
@@ -466,13 +466,13 @@ module ActionView
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.
+ # 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)
diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
index 0323ebef6a..2434250b2d 100644
--- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb
+++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
@@ -27,8 +27,8 @@ 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.
+ # 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
diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb
index 331a5ea228..4bcf009e27 100644
--- a/actionview/lib/action_view/renderer/template_renderer.rb
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -16,7 +16,7 @@ module ActionView
private
- # Determine the template to be rendered using the given options.
+ # Determine the template to be rendered using the given options.
def determine_template(options)
keys = options.has_key?(:locals) ? options[:locals].keys : []
@@ -44,8 +44,8 @@ module ActionView
end
end
- # Renders the given template. A string representing the layout can be
- # supplied as well.
+ # 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 || {}
@@ -69,9 +69,9 @@ module ActionView
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.
+ # 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
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 513935cef0..c01dd1c028 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -1,5 +1,6 @@
require "active_support/core_ext/object/try"
require "active_support/core_ext/kernel/singleton_class"
+require "active_support/core_ext/module/delegation"
require "thread"
module ActionView
@@ -324,8 +325,13 @@ module ActionView
end
def locals_code #:nodoc:
+ # Only locals with valid variable names get set directly. Others will
+ # still be available in local_assigns.
+ locals = @locals.to_set - Module::DELEGATION_RESERVED_METHOD_NAMES
+ locals = locals.grep(/\A(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
+
# Double assign to suppress the dreaded 'assigned but unused variable' warning
- @locals.each_with_object("") { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" }
+ locals.each_with_object("") { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" }
end
def method_name #:nodoc:
diff --git a/actionview/lib/action_view/template/html.rb b/actionview/lib/action_view/template/html.rb
index ddbd2ea36a..0ffae10432 100644
--- a/actionview/lib/action_view/template/html.rb
+++ b/actionview/lib/action_view/template/html.rb
@@ -14,9 +14,7 @@ module ActionView #:nodoc:
"html template"
end
- def inspect
- "html template"
- end
+ alias_method :inspect, :identifier
def to_str
ERB::Util.h(@string)
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index 20c2d5c782..5a2948d5a9 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -256,7 +256,7 @@ module ActionView
filename.start_with?(path)
end
- # Helper for building query glob string based on resolver's pattern.
+ # Helper for building query glob string based on resolver's pattern.
def build_query(path, details)
query = @pattern.dup
@@ -281,14 +281,14 @@ module ActionView
entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze)
end
- # Returns the file mtime from the filesystem.
+ # Returns the file mtime from the filesystem.
def mtime(p)
File.mtime(p)
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.
+ # 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
diff --git a/actionview/lib/action_view/template/text.rb b/actionview/lib/action_view/template/text.rb
index 898593e702..e8d4e18f04 100644
--- a/actionview/lib/action_view/template/text.rb
+++ b/actionview/lib/action_view/template/text.rb
@@ -14,9 +14,7 @@ module ActionView #:nodoc:
"text template"
end
- def inspect
- "text template"
- end
+ alias_method :inspect, :identifier
def to_str
@string
diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb
index 2ed62ca88f..b5cde5b43f 100644
--- a/actionview/lib/action_view/view_paths.rb
+++ b/actionview/lib/action_view/view_paths.rb
@@ -22,8 +22,8 @@ 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>.
+ # 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
diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb
index 3bb8d21e86..88c7189d22 100644
--- a/actionview/test/abstract_unit.rb
+++ b/actionview/test/abstract_unit.rb
@@ -264,7 +264,7 @@ module ActionDispatch
class DebugExceptions
private
remove_method :stderr_logger
- # Silence logger
+ # Silence logger
def stderr_logger
nil
end
diff --git a/actionview/test/actionpack/abstract/abstract_controller_test.rb b/actionview/test/actionpack/abstract/abstract_controller_test.rb
index 863efd15fa..a2cd3deb58 100644
--- a/actionview/test/actionpack/abstract/abstract_controller_test.rb
+++ b/actionview/test/actionpack/abstract/abstract_controller_test.rb
@@ -227,7 +227,7 @@ module AbstractController
end
class ActionMissingRespondToActionController < AbstractController::Base
- # No actions
+ # No actions
private
def action_missing(action_name)
self.response_body = "success"
diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb
index 952ed72aa9..8495949975 100644
--- a/actionview/test/activerecord/polymorphic_routes_test.rb
+++ b/actionview/test/activerecord/polymorphic_routes_test.rb
@@ -598,7 +598,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
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_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/template/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb
index 7e3e5883b4..3ecac46d34 100644
--- a/actionview/test/template/compiled_templates_test.rb
+++ b/actionview/test/template/compiled_templates_test.rb
@@ -9,6 +9,25 @@ class CompiledTemplatesTest < ActiveSupport::TestCase
assert_equal "This is nil: \n", render(template: "test/nil_return")
end
+ def test_template_with_ruby_keyword_locals
+ assert_equal "The class is foo",
+ render(file: "test/render_file_with_ruby_keyword_locals", locals: { class: "foo" })
+ end
+
+ def test_template_with_invalid_identifier_locals
+ locals = {
+ foo: "bar",
+ Foo: "bar",
+ "d-a-s-h-e-s": "",
+ "white space": "",
+ }
+ assert_equal locals.inspect, render(file: "test/render_file_inspect_local_assigns", locals: locals)
+ end
+
+ def test_template_with_unicode_identifier
+ assert_equal "🎂", render(file: "test/render_file_unicode_local", locals: { 🎃: "🎂" })
+ end
+
def test_template_gets_recompiled_when_using_different_keys_in_local_assigns
assert_equal "one", render(file: "test/render_file_with_locals_and_default")
assert_equal "two", render(file: "test/render_file_with_locals_and_default", locals: { secret: "two" })
diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb
index e82905175a..395c4e4f41 100644
--- a/actionview/test/template/form_helper_test.rb
+++ b/actionview/test/template/form_helper_test.rb
@@ -1808,7 +1808,7 @@ class FormHelperTest < ActionView::TestCase
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.text_field(:title)
@@ -3120,7 +3120,7 @@ class FormHelperTest < ActionView::TestCase
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
diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb
index 40d8d6f3b8..b47d92df34 100644
--- a/actionview/test/template/lookup_context_test.rb
+++ b/actionview/test/template/lookup_context_test.rb
@@ -120,8 +120,8 @@ class LookupContextTest < ActiveSupport::TestCase
@lookup_context.with_fallbacks do
assert_equal 3, @lookup_context.view_paths.size
- assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new(""))
- assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new("/"))
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("")
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/")
end
end
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index 7c8a6aae47..e7e0b147c7 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -460,7 +460,7 @@ module RenderTestCases
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
diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb
index 597b2aa8dd..3f51636603 100644
--- a/actionview/test/template/test_case_test.rb
+++ b/actionview/test/template/test_case_test.rb
@@ -25,7 +25,7 @@ module ActionView
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_includes test_case.ancestors, ASharedTestHelper
assert_equal "Holla!", from_shared_helper
end
end
@@ -64,7 +64,7 @@ module ActionView
helper AnotherTestHelper
test "additional helper classes can be specified as in a controller" do
- assert test_case.ancestors.include?(AnotherTestHelper)
+ assert_includes test_case.ancestors, AnotherTestHelper
assert_equal "Howdy!", from_another_helper
end
@@ -96,12 +96,12 @@ 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_includes test_case.ancestors, AnotherTestHelper
assert_equal "Howdy!", from_another_helper
test_case.helper_class.module_eval do
@@ -161,7 +161,7 @@ module ActionView
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
@@ -200,7 +200,7 @@ 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
@@ -209,7 +209,7 @@ module ActionView
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
diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb
index 86cc880b14..c8eedb6156 100644
--- a/activejob/lib/active_job/queue_adapters.rb
+++ b/activejob/lib/active_job/queue_adapters.rb
@@ -8,7 +8,7 @@ module ActiveJob
# * {Qu}[https://github.com/bkeepers/qu]
# * {Que}[https://github.com/chanks/que]
# * {queue_classic}[https://github.com/QueueClassic/queue_classic]
- # * {Resque 1.x}[https://github.com/resque/resque/tree/1-x-stable]
+ # * {Resque}[https://github.com/resque/resque]
# * {Sidekiq}[http://sidekiq.org]
# * {Sneakers}[https://github.com/jondot/sneakers]
# * {Sucker Punch}[https://github.com/brandonhilkert/sucker_punch]
diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb
index 8bcad6db0f..12687c70d3 100644
--- a/activemodel/lib/active_model/conversion.rb
+++ b/activemodel/lib/active_model/conversion.rb
@@ -46,9 +46,13 @@ module ActiveModel
# 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,12 +65,17 @@ 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
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index f87973301b..6e0af99ad7 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -26,8 +26,8 @@ module ActiveModel
#
# define_attribute_methods :name
#
- # def initialize(name)
- # @name = name
+ # def initialize
+ # @name = nil
# end
#
# def name
@@ -58,7 +58,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 +66,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 +80,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 # => {}
#
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 191039f598..72746e194e 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -280,7 +280,7 @@ module ActiveModel
messages[attribute] = array.map { |message| full_message(attribute, message) }
end
else
- messages.dup
+ without_default_proc(messages)
end
end
diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb
index f88c9e5071..5cb0077e45 100644
--- a/activemodel/lib/active_model/type/date_time.rb
+++ b/activemodel/lib/active_model/type/date_time.rb
@@ -19,8 +19,8 @@ module ActiveModel
fast_string_to_time(value) || fallback_string_to_time(value)
end
- # '0.123456' -> 123456
- # '1.123456' -> 123456
+ # '0.123456' -> 123456
+ # '1.123456' -> 123456
def microseconds(time)
time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
end
diff --git a/activemodel/lib/active_model/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb
index 64780ebbdc..d8c5d929a3 100644
--- a/activemodel/lib/active_model/type/helpers/time_value.rb
+++ b/activemodel/lib/active_model/type/helpers/time_value.rb
@@ -64,7 +64,7 @@ module ActiveModel
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
- # Doesn't handle time zones.
+ # Doesn't handle time zones.
def fast_string_to_time(string)
if string =~ ISO_DATETIME
microsec = ($7.to_r * 1_000_000).to_i
diff --git a/activemodel/lib/active_model/type/value.rb b/activemodel/lib/active_model/type/value.rb
index 4d3a1b29d6..7e9ae92245 100644
--- a/activemodel/lib/active_model/type/value.rb
+++ b/activemodel/lib/active_model/type/value.rb
@@ -105,9 +105,9 @@ 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+.
+ # 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
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index da99a0eca0..95ca1f3969 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -37,7 +37,7 @@ class ErrorsTest < ActiveModel::TestCase
def test_include?
errors = ActiveModel::Errors.new(self)
errors[:foo] << "omg"
- assert errors.include?(:foo), "errors should include :foo"
+ assert_includes errors, :foo, "errors should include :foo"
end
def test_dup
@@ -125,7 +125,7 @@ class ErrorsTest < ActiveModel::TestCase
person.errors[:foo]
assert person.errors.empty?
assert person.errors.blank?
- assert !person.errors.include?(:foo)
+ assert_not_includes person.errors, :foo
end
test "include? does not add a key to messages hash" do
@@ -250,6 +250,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")
diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb
index 45c166f33a..d15ba64eb0 100644
--- a/activemodel/test/cases/serializers/json_serialization_test.rb
+++ b/activemodel/test/cases/serializers/json_serialization_test.rb
@@ -18,7 +18,7 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_no_match %r{^\{"contact":\{}, json
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
- assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
assert_match %r{"awesome":true}, json
assert_match %r{"preferences":\{"shows":"anime"\}}, json
end
@@ -32,7 +32,7 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_match %r{^\{"contact":\{}, json
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
- assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})
assert_match %r{"awesome":true}, json
assert_match %r{"preferences":\{"shows":"anime"\}}, json
ensure
@@ -58,7 +58,7 @@ class JsonSerializationTest < ActiveModel::TestCase
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
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
index 85212a80fc..f28cfc0ef5 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -46,7 +46,7 @@ class I18nValidationTest < ActiveModel::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 } }, {}],
diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb
index e5690da89a..25c37a572f 100644
--- a/activemodel/test/cases/validations/validations_context_test.rb
+++ b/activemodel/test/cases/validations/validations_context_test.rb
@@ -38,7 +38,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 +48,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 +62,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 7af51c5cc5..20c11dd852 100644
--- a/activemodel/test/cases/validations/with_validation_test.rb
+++ b/activemodel/test/cases/validations/with_validation_test.rb
@@ -51,7 +51,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,8 +64,8 @@ class ValidatesWithTest < ActiveModel::TestCase
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)
+ assert_includes topic.errors[:base], ERROR_MESSAGE
+ assert_includes topic.errors[:base], OTHER_ERROR_MESSAGE
end
test "with if statements that return false" do
@@ -78,7 +78,7 @@ class ValidatesWithTest < ActiveModel::TestCase
Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 1")
topic = Topic.new
assert topic.invalid?
- assert topic.errors[:base].include?(ERROR_MESSAGE)
+ assert_includes topic.errors[:base], ERROR_MESSAGE
end
test "with unless statements that return true" do
@@ -91,7 +91,7 @@ class ValidatesWithTest < ActiveModel::TestCase
Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 2")
topic = Topic.new
assert topic.invalid?
- assert topic.errors[:base].include?(ERROR_MESSAGE)
+ assert_includes topic.errors[:base], ERROR_MESSAGE
end
test "passes all configuration options to the validator class" do
@@ -111,7 +111,7 @@ class ValidatesWithTest < ActiveModel::TestCase
Topic.validates_with(ValidatorThatValidatesOptions, field: :first_name)
topic = Topic.new
assert topic.invalid?
- assert topic.errors[:base].include?(ERROR_MESSAGE)
+ assert_includes topic.errors[:base], ERROR_MESSAGE
end
test "validates_with each validator" do
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb
index 7aaafc428e..6647191205 100644
--- a/activemodel/test/cases/validations_test.rb
+++ b/activemodel/test/cases/validations_test.rb
@@ -53,8 +53,8 @@ class ValidationsTest < ActiveModel::TestCase
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 +86,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,8 +101,8 @@ 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
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 3eac34d65e..4d0c1a4178 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,41 @@
+* Serialize JSON attribute value `nil` as SQL `NULL`, not JSON `null`
+
+ *Trung Duc Tran*
+
+* Return `true` from `update_attribute` when the value of the attribute
+ to be updated is unchanged.
+
+ Fixes #26593.
+
+ *Prathamesh Sonpatki*
+
+* Always store errors details information with symbols.
+
+ When the association is autosaved we were storing the details with
+ string keys. This was creating inconsistency with other details that are
+ added using the `Errors#add` method. It was also inconsistent with the
+ `Errors#messages` storage.
+
+ To fix this inconsistency we are always storing with symbols. This will
+ cause a small breaking change because in those cases the details could
+ be accessed as strings keys but now it can not.
+
+ Fix #26499.
+
+ *Rafael Mendonça França*, *Marcus Vieira*
+
+* Calling `touch` on a model using optimistic locking will now leave the model
+ in a non-dirty state with no attribute changes.
+
+ Fixes #26496.
+
+ *Jakob Skjerning*
+
+* Using a mysql2 connection after it fails to reconnect will now have an error message
+ saying the connection is closed rather than an undefined method error message.
+
+ *Dylan Thacker-Smith*
+
* PostgreSQL array columns will now respect the encoding of strings contained
in the array.
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 8979b13286..5ca8fe576e 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -24,161 +24,161 @@ module ActiveRecord
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"))
- #
+ # 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.
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index dc6fe1640e..b5f1f1980a 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -274,882 +274,882 @@ 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}[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(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>, then the module is
- # named <tt>Project::GeneratedAssociationMethods</tt>. The +GeneratedAssociationMethods+ module is
- # included in the model class immediately after the (anonymous) generated attributes methods
- # module, meaning an association will override the methods for an attribute with the same name.
- #
- # == 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 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
- #
- # There are limitations to <tt>:inverse_of</tt> support:
- #
- # * does not work with <tt>:through</tt> associations.
- # * does not work with <tt>:polymorphic</tt> associations.
- # * inverse associations for #belongs_to associations #has_many are ignored.
- #
- # For more information, see the documentation for the +:inverse_of+ option.
- #
- # == Deleting from associations
- #
- # === 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.
+ # \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}[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(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>, then the module is
+ # named <tt>Project::GeneratedAssociationMethods</tt>. The +GeneratedAssociationMethods+ module is
+ # included in the model class immediately after the (anonymous) generated attributes methods
+ # module, meaning an association will override the methods for an attribute with the same name.
+ #
+ # == 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 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
+ #
+ # There are limitations to <tt>:inverse_of</tt> support:
+ #
+ # * does not work with <tt>:through</tt> associations.
+ # * does not work with <tt>:polymorphic</tt> associations.
+ # * inverse associations for #belongs_to associations #has_many are ignored.
+ #
+ # For more information, see the documentation for the +:inverse_of+ option.
+ #
+ # == Deleting from associations
+ #
+ # === 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:
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 08bd532fb0..278c95e27b 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -259,7 +259,6 @@ module ActiveRecord
seen[record.id] = true unless seen.key?(record.id)
end
end
- alias uniq distinct
# Replace this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
@@ -310,15 +309,12 @@ module ActiveRecord
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
+ if index
+ @target[index] = record
+ else
+ append_record(record)
end
callback(:after_add, record) unless skip_callbacks
@@ -515,6 +511,10 @@ module ActiveRecord
load_target.select { |r| ids.include?(r.id.to_s) }
end
end
+
+ def append_record(record)
+ @target << record unless @target.include?(record)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index d258eac0ed..1f264d325a 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -203,6 +203,10 @@ module ActiveRecord
def invertible_for?(record)
false
end
+
+ def append_record(record)
+ @target << record
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index a81860e40f..9f77f38b35 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -106,7 +106,7 @@ module ActiveRecord
private
- # Loads all the given data into +records+ for the +association+.
+ # Loads all the given data into +records+ for the +association+.
def preloaders_on(association, records, scope)
case association
when Hash
@@ -132,18 +132,18 @@ module ActiveRecord
}
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.
+ # 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|
@@ -187,10 +187,10 @@ module ActiveRecord
def self.owners; []; end
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.
+ # 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
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index 483a9df740..e386cc0e4c 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -48,9 +48,9 @@ module ActiveRecord
end
binds = AssociationScope.get_bind_values(owner, reflection.chain)
- if record = sc.execute(binds, klass, conn).first
+ sc.execute(binds, klass, conn) do |record|
set_inverse_instance record
- end
+ end.first
end
def replace(record)
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index f3ce52fdfe..9843e0ca66 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -29,17 +29,17 @@ module ActiveRecord
assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
end
- # Assign any deferred nested attributes after the base attributes have been set.
+ # 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 Integer and
- # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
+ # 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)
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index ba26a11b39..1ed1deec55 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -416,8 +416,8 @@ module ActiveRecord
private
- # Returns a Hash of the Arel::Attributes and attribute values that have been
- # typecasted for use in an Arel insert/update method.
+ # 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
@@ -428,15 +428,15 @@ module ActiveRecord
attrs
end
- # Filters the primary keys and readonly attributes from the attribute names.
+ # 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)
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).
+ # 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?
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 92f124078c..115eb1ef3f 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -63,7 +63,7 @@ module ActiveRecord
private
- # Handle *_before_type_cast for method_missing.
+ # Handle *_before_type_cast for method_missing.
def attribute_before_type_cast(attribute_name)
read_attribute_before_type_cast(attribute_name)
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 131ed8740b..30f7750884 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -6,24 +6,24 @@ module ActiveRecord
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.
+ # 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}"
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index c70178cd2c..945192fe04 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -26,7 +26,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
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index e9d044ef13..f65c297e01 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -37,7 +37,7 @@ module ActiveRecord
end
private
- # Handle *= for method_missing.
+ # Handle *= for method_missing.
def attribute=(attribute_name, value)
write_attribute(attribute_name, value)
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index db84876b0a..d3e0dee731 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -329,26 +329,20 @@ module ActiveRecord
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)
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,6 +356,14 @@ 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
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 d0c5bbe17d..2d62fd8d50 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -158,33 +158,33 @@ module ActiveRecord
@lock.synchronize(&block)
end
- # Test if the queue currently contains any elements.
+ # 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.
+ # 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.
+ # Removes and returns the head of the queue if possible, or nil.
def remove
@queue.shift
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.
+ # 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.
+ # Waits on the queue up to +timeout+ seconds, then removes and
+ # returns the head of the queue.
def wait_poll(timeout)
@num_waiting += 1
@@ -582,8 +582,8 @@ module ActiveRecord
end
private
- #--
- # this is unfortunately not concurrent
+ #--
+ # 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
@@ -594,19 +594,19 @@ module ActiveRecord
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.
+ #--
+ # 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.
+ # 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)
@@ -658,8 +658,8 @@ module ActiveRecord
end
end
- #--
- # Must be called in a synchronize block.
+ #--
+ # Must be called in a synchronize block.
def checkout_for_exclusive_access(checkout_timeout)
checkout(checkout_timeout)
rescue ConnectionTimeoutError
@@ -690,17 +690,17 @@ module ActiveRecord
synchronize { @new_cons_enabled = previous_value }
end
- # 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.
+ # 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+
@@ -716,8 +716,8 @@ module ActiveRecord
end
end
- #--
- # if owner_thread param is omitted, this must be called in synchronize block
+ #--
+ # 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
@@ -729,11 +729,11 @@ module ActiveRecord
end
end
- # 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.
+ # 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
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 def1dadb6a..aa2dfdd573 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -245,7 +245,7 @@ module ActiveRecord
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
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 10c60080d5..2f8a89e88e 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -65,7 +65,7 @@ module ActiveRecord
if @query_cache_enabled && !locked?(arel)
arel, binds = binds_from_relation arel, binds
sql = to_sql(arel, binds)
- cache_sql(sql, binds) { super(sql, name, binds, preparable: preparable) }
+ cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable) }
else
super
end
@@ -73,11 +73,17 @@ module ActiveRecord
private
- def cache_sql(sql, binds)
+ def cache_sql(sql, name, binds)
result =
if @query_cache[sql].key?(binds)
- ActiveSupport::Notifications.instrument("sql.active_record",
- sql: sql, binds: binds, name: "CACHE", connection_id: object_id)
+ ActiveSupport::Notifications.instrument(
+ "sql.active_record",
+ sql: sql,
+ binds: binds,
+ name: name,
+ connection_id: object_id,
+ cached: true,
+ )
@query_cache[sql][binds]
else
@query_cache[sql][binds] = yield
@@ -85,8 +91,8 @@ module ActiveRecord
result.dup
end
- # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such
- # queries should not be cached.
+ # 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
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 0bd5ec4b26..29520ed9c8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -511,7 +511,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
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 d0ea1ce0cf..be8511f119 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -818,8 +818,8 @@ module ActiveRecord
private
- # MySQL is too stupid to create a temporary table for use subquery, so we have
- # to give it some prompting in the form of a subsubquery. Ugh!
+ # 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]
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index be6b55e53c..849130ba43 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -63,15 +63,15 @@ module ActiveRecord
@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"
- # # => {}
+ # 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
@@ -92,7 +92,7 @@ module ActiveRecord
end
end
- # Returns name of the database.
+ # Returns name of the database.
def database_from_path
if @adapter == "sqlite3"
# 'sqlite3:/foo' is absolute, because that makes sense. The
@@ -192,26 +192,26 @@ module ActiveRecord
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" }
- #
+ # 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
@@ -223,13 +223,13 @@ module ActiveRecord
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)
- # # => {}
- #
+ # 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).merge("name" => spec.to_s)
@@ -238,10 +238,10 @@ module ActiveRecord
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
+ # 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"))
@@ -250,11 +250,11 @@ module ActiveRecord
spec
end
- # Takes a connection URL.
- #
- # Resolver.new({}).resolve_url_connection("postgresql://localhost/foo")
- # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
- #
+ # 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
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
index c8238eb266..56800f7590 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -24,11 +24,9 @@ module ActiveRecord
# 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
+ # 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
@@ -71,11 +69,9 @@ module ActiveRecord
end
def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
- 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
+ # 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)
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 0130b4ef62..a3e2c913c5 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -90,7 +90,6 @@ module ActiveRecord
#++
def active?
- return false unless @connection
@connection.ping
end
@@ -105,10 +104,7 @@ module ActiveRecord
# Otherwise, this method does nothing.
def disconnect!
super
- unless @connection.nil?
- @connection.close
- @connection = nil
- end
+ @connection.close
end
private
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 7414eba6c5..092543259f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -89,10 +89,10 @@ module ActiveRecord
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.
+ # 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)
log(sql, name) do
@connection.async_exec(sql)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 03ee0eec5b..8001c0dd53 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -527,7 +527,7 @@ module ActiveRecord
case default
# Quoted types
when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m
- # The default 'now'::date is CURRENT_DATE
+ # The default 'now'::date is CURRENT_DATE
if $1 == "now".freeze && $2 == "date".freeze
nil
else
@@ -542,9 +542,9 @@ module ActiveRecord
# 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.
+ 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
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 3465b68ac6..622df0cfc1 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -452,7 +452,7 @@ module ActiveRecord
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
def hash
if id
- [self.class, id].hash
+ self.class.hash ^ self.id.hash
else
super
end
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index bbd8ca2377..9a7a8d25bb 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -75,14 +75,14 @@ module ActiveRecord
"#{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 `_`.
+ # 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.
+ # 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
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index 706b57842f..abd8cfc8f2 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -18,10 +18,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/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 40a9aa2783..8b47fbdbe4 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -902,7 +902,7 @@ module ActiveRecord
def fixtures(*fixture_set_names)
if fixture_set_names.first == :all
- fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"]
+ 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)
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 4adcd7e65c..a1d4f47372 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -132,8 +132,8 @@ module ActiveRecord
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.
+ # 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
@@ -156,9 +156,9 @@ module ActiveRecord
private
- # 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.
+ # 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])
@@ -199,8 +199,8 @@ module ActiveRecord
sti_column.in(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
+ # 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)
@@ -225,11 +225,11 @@ module ActiveRecord
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.
+ # 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?
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 1b6cda3861..8e8a97990a 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -168,10 +168,10 @@ module ActiveRecord
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.
+ # 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 }
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index f31931316c..b27e84a5be 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -35,6 +35,7 @@ module ActiveRecord
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
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 063366bc60..627e93b5b6 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,4 +1,5 @@
require "set"
+require "zlib"
require "active_support/core_ext/module/attribute_accessors"
require "active_support/core_ext/regexp"
@@ -1163,7 +1164,7 @@ module ActiveRecord
private
- # Used for running a specific migration.
+ # 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?
@@ -1172,7 +1173,7 @@ module ActiveRecord
record_environment
end
- # Used for running multiple migrations up to or down to a certain value.
+ # 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)
@@ -1185,7 +1186,7 @@ module ActiveRecord
record_environment
end
- # Stores the current environment in the database.
+ # Stores the current environment in the database.
def record_environment
return if down?
ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
@@ -1195,7 +1196,7 @@ module ActiveRecord
migrated.include?(migration.version.to_i)
end
- # Return true if a valid version is not provided.
+ # Return true if a valid version is not provided.
def invalid_target?
!target && @target_version && @target_version > 0
end
@@ -1272,7 +1273,7 @@ module ActiveRecord
@direction == :down
end
- # Wrap the migration in a transaction only if supported by the adapter.
+ # Wrap the migration in a transaction only if supported by the adapter.
def ddl_transaction(migration)
if use_transaction?(migration)
Base.transaction { yield }
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 44ea756028..03103bba98 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -225,7 +225,7 @@ module ActiveRecord
[:add_foreign_key, reversed_args]
end
- # Forwards any missing method call to the \target.
+ # Forwards any missing method call to the \target.
def method_missing(method, *args, &block)
if @delegate.respond_to?(method)
@delegate.send(method, *args, &block)
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index f49f8da2ed..76b3169411 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -397,13 +397,13 @@ module ActiveRecord
end
end
- # Guesses the table name, but does not decorate it with prefix and suffix information.
+ # 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
- # Computes and returns a table name according to default conventions.
+ # Computes and returns a table name according to default conventions.
def compute_table_name
base = base_class
if self == base
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index f0f88b120a..e983026961 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -341,17 +341,17 @@ 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.
+ # 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=)
@@ -375,23 +375,23 @@ module ActiveRecord
private
- # Attribute hash keys that should not be assigned as normal attributes.
- # These hash keys are nested attributes implementation details.
+ # 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.
+ # 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?)
@@ -424,33 +424,33 @@ module ActiveRecord
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 }
- # ])
+ # 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?)
@@ -511,12 +511,12 @@ module ActiveRecord
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.
+ # 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 = \
@@ -535,30 +535,30 @@ module ActiveRecord
end
end
- # Updates a record with the +attributes+ or marks it for destruction if
- # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
+ # 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.
+ # 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+.
+ # 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.
+ # 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)
@@ -570,7 +570,7 @@ module ActiveRecord
end
end
- # Only take into account the destroy flag if <tt>:allow_destroy</tt> is true
+ # 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
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index a04ef2e263..6933f3f9b8 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -107,7 +107,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
@@ -134,7 +134,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
@@ -252,7 +252,8 @@ module ActiveRecord
name = name.to_s
verify_readonly_attribute(name)
public_send("#{name}=", value)
- save(validate: false) if changed?
+
+ changed? ? save(validate: false) : true
end
# Updates the attributes of the model from the passed-in hash and saves the
@@ -498,7 +499,6 @@ module ActiveRecord
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))
@@ -508,6 +508,7 @@ module ActiveRecord
changes[locking_column] = increment_lock
end
+ clear_attribute_changes(changes.keys)
result = scope.update_all(changes) == 1
if !result && locking_enabled?
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 6d571cf026..ef629dcb3b 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -243,7 +243,6 @@ module ActiveRecord
# Please see further details in the
# {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain].
def explain
- #TODO: Fix for binds.
exec_explain(collecting_queries_for_explain { exec_queries })
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index a796e35261..a887be8a20 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -252,11 +252,11 @@ module ActiveRecord
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 = result.column_types.fetch(column_alias) do
type_for(column_name)
end
- type_cast_calculated_value(value, column, operation)
+ type_cast_calculated_value(value, type, operation)
end
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
@@ -310,28 +310,26 @@ module ActiveRecord
Hash[calculated_data.map do |row|
key = group_columns.map { |aliaz, col_name|
- column = type_for(col_name) do
- calculated_data.column_types.fetch(aliaz) do
- Type.default_value
- end
+ type = type_for(col_name) do
+ calculated_data.column_types.fetch(aliaz, Type.default_value)
end
- type_cast_calculated_value(row[aliaz], column)
+ type_cast_calculated_value(row[aliaz], type)
}
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)]
+ type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
+ [key, type_cast_calculated_value(row[aggregate_alias], type, operation)]
end]
end
- # 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"
+ # 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}"
@@ -356,7 +354,7 @@ module ActiveRecord
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)
+ else type.deserialize(value)
end
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 78570140e5..9fbbe32e7f 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -1150,22 +1150,22 @@ module ActiveRecord
end.flatten!
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
+ # 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."
diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb
index dbd08811fa..31544c730e 100644
--- a/activerecord/lib/active_record/relation/record_fetch_warning.rb
+++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb
@@ -2,15 +2,15 @@ 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/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
index dc00149130..1e7deeffad 100644
--- a/activerecord/lib/active_record/relation/where_clause_factory.rb
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -7,8 +7,6 @@ module ActiveRecord
end
def build(opts, other)
- binds = []
-
case opts
when String, Array
parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
@@ -26,7 +24,7 @@ module ActiveRecord
raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})"
end
- WhereClause.new(parts, binds)
+ WhereClause.new(parts, binds || [])
end
protected
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 7f596120eb..e7c0936984 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -7,20 +7,20 @@ module ActiveRecord
module ClassMethods
protected
- # Accepts an array or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for a WHERE clause.
- #
- # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
- # # => "name='foo''bar' and group_id=4"
- #
- # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
- # # => "name='foo''bar' and group_id='4'"
- #
- # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
- # # => "name='foo''bar' and group_id='4'"
- #
- # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
- # # => "name='foo''bar' and group_id='4'"
+ # Accepts an array or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for a WHERE clause.
+ #
+ # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
+ # # => "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
+ # # => "name='foo''bar' and group_id='4'"
def sanitize_sql_for_conditions(condition)
return nil if condition.blank?
@@ -33,20 +33,20 @@ module ActiveRecord
alias :sanitize_conditions :sanitize_sql
deprecate sanitize_conditions: :sanitize_sql
- # Accepts an array, hash, or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for a SET clause.
- #
- # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
- # # => "name=NULL and group_id=4"
- #
- # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
- # # => "name=NULL and group_id=4"
- #
- # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 })
- # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
- #
- # sanitize_sql_for_assignment("name=NULL and group_id='4'")
- # # => "name=NULL and group_id='4'"
+ # Accepts an array, hash, or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for a SET clause.
+ #
+ # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
+ # # => "name=NULL and group_id=4"
+ #
+ # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
+ # # => "name=NULL and group_id=4"
+ #
+ # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 })
+ # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
+ #
+ # sanitize_sql_for_assignment("name=NULL and group_id='4'")
+ # # => "name=NULL and group_id='4'"
def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name)
case assignments
when Array; sanitize_sql_array(assignments)
@@ -55,14 +55,14 @@ module ActiveRecord
end
end
- # Accepts an array, or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for an ORDER clause.
- #
- # sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
- # # => "field(id, 1,3,2)"
- #
- # sanitize_sql_for_order("id ASC")
- # # => "id ASC"
+ # Accepts an array, or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for an ORDER clause.
+ #
+ # sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
+ # # => "field(id, 1,3,2)"
+ #
+ # sanitize_sql_for_order("id ASC")
+ # # => "id ASC"
def sanitize_sql_for_order(condition)
if condition.is_a?(Array) && condition.first.to_s.include?("?")
sanitize_sql_array(condition)
@@ -71,21 +71,21 @@ module ActiveRecord
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" }
+ # 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|
@@ -105,10 +105,10 @@ module ActiveRecord
expanded_attrs
end
- # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
- #
- # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
- # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
+ # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
+ #
+ # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
+ # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
def sanitize_sql_hash_for_assignment(attrs, table)
c = connection
attrs.map do |attr, value|
@@ -117,36 +117,36 @@ module ActiveRecord
end.join(", ")
end
- # Sanitizes a +string+ so that it is safe to use within an SQL
- # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
- #
- # sanitize_sql_like("100%")
- # # => "100\\%"
- #
- # sanitize_sql_like("snake_cased_string")
- # # => "snake\\_cased\\_string"
- #
- # sanitize_sql_like("100%", "!")
- # # => "100!%"
- #
- # sanitize_sql_like("snake_cased_string", "!")
- # # => "snake!_cased!_string"
+ # Sanitizes a +string+ so that it is safe to use within an SQL
+ # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
+ #
+ # sanitize_sql_like("100%")
+ # # => "100\\%"
+ #
+ # sanitize_sql_like("snake_cased_string")
+ # # => "snake\\_cased\\_string"
+ #
+ # sanitize_sql_like("100%", "!")
+ # # => "100!%"
+ #
+ # sanitize_sql_like("snake_cased_string", "!")
+ # # => "snake!_cased!_string"
def sanitize_sql_like(string, escape_character = "\\")
pattern = Regexp.union(escape_character, "%", "_")
string.gsub(pattern) { |x| [escape_character, x].join }
end
- # Accepts an array of conditions. The array has each value
- # sanitized and interpolated into the SQL statement.
- #
- # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
- # # => "name='foo''bar' and group_id=4"
- #
- # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
- # # => "name='foo''bar' and group_id=4"
- #
- # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
- # # => "name='foo''bar' and group_id='4'"
+ # Accepts an array of conditions. The array has each value
+ # sanitized and interpolated into the SQL statement.
+ #
+ # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
def sanitize_sql_array(ary)
statement, *values = ary
if values.first.is_a?(Hash) && /:\w+/.match?(statement)
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 7409706851..9d8253faa3 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -46,47 +46,47 @@ module ActiveRecord
protected
- # 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
+ # 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?
@@ -130,9 +130,9 @@ module ActiveRecord
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...
+ # 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?
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index d19bb96ede..691940ab70 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -7,12 +7,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)
#
# 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
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index b19ae5c46e..af3fc88282 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -409,7 +409,7 @@ module ActiveRecord
protected
- # Save the new record state and id of a record so it can be restored later if a transaction fails.
+ # 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!(
@@ -420,18 +420,18 @@ module ActiveRecord
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
end
- # Clear the new record state and id of a record.
+ # 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
- # Force to clear the transaction record state.
+ # Force to clear the transaction record state.
def force_clear_transaction_record_state #:nodoc:
@_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.
+ # 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
@@ -449,12 +449,12 @@ module ActiveRecord
end
end
- # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
+ # 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.
+ # 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
@@ -478,23 +478,23 @@ module ActiveRecord
!_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.
+ # 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
diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb
index 513c938088..e19c5a14da 100644
--- a/activerecord/lib/active_record/type/internal/abstract_json.rb
+++ b/activerecord/lib/active_record/type/internal/abstract_json.rb
@@ -17,7 +17,11 @@ module ActiveRecord
end
def serialize(value)
- ::ActiveSupport::JSON.encode(value)
+ if value.nil?
+ nil
+ else
+ ::ActiveSupport::JSON.encode(value)
+ end
end
def accessor
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 bc9037c476..76ed25ea75 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -17,9 +17,9 @@ module ActiveRecord
protected
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.
+ # 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
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index a72981e4d5..8fa0645b0f 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -33,10 +33,10 @@ module ActiveRecord
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")
+ assert_includes tables, "accounts"
+ assert_includes tables, "authors"
+ assert_includes tables, "tasks"
+ assert_includes tables, "topics"
end
def test_table_exists?
@@ -53,10 +53,10 @@ module ActiveRecord
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?
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 9ede57d395..8d8955e5c9 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -63,6 +63,27 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
assert @connection.active?
end
+ def test_execute_after_disconnect
+ @connection.disconnect!
+ error = assert_raise(ActiveRecord::StatementInvalid) do
+ @connection.execute("SELECT 1")
+ end
+ assert_match(/closed MySQL connection/, error.message)
+ end
+
+ def test_quote_after_disconnect
+ @connection.disconnect!
+ error = assert_raise(Mysql2::Error) do
+ @connection.quote("string")
+ end
+ assert_match(/closed MySQL connection/, error.message)
+ end
+
+ def test_active_after_disconnect
+ @connection.disconnect!
+ assert_equal false, @connection.active?
+ 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")
diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb
index 6b7d259023..630cdb36a4 100644
--- a/activerecord/test/cases/adapters/mysql2/json_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/json_test.rb
@@ -102,6 +102,22 @@ if ActiveRecord::Base.connection.supports_json?
assert_equal(["v0", { "k1" => "v1" }], x.payload)
end
+ def test_select_nil_json_after_create
+ json = JsonDataType.create(payload: nil)
+ x = JsonDataType.where(payload:nil).first
+ assert_equal(json, x)
+ end
+
+ def test_select_nil_json_after_update
+ json = JsonDataType.create(payload: "foo")
+ x = JsonDataType.where(payload:nil).first
+ assert_equal(nil, x)
+
+ json.update_attributes payload: nil
+ x = JsonDataType.where(payload:nil).first
+ assert_equal(json.reload, x)
+ end
+
def test_rewrite_array_json_value
@connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|
x = JsonDataType.first
diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
index 7736113a19..776549eb7a 100644
--- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
@@ -130,19 +130,19 @@ class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase
#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
+ # 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
+ # 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
+ # 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} )")
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index 3a8fcf388a..9236a67b11 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -38,7 +38,7 @@ if ActiveRecord::Base.connection.supports_extensions?
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"
+ assert_includes @connection.extensions, "hstore", "extension list should include hstore"
end
def test_disable_enable_hstore
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index c74f70f251..273b2c1c7b 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -113,6 +113,22 @@ module PostgresqlJSONSharedTestCases
assert_equal(nil, x.payload)
end
+ def test_select_nil_json_after_create
+ json = JsonDataType.create(payload: nil)
+ x = JsonDataType.where(payload:nil).first
+ assert_equal(json, x)
+ end
+
+ def test_select_nil_json_after_update
+ json = JsonDataType.create(payload: "foo")
+ x = JsonDataType.where(payload:nil).first
+ assert_equal(nil, x)
+
+ json.update_attributes payload: nil
+ x = JsonDataType.where(payload:nil).first
+ assert_equal(json.reload, x)
+ end
+
def test_select_array_json_value
@connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|
x = JsonDataType.first
diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
index 22f935f24e..7193f23880 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
@@ -100,10 +100,10 @@ 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
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index b0be25de0c..51a2306c59 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -130,7 +130,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
diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
index d992e22305..00119f13bb 100644
--- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
@@ -1,5 +1,6 @@
require "cases/helper"
require "support/connection_helper"
+require "concurrent/atomic/cyclic_barrier"
module ActiveRecord
class PostgresqlTransactionTest < ActiveRecord::PostgreSQLTestCase
@@ -61,26 +62,28 @@ module ActiveRecord
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!
- sleep 1
+ barrier.wait
s2.update_attributes value: 1
end
end
- sleep 0.5
-
- Sample.transaction do
- s2.lock!
- sleep 1
- s1.update_attributes value: 2
+ begin
+ Sample.transaction do
+ s2.lock!
+ barrier.wait
+ s1.update_attributes value: 2
+ end
+ ensure
+ thread.join
end
-
- thread.join
end
end
end
diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb
index c3d560cb56..2f62d0367e 100644
--- a/activerecord/test/cases/associations/callbacks_test.rb
+++ b/activerecord/test/cases/associations/callbacks_test.rb
@@ -176,14 +176,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_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 9ae1558dc9..e87431bf32 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -134,8 +134,8 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
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))
+ assert_includes replies, topics(:second)
+ assert_not_includes replies, topics(:first)
assert_equal topics(:first), assert_no_queries { replies.first.topic }
end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 6011c552b2..d1c4c1cef8 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -42,11 +42,11 @@ class EagerAssociationTest < ActiveRecord::TestCase
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
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
post = posts.find { |p| p.id == 1 }
@@ -103,7 +103,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
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
@@ -349,8 +349,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
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
@@ -630,8 +630,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
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
@@ -695,8 +695,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
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
@@ -895,9 +895,9 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_polymorphic_type_condition
post = Post.all.merge!(includes: :taggings).find(posts(:thinking).id)
- assert post.taggings.include?(taggings(:thinking_general))
+ assert_includes post.taggings, taggings(:thinking_general)
post = SpecialPost.all.merge!(includes: :taggings).find(posts(:thinking).id)
- assert post.taggings.include?(taggings(:thinking_general))
+ assert_includes post.taggings, taggings(:thinking_general)
end
def test_eager_with_multiple_associations_with_same_table_has_many_and_habtm
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 36f541a506..06fc7a4388 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
@@ -169,7 +169,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
active_record = Project.find(1)
assert !active_record.developers.empty?
assert_equal 3, active_record.developers.size
- assert active_record.developers.include?(david)
+ assert_includes active_record.developers, david
end
def test_adding_single
@@ -544,7 +544,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_no_queries(ignore_none: false) do
assert project.developers.loaded?
- assert project.developers.include?(developer)
+ assert_includes project.developers, developer
end
end
@@ -555,7 +555,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
project.reload
assert ! project.developers.loaded?
assert_queries(1) do
- assert project.developers.include?(developer)
+ assert_includes project.developers, developer
end
assert ! project.developers.loaded?
end
@@ -600,8 +600,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
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
@@ -628,7 +628,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 +646,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
@@ -854,7 +854,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
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 1bfb1ea0c8..0ce67f971b 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -870,7 +870,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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
@@ -2461,9 +2461,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
test "double insertion of new object to association when same association used in the after create callback of a new object" do
- car = Car.create!
- car.bulbs << TrickyBulb.new
- assert_equal 1, car.bulbs.size
+ reset_callbacks(:save, Bulb) do
+ Bulb.after_save { |record| record.car.bulbs.to_a }
+ car = Car.create!
+ car.bulbs << Bulb.new
+ assert_equal 1, car.bulbs.size
+ end
end
def test_association_force_reload_with_only_true_is_deprecated
@@ -2510,9 +2513,34 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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 { |m| m.car.bulbs.load }
+
+ car = Car.create!(name: "Car")
+ bulb = car.bulbs.create!
+
+ assert_equal [bulb], car.bulbs
+ end
+ end
+
private
def force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.load_target
end
+
+ def reset_callbacks(kind, klass)
+ old_callbacks = {}
+ old_callbacks[klass] = klass.send("_#{kind}_callbacks").dup
+ klass.subclasses.each do |subclass|
+ old_callbacks[subclass] = subclass.send("_#{kind}_callbacks").dup
+ end
+ yield
+ ensure
+ klass.send("_#{kind}_callbacks=", old_callbacks[klass])
+ klass.subclasses.each do |subclass|
+ subclass.send("_#{kind}_callbacks=", old_callbacks[subclass])
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index 3508793122..9f716d7820 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -175,7 +175,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
@@ -187,10 +187,10 @@ 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
@@ -266,7 +266,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post.people.delete(person)
end
- assert !post.people.reload.include?(person)
+ assert_not_includes post.people.reload, person
end
def test_associating_new
@@ -284,10 +284,10 @@ 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
@@ -300,8 +300,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
# 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)
@@ -312,8 +312,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
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
@@ -322,7 +322,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
person.save
post.reload
- assert post.people.include?(person)
+ assert_includes post.people, person
end
def test_build_then_save_with_has_one_inverse
@@ -331,7 +331,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
person.save
post.reload
- assert post.single_people.include?(person)
+ assert_includes post.single_people, person
end
def test_both_parent_ids_set_when_saving_new
@@ -550,12 +550,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
assert_queries(0) {
- assert posts(:welcome).people.include?(people(:david))
- assert !posts(:welcome).people.include?(people(:michael))
+ assert_includes posts(:welcome).people, people(:david)
+ assert_not_includes posts(:welcome).people, people(:michael)
}
- assert posts(:welcome).reload.people.reload.include?(people(:david))
- assert !posts(:welcome).reload.people.reload.include?(people(:michael))
+ assert_includes posts(:welcome).reload.people.reload, people(:david)
+ assert_not_includes posts(:welcome).reload.people.reload, people(:michael)
end
def test_replace_order_is_preserved
@@ -591,10 +591,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
# *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
@@ -832,14 +832,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
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_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)
+ assert_includes author.named_categories.reload, category
end
def test_collection_exists
@@ -908,14 +908,14 @@ 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
@@ -1022,7 +1022,7 @@ 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?
end
@@ -1107,7 +1107,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
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
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 e95eeb64db..b2f47d2daf 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -182,7 +182,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
@member.organization = @organization
end
assert_equal @organization, @member.organization
- assert @organization.members.include?(@member)
+ assert_includes @organization.members, @member
assert_equal "Extra", @member.member_detail.extra_data
end
@@ -197,16 +197,16 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
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_includes @organization.members, @member
+ assert_not_includes @new_organization.members, @member
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_not_includes @organization.members, @member
+ assert_includes @new_organization.members, @member
end
def test_preloading_has_one_through_on_belongs_to
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 61678ae210..15a7ae941a 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -24,15 +24,15 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
: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
@@ -467,10 +467,10 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
saved_post.tags << new_tag
assert new_tag.persisted? #consistent with habtm!
assert saved_post.persisted?
- assert saved_post.tags.include?(new_tag)
+ assert_includes saved_post.tags, new_tag
assert new_tag.persisted?
- assert saved_post.reload.tags.reload.include?(new_tag)
+ assert_includes saved_post.reload.tags.reload, new_tag
new_post = Post.new(title: "Association replacement works!", body: "You best believe it.")
saved_tag = tags(:general)
@@ -478,11 +478,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
new_post.tags << saved_tag
assert !new_post.persisted?
assert saved_tag.persisted?
- assert new_post.tags.include?(saved_tag)
+ assert_includes new_post.tags, saved_tag
new_post.save!
assert new_post.persisted?
- assert new_post.reload.tags.reload.include?(saved_tag)
+ assert_includes new_post.reload.tags.reload, saved_tag
assert !posts(:thinking).tags.build.persisted?
assert !posts(:thinking).tags.new.persisted?
@@ -642,8 +642,8 @@ 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))
+ assert_no_queries { assert_includes p.taggings, expected }
+ assert_includes posts(:welcome).taggings, taggings(:welcome_general)
end
def test_polymorphic_has_one
@@ -675,8 +675,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
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
@@ -709,7 +709,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert_no_queries do
assert david.categories.loaded?
- assert david.categories.include?(category)
+ assert_includes david.categories, category
end
end
@@ -720,7 +720,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
david.reload
assert ! david.categories.loaded?
assert_queries(1) do
- assert david.categories.include?(category)
+ assert_includes david.categories, category
end
assert ! david.categories.loaded?
end
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index 5222703570..c095b3a91c 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -131,7 +131,7 @@ class AssociationProxyTest < ActiveRecord::TestCase
david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!"))
assert !david.posts.loaded?
- assert david.posts.include?(post)
+ assert_includes david.posts, post
end
def test_push_has_many_through_does_not_load_target
@@ -139,7 +139,7 @@ class AssociationProxyTest < ActiveRecord::TestCase
david.categories << categories(:technology)
assert !david.categories.loaded?
- assert david.categories.include?(categories(:technology))
+ assert_includes david.categories, categories(:technology)
end
def test_push_followed_by_save_does_not_load_target
@@ -149,7 +149,7 @@ class AssociationProxyTest < ActiveRecord::TestCase
assert !david.posts.loaded?
david.save
assert !david.posts.loaded?
- assert david.posts.include?(post)
+ assert_includes david.posts, post
end
def test_push_does_not_lose_additions_to_new_record
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index 2d2ecb4079..a8592bd179 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -1,5 +1,4 @@
require "cases/helper"
-require "thread"
module ActiveRecord
module AttributeMethods
@@ -40,13 +39,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 993d9f8b94..4c77ecab7c 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -755,7 +755,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
topic = @target.new(title: "The pros and cons of programming naked.")
assert !topic.respond_to?(: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
@@ -765,7 +765,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
topic = @target.new
assert !topic.respond_to?(:title=)
exception = assert_raise(NoMethodError) { topic.title = "Pants" }
- assert exception.message.include?("private method")
+ assert_includes exception.message, "private method"
topic.send(:title=, "Very large pants")
end
@@ -775,7 +775,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
topic = @target.new(title: "Isaac Newton's pants")
assert !topic.respond_to?(:title?)
exception = assert_raise(NoMethodError) { topic.title? }
- assert exception.message.include?("private method")
+ assert_includes exception.message, "private method"
assert topic.send(:title?)
end
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
index c18851257e..f4620ae2da 100644
--- a/activerecord/test/cases/attributes_test.rb
+++ b/activerecord/test/cases/attributes_test.rb
@@ -112,7 +112,7 @@ module ActiveRecord
assert_equal 7, klass.attribute_types.length
assert_equal 7, klass.column_defaults.length
- assert klass.attribute_types.include?("wibble")
+ assert_includes klass.attribute_types, "wibble"
end
test "the given default value is cast from user" do
@@ -253,7 +253,7 @@ module ActiveRecord
test "attributes not backed by database columns appear in inspect" do
inspection = OverloadedType.new.inspect
- assert inspection.include?("non_existent_decimal")
+ assert_includes inspection, "non_existent_decimal"
end
end
end
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 08df945063..c24d7b8835 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -443,7 +443,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
assert_not invalid_electron.valid?
assert valid_electron.valid?
assert_not molecule.valid?
- assert_equal [{ error: :blank }], molecule.errors.details["electrons.name"]
+ assert_equal [{ error: :blank }], molecule.errors.details[:"electrons.name"]
end
def test_errors_details_should_be_indexed_when_passed_as_array
@@ -457,8 +457,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
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_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
@@ -474,8 +474,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
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_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
@@ -585,7 +585,7 @@ 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
@@ -594,7 +594,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
post.save
post.reload
assert_equal 2, post.people.length
- assert post.people.include?(people(:david))
+ assert_includes post.people, people(:david)
end
def test_build_before_save
@@ -647,7 +647,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
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index c03c3f9546..fafa144c6f 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1519,7 +1519,7 @@ class BasicsTest < ActiveRecord::TestCase
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_not_includes Developer.columns_hash.keys, "first_name"
end
test "ignored columns have no attribute methods" do
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index bacf93d275..db2871d383 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -93,20 +93,20 @@ 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}"
+ 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}"
+ 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) }
+ [ [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
@@ -453,7 +453,7 @@ class CalculationsTest < ActiveRecord::TestCase
def test_should_count_field_in_joined_table_with_group_by
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
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 6a7d333766..09bd00291d 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -495,8 +495,8 @@ class DirtyTest < ActiveRecord::TestCase
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_includes pirate.previous_changes, "updated_on"
+ assert_includes pirate.previous_changes, "created_on"
assert !pirate.previous_changes.key?("parrot_id")
pirate.catchphrase = "Yar!!"
@@ -631,7 +631,7 @@ 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
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
index c2009843f0..b7641fcf32 100644
--- a/activerecord/test/cases/enum_test.rb
+++ b/activerecord/test/cases/enum_test.rb
@@ -18,6 +18,7 @@ class EnumTest < ActiveRecord::TestCase
assert @book.author_visibility_visible?
assert @book.illustrator_visibility_visible?
assert @book.with_medium_font_size?
+ assert @book.medium_to_read?
end
test "query state with strings" do
@@ -26,6 +27,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 +36,7 @@ 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
end
test "find via where with values" do
@@ -422,6 +425,43 @@ class EnumTest < ActiveRecord::TestCase
assert_not @book.in_french?
end
+ test "query state by predicate with custom suffix" do
+ assert @book.medium_to_read?
+ assert_not @book.easy_to_read?
+ assert_not @book.hard_to_read?
+ end
+
+ test "enum methods with custom suffix defined" do
+ assert @book.class.respond_to?(:easy_to_read)
+ assert @book.class.respond_to?(:medium_to_read)
+ assert @book.class.respond_to?(:hard_to_read)
+
+ assert @book.respond_to?(:easy_to_read?)
+ assert @book.respond_to?(:medium_to_read?)
+ assert @book.respond_to?(:hard_to_read?)
+
+ assert @book.respond_to?(:easy_to_read!)
+ assert @book.respond_to?(:medium_to_read!)
+ assert @book.respond_to?(:hard_to_read!)
+ end
+
+ test "update enum attributes with custom suffix" do
+ @book.medium_to_read!
+ assert_not @book.easy_to_read?
+ assert @book.medium_to_read?
+ assert_not @book.hard_to_read?
+
+ @book.easy_to_read!
+ assert @book.easy_to_read?
+ assert_not @book.medium_to_read?
+ assert_not @book.hard_to_read?
+
+ @book.hard_to_read!
+ assert_not @book.easy_to_read?
+ assert_not @book.medium_to_read?
+ assert @book.hard_to_read?
+ end
+
test "uses default status when no status is provided in fixtures" do
book = books(:tlg)
assert book.proposed?, "expected fixture to default to proposed status"
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 31bc4fa1f2..51563b347c 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -1008,8 +1008,8 @@ class FinderTest < ActiveRecord::TestCase
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_joins_dont_clobber_id
@@ -1092,7 +1092,7 @@ class FinderTest < ActiveRecord::TestCase
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
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index 24a9894f19..3f111447ff 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -728,7 +728,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
@@ -741,7 +741,7 @@ class LoadAllFixturesWithPathnameTest < 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
@@ -923,7 +923,7 @@ 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
diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb
index e0946544ef..e107ff2362 100644
--- a/activerecord/test/cases/hot_compatibility_test.rb
+++ b/activerecord/test/cases/hot_compatibility_test.rb
@@ -119,11 +119,11 @@ class HotCompatibilityTest < ActiveRecord::TestCase
.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.
+ # 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))
diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb
index 814d3b63b2..b06fed4f0d 100644
--- a/activerecord/test/cases/json_serialization_test.rb
+++ b/activerecord/test/cases/json_serialization_test.rb
@@ -51,7 +51,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,7 +62,7 @@ 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
@@ -73,7 +73,7 @@ class JsonSerializationTest < ActiveRecord::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
@@ -83,7 +83,7 @@ class JsonSerializationTest < ActiveRecord::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
@@ -275,7 +275,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
['"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/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 5c55584ff7..13b6f6daaf 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -181,6 +181,7 @@ 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
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index ab8229dbae..03d781d3d2 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -63,8 +63,6 @@ module ActiveRecord
# 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
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index 870a7f4fa7..55c06da411 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -32,7 +32,7 @@ module ActiveRecord
rename_column :test_models, :first_name, :nick_name
TestModel.reset_column_information
- assert TestModel.column_names.include?("nick_name")
+ assert_includes TestModel.column_names, "nick_name"
assert_equal ["foo"], TestModel.all.map(&:nick_name)
end
@@ -45,7 +45,7 @@ module ActiveRecord
rename_column "test_models", "first_name", "nick_name"
TestModel.reset_column_information
- assert TestModel.column_names.include?("nick_name")
+ assert_includes TestModel.column_names, "nick_name"
assert_equal ["foo"], TestModel.all.map(&:nick_name)
end
@@ -57,7 +57,7 @@ module ActiveRecord
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
end
@@ -88,7 +88,7 @@ module ActiveRecord
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
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index 5474c9ce71..fc4f700916 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -57,7 +57,7 @@ module ActiveRecord
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_includes index.columns, "url"
assert_equal "index_octopi_on_url", index.name
end
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index d83360e327..688c3ed2b1 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -391,14 +391,14 @@ class PersistenceTest < ActiveRecord::TestCase
end
topic = klass.create(title: "Another New Topic")
assert_queries(0) do
- topic.update_attribute(:title, "Another New Topic")
+ 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") }
+ assert_queries(0) { assert topic.update(title: "Another New Topic") }
+ assert_queries(0) { assert topic.update_attributes(title: "Another New Topic") }
end
def test_delete
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index 1c276aa765..296dafacc2 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -133,16 +133,12 @@ module ActiveRecord
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
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index cb47237e4b..5dac3d064b 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -127,9 +127,9 @@ class ReflectionTest < ActiveRecord::TestCase
: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)
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
index bc3b1de697..278dac8171 100644
--- a/activerecord/test/cases/relation/merging_test.rb
+++ b/activerecord/test/cases/relation/merging_test.rb
@@ -86,9 +86,9 @@ class RelationMergingTest < ActiveRecord::TestCase
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
diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
index e7dc06c106..966ae83a3f 100644
--- a/activerecord/test/cases/relation/mutation_test.rb
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -71,7 +71,7 @@ module ActiveRecord
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
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 3efaace69d..dcaae5b462 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -530,8 +530,8 @@ class RelationTest < ActiveRecord::TestCase
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
@@ -748,11 +748,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 }
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
index 6cfe4b9581..61062da3e1 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -63,7 +63,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_default_scoping_with_threads
2.times do
Thread.new {
- assert DeveloperOrderedBySalary.all.to_sql.include?("salary DESC")
+ assert_includes DeveloperOrderedBySalary.all.to_sql, "salary DESC"
DeveloperOrderedBySalary.connection.close
}.join
end
@@ -368,7 +368,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
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
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index 2f328e6e70..58e1310ab0 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -33,8 +33,8 @@ 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
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index a46123f451..27b4583457 100644
--- a/activerecord/test/cases/scoping/relation_scoping_test.rb
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -133,8 +133,8 @@ class RelationScopingTest < ActiveRecord::TestCase
scoped_developers = Developer.includes(:projects).scoping do
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
@@ -143,8 +143,8 @@ class RelationScopingTest < ActiveRecord::TestCase
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
@@ -155,7 +155,7 @@ class RelationScopingTest < ActiveRecord::TestCase
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
@@ -164,7 +164,7 @@ class RelationScopingTest < ActiveRecord::TestCase
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
@@ -173,7 +173,7 @@ class RelationScopingTest < ActiveRecord::TestCase
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
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index 079db3fe8b..d847a02679 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -30,7 +30,7 @@ module ActiveRecord
protected_environments = ActiveRecord::Base.protected_environments.dup
current_env = ActiveRecord::Migrator.current_environment
- assert !protected_environments.include?(current_env)
+ assert_not_includes protected_environments, current_env
# Assert no error
ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index 60ac3e08a1..8eddc5a9ed 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -125,12 +125,9 @@ 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.match?(sql)
end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 8f0b2bd313..834365660f 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -511,9 +511,10 @@ class TransactionTest < ActiveRecord::TestCase
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.
+ # 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 !topic.persisted?, "not persisted"
assert_nil topic.id
assert topic.frozen?, "not frozen"
@@ -630,11 +631,11 @@ 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")
+ assert_includes Topic.column_names, "stuff"
Topic.reset_column_information
Topic.connection.remove_column("topics", "stuff")
- assert !Topic.column_names.include?("stuff")
+ assert_not_includes Topic.column_names, "stuff"
end
if Topic.connection.supports_ddl_transactions?
diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb
index bdae806f7c..fd88a3ea67 100644
--- a/activerecord/test/cases/validations/i18n_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_validation_test.rb
@@ -36,7 +36,7 @@ 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 } }, {}],
diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb
index 3cbfbc22c6..0e38cee334 100644
--- a/activerecord/test/cases/view_test.rb
+++ b/activerecord/test/cases/view_test.rb
@@ -149,7 +149,7 @@ if ActiveRecord::Base.connection.supports_views?
end
end
-# sqlite dose not support CREATE, INSERT, and DELETE for VIEW
+ # sqlite dose not support CREATE, INSERT, and DELETE for VIEW
if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
class UpdateableViewTest < ActiveRecord::TestCase
self.use_transactional_tests = false
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/books.yml b/activerecord/test/fixtures/books.yml
index a304fba399..b3625ee72e 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
diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb
index 4c275fdebb..17bf3fbcb4 100644
--- a/activerecord/test/models/book.rb
+++ b/activerecord/test/models/book.rb
@@ -14,6 +14,7 @@ class Book < ActiveRecord::Base
enum author_visibility: [:visible, :invisible], _prefix: true
enum illustrator_visibility: [:visible, :invisible], _prefix: true
enum font_size: [:small, :medium, :large], _prefix: :with, _suffix: true
+ enum difficulty: [:easy, :medium, :hard], _suffix: :to_read
enum cover: { hard: "hard", soft: "soft" }
def published!
diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb
index 3196207ac9..113d21cb84 100644
--- a/activerecord/test/models/bulb.rb
+++ b/activerecord/test/models/bulb.rb
@@ -50,9 +50,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/schema/schema.rb b/activerecord/test/schema/schema.rb
index 9997ddee77..a4756ec75a 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -98,6 +98,7 @@ ActiveRecord::Schema.define do
t.column :author_visibility, :integer, default: 0
t.column :illustrator_visibility, :integer, default: 0
t.column :font_size, :integer, default: 0
+ t.column :difficulty, :integer, default: 0
t.column :cover, :string, default: "hard"
end
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 30985060fd..f840783059 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,22 @@
+* Fix `ActiveSupport::TimeWithZone#in` across DST boundaries.
+
+ Previously calls to `in` were being sent to the non-DST aware
+ method `Time#since` via `method_missing`. It is now aliased to
+ the DST aware `ActiveSupport::TimeWithZone#+` which handles
+ transitions across DST boundaries, e.g:
+
+ Time.zone = "US/Eastern"
+
+ t = Time.zone.local(2016,11,6,1)
+ # => Sun, 06 Nov 2016 01:00:00 EDT -05:00
+
+ t.in(1.hour)
+ # => Sun, 06 Nov 2016 01:00:00 EST -05:00
+
+ Fixes #26580.
+
+ *Thomas Balthazar*
+
* Remove unused parameter `options = nil` for `#clear` of
`ActiveSupport::Cache::Strategy::LocalCache::LocalStore` and
`ActiveSupport::Cache::Strategy::LocalCache`.
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index 8ef91fa3f0..e65e472d08 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -200,15 +200,15 @@ module ActiveSupport
# You may also specify additional options via the +options+ argument.
# 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.
+ # required when +force+ is true so this always results in a cache write.
#
# cache.write('today', 'Monday')
# 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
+ # The +:force+ option is useful when you're calling some other method to
# ask whether you should force a cache write. Otherwise, it's clearer to
- # just call `Cache#write`.
+ # just call <tt>Cache#write</tt>.
#
# Setting <tt>:compress</tt> will store a large cache entry set by the call
# in a compressed format.
@@ -361,6 +361,9 @@ module ActiveSupport
# the cache with the given keys, then that data is returned. Otherwise,
# the supplied block is called for each key for which there was no data,
# and the result will be written to the cache and returned.
+ # 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.
#
# Options are passed to the underlying cache implementation.
#
@@ -374,6 +377,8 @@ module ActiveSupport
# # "unknown_key" => "Fallback value for key: unknown_key" }
#
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)
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index ecf0c0eec7..890b1cd73b 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -63,6 +63,8 @@ module ActiveSupport
included do
extend ActiveSupport::DescendantsTracker
+ class_attribute :__callbacks, instance_writer: false
+ self.__callbacks ||= {}
end
CALLBACK_FILTER_TYPES = [:before, :after, :around]
@@ -86,25 +88,61 @@ 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]
+
+ if callbacks.empty?
+ yield if block_given?
+ else
+ env = Filters::Environment.new(self, false, nil)
+ next_sequence = callbacks.compile
+
+ invoke_sequence = Proc.new do
+ skipped = nil
+ while true
+ current, next_sequence = next_sequence, next_sequence.nested
+ current.invoke_before(env)
+ if current.final?
+ env.value = !env.halted && (!block_given? || yield)
+ elsif current.skip?(env)
+ (skipped ||= []) << current
+ next
+ else
+ expanded = current.expand_call_template(env, invoke_sequence)
+ expanded.shift.send(*expanded, &expanded.shift)
+ end
+ current.invoke_after(env)
+ skipped.pop.invoke_after(env) while skipped && skipped.first
+ break env.value
+ end
+ end
- def __run_callbacks__(callbacks, &block)
- if callbacks.empty?
- yield if block_given?
+ # 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
- runner = callbacks.compile
- e = Filters::Environment.new(self, false, nil, block)
- runner.call(e).value
+ invoke_sequence.call
end
end
+ end
+
+ private
- # 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.
+ # 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
@@ -118,16 +156,7 @@ module ActiveSupport
end
module Filters
- Environment = Struct.new(:target, :halted, :value, :run_block)
-
- class End
- def call(env)
- block = env.run_block
- env.value = !env.halted && (!block || block.call)
- env
- end
- end
- ENDING = End.new
+ Environment = Struct.new(:target, :halted, :value)
class Before
def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter)
@@ -246,51 +275,6 @@ module ActiveSupport
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)
- end
- end
-
- def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
- callback_sequence.around do |env, &run|
- target = env.target
- value = env.value
- halted = env.halted
-
- if !halted && user_conditions.all? { |c| c.call(target, value) }
- user_callback.call(target, value) {
- run.call.value
- }
- env
- else
- run.call
- end
- 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
-
- if env.halted
- run.call
- else
- user_callback.call(target, value) {
- run.call.value
- }
- env
- end
- end
- end
- private_class_method :halting
- end
end
class Callback #:nodoc:#
@@ -349,23 +333,96 @@ module ActiveSupport
# Wraps code with filter
def apply(callback_sequence)
user_conditions = conditions_lambdas
- user_callback = make_lambda @filter
+ user_callback = CallTemplate.build(@filter, self)
case kind
when :before
- Filters::Before.build(callback_sequence, user_callback, user_conditions, chain_config, @filter)
+ Filters::Before.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config, @filter)
when :after
- Filters::After.build(callback_sequence, user_callback, user_conditions, chain_config)
+ Filters::After.build(callback_sequence, user_callback.make_lambda, user_conditions, chain_config)
when :around
- Filters::Around.build(callback_sequence, user_callback, user_conditions, chain_config)
+ callback_sequence.around(user_callback, user_conditions)
end
end
+ def current_scopes
+ Array(chain_config[:scope]).map { |s| public_send(s) }
+ end
+
private
+ def compute_identifier(filter)
+ case filter
+ when String, ::Proc
+ filter.object_id
+ else
+ filter
+ end
+ end
- def invert_lambda(l)
- lambda { |*args, &blk| !l.call(*args, &blk) }
+ def conditions_lambdas
+ @if.map { |c| CallTemplate.build(c, self).make_lambda } +
+ @unless.map { |c| CallTemplate.build(c, self).inverted_lambda }
end
+ end
+
+ # A future invocation of user-supplied code (either as a callback,
+ # or a condition filter).
+ class CallTemplate # :nodoc:
+ def initialize(target, method, arguments, block)
+ @override_target = target
+ @method_name = method
+ @arguments = arguments
+ @override_block = block
+ end
+
+ # 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
+ }
+
+ result.unshift @method_name
+ result.unshift @override_block || block
+ result.unshift @override_target || target
+
+ # target, block, method, *arguments = result
+ # target.send(method, *arguments, &block)
+ result
+ end
+
+ # Return a lambda that will make this call when given the input
+ # values.
+ def make_lambda
+ lambda do |target, value, &block|
+ c = expand(target, value, block)
+ c.shift.send(*c, &c.shift)
+ end
+ end
+
+ # Return a lambda that will make this call when given the input
+ # values, but then return the boolean inverse of that result.
+ def inverted_lambda
+ lambda do |target, value, &block|
+ c = expand(target, value, block)
+ ! c.shift.send(*c, &c.shift)
+ end
+ end
# Filters support:
#
@@ -374,60 +431,45 @@ module ActiveSupport
# 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
+ # All of these objects are converted into a CallTemplate 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
+ def self.build(filter, callback)
+ case filter
+ when Symbol
+ new(nil, filter, [], nil)
+ when String
+ new(nil, :instance_exec, [:value], compile_lambda(filter))
+ when Conditionals::Value
+ new(filter, :call, [:target, :value], nil)
+ when ::Proc
+ if filter.arity > 1
+ new(nil, :instance_exec, [:target, :block], filter)
+ elsif filter.arity > 0
+ new(nil, :instance_exec, [:target], filter)
else
- scopes = Array(chain_config[:scope])
- method_to_call = scopes.map { |s| public_send(s) }.join("_")
-
- lambda { |target, _, &blk|
- filter.public_send method_to_call, target, &blk
- }
+ new(nil, :instance_exec, [], filter)
end
- end
+ else
+ method_to_call = callback.current_scopes.join("_")
- def compute_identifier(filter)
- case filter
- when String, ::Proc
- filter.object_id
- else
- filter
- end
+ new(filter, method_to_call, [:target], nil)
end
+ end
- def conditions_lambdas
- @if.map { |c| make_lambda c } +
- @unless.map { |c| invert_lambda make_lambda c }
- end
+ def self.compile_lambda(filter)
+ eval("lambda { |value| #{filter} }")
+ end
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
+ # 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
@@ -442,23 +484,36 @@ module ActiveSupport
self
end
- def around(&around)
- CallbackSequence.new do |arg|
- around.call(arg) {
- call(arg)
- }
- end
+ def around(call_template, user_conditions)
+ CallbackSequence.new(self, call_template, user_conditions)
+ end
+
+ def skip?(arg)
+ arg.halted || !@user_conditions.all? { |c| c.call(arg.target, arg.value) }
+ end
+
+ def nested
+ @nested
+ end
+
+ def final?
+ !@call_template
end
- def call(arg)
+ def expand_call_template(arg, block)
+ @call_template.expand(arg.target, arg.value, block)
+ end
+
+ def invoke_before(arg)
@before.each { |b| b.call(arg) }
- value = @call.call(arg)
+ end
+
+ def invoke_after(arg)
@after.each { |a| a.call(arg) }
- value
end
end
- # An Array with a compile method.
+ # An Array with a compile method.
class CallbackChain #:nodoc:#
include Enumerable
@@ -503,7 +558,7 @@ module ActiveSupport
def compile
@callbacks || @mutex.synchronize do
- final_sequence = CallbackSequence.new { |env| Filters::ENDING.call(env) }
+ final_sequence = CallbackSequence.new
@callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback|
callback.apply callback_sequence
end
@@ -738,21 +793,34 @@ module ActiveSupport
#
# ===== Notes
#
- # +names+ passed to `define_callbacks` must not end with
- # `!`, `?` or `=`.
+ # +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`.
+ # 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|
- class_attribute "_#{name}_callbacks", instance_writer: false
+ name = name.to_sym
+
set_callbacks name, CallbackChain.new(name, options)
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def _run_#{name}_callbacks(&block)
- __run_callbacks__(_#{name}_callbacks, &block)
+ run_callbacks #{name.inspect}, &block
+ end
+
+ def self._#{name}_callbacks
+ get_callbacks(#{name.inspect})
+ end
+
+ def self._#{name}_callbacks=(value)
+ set_callbacks(#{name.inspect}, value)
+ end
+
+ def _#{name}_callbacks
+ __callbacks[#{name.inspect}]
end
RUBY
end
@@ -761,11 +829,11 @@ module ActiveSupport
protected
def get_callbacks(name) # :nodoc:
- send "_#{name}_callbacks"
+ __callbacks[name.to_sym]
end
def set_callbacks(name, callbacks) # :nodoc:
- send "_#{name}_callbacks=", callbacks
+ self.__callbacks = __callbacks.merge(name.to_sym => callbacks)
end
def deprecated_false_terminator # :nodoc:
diff --git a/activesupport/lib/active_support/concurrency/share_lock.rb b/activesupport/lib/active_support/concurrency/share_lock.rb
index 8969ebb080..4318523b07 100644
--- a/activesupport/lib/active_support/concurrency/share_lock.rb
+++ b/activesupport/lib/active_support/concurrency/share_lock.rb
@@ -199,7 +199,7 @@ module ActiveSupport
private
- # Must be called within synchronize
+ # Must be called within synchronize
def busy_for_exclusive?(purpose)
busy_for_sharing?(purpose) ||
@sharing.size > (@sharing[Thread.current] > 0 ? 1 : 0)
diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb
index 9b4a9a9992..ba422f9071 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute.rb
@@ -20,7 +20,7 @@ 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.
#
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 792076a449..c614f14289 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
@@ -300,7 +300,7 @@ module DateAndTime
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
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
index 3f4e236ab7..db95ae0db5 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
@@ -12,7 +12,11 @@ module DateAndTime
mattr_accessor(:preserve_timezone, instance_writer: false) { false }
def to_time
- preserve_timezone ? getlocal(utc_offset) : getlocal
+ if preserve_timezone
+ @_to_time_with_instance_offset ||= getlocal(utc_offset)
+ else
+ @_to_time_with_system_offset ||= getlocal
+ end
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb
index 4cb6ffea5e..cd00d1b662 100644
--- a/activesupport/lib/active_support/core_ext/load_error.rb
+++ b/activesupport/lib/active_support/core_ext/load_error.rb
@@ -1,3 +1,4 @@
+require "active_support/deprecation"
require "active_support/deprecation/proxy_wrappers"
class LoadError
diff --git a/activesupport/lib/active_support/duration/iso8601_parser.rb b/activesupport/lib/active_support/duration/iso8601_parser.rb
index df56f13b8d..e96cb8e883 100644
--- a/activesupport/lib/active_support/duration/iso8601_parser.rb
+++ b/activesupport/lib/active_support/duration/iso8601_parser.rb
@@ -84,7 +84,7 @@ module ActiveSupport
scanner.eos?
end
- # Parses number which can be a float with either comma or period.
+ # 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
@@ -97,7 +97,7 @@ module ActiveSupport
raise ParsingError, "Invalid ISO 8601 duration: #{scanner.string.inspect} #{reason}".strip
end
- # Checks for various semantic errors as stated in ISO 8601 standard.
+ # Checks for various semantic errors as stated in ISO 8601 standard.
def validate!
raise_parsing_error("is empty duration") if parts.empty?
diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb
index 0b7d67e37a..98aceabe21 100644
--- a/activesupport/lib/active_support/file_update_checker.rb
+++ b/activesupport/lib/active_support/file_update_checker.rb
@@ -105,13 +105,13 @@ module ActiveSupport
@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.
+ # 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
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index 79cede2a0c..c80243c40a 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -356,11 +356,11 @@ module ActiveSupport
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("::") # => "::"
+ # 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)
@@ -373,10 +373,10 @@ module ActiveSupport
end
end
- # Applies inflection rules for +singularize+ and +pluralize+.
- #
- # apply_inflections('post', inflections.plurals) # => "posts"
- # apply_inflections('posts', inflections.singulars) # => "post"
+ # 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
diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb
index 156cfc32f8..1064398d8c 100644
--- a/activesupport/lib/active_support/key_generator.rb
+++ b/activesupport/lib/active_support/key_generator.rb
@@ -51,8 +51,8 @@ module ActiveSupport
private
- # To prevent users from using something insecure like "Password" we make sure that the
- # secret they've provided is at least 30 characters in length.
+ # 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 " \
diff --git a/activesupport/lib/active_support/lazy_load_hooks.rb b/activesupport/lib/active_support/lazy_load_hooks.rb
index b84c7253a0..ae1897b886 100644
--- a/activesupport/lib/active_support/lazy_load_hooks.rb
+++ b/activesupport/lib/active_support/lazy_load_hooks.rb
@@ -15,9 +15,9 @@ 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)
module LazyLoadHooks
diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb
index 2159abef14..217919ccb8 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -30,36 +30,6 @@ module ActiveSupport
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.
diff --git a/activesupport/lib/active_support/testing/autorun.rb b/activesupport/lib/active_support/testing/autorun.rb
index 898ef209da..3108e3e549 100644
--- a/activesupport/lib/active_support/testing/autorun.rb
+++ b/activesupport/lib/active_support/testing/autorun.rb
@@ -2,11 +2,8 @@ gem "minitest"
require "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
+if Minitest.respond_to?(:run_via) && !Minitest.run_via[:rails]
+ Minitest.run_via[:ruby] = true
end
+
+Minitest.autorun
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index be2fceb123..8de8120fba 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -80,7 +80,7 @@ module ActiveSupport
# Returns a <tt>Time</tt> instance of the simultaneous time in the system timezone.
def localtime(utc_offset = nil)
- @localtime ||= utc.getlocal(utc_offset)
+ utc.getlocal(utc_offset)
end
alias_method :getlocal, :localtime
@@ -133,7 +133,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
@@ -279,6 +279,7 @@ 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.
diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb
index 70864038e8..46b91806f6 100644
--- a/activesupport/lib/active_support/xml_mini.rb
+++ b/activesupport/lib/active_support/xml_mini.rb
@@ -157,7 +157,7 @@ module ActiveSupport
"#{left}#{middle.tr('_ ', '--')}#{right}"
end
- # TODO: Add support for other encodings
+ # TODO: Add support for other encodings
def _parse_binary(bin, entity) #:nodoc:
case entity["encoding"]
when "base64"
diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb
index c698780da8..10498c9be0 100644
--- a/activesupport/lib/active_support/xml_mini/jdom.rb
+++ b/activesupport/lib/active_support/xml_mini/jdom.rb
@@ -52,12 +52,12 @@ module ActiveSupport
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
+ # 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)
@@ -68,10 +68,10 @@ module ActiveSupport
hash.delete(CONTENT_KEY) if hash[CONTENT_KEY] == ""
end
- # Actually converts an XML document element into a data structure.
- #
- # element::
- # The document element to be collapsed.
+ # 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)
@@ -88,12 +88,12 @@ module ActiveSupport
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
+ # 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)
@@ -105,17 +105,17 @@ module ActiveSupport
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.
+ # 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)
@@ -131,11 +131,11 @@ module ActiveSupport
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.
+ # 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
@@ -146,10 +146,10 @@ module ActiveSupport
attribute_hash
end
- # Determines if a document element has text content
- #
- # element::
- # XML element to be checked.
+ # Determines if a document element has text content
+ #
+ # element::
+ # XML element to be checked.
def texts(element)
texts = []
child_nodes = element.child_nodes
@@ -162,10 +162,10 @@ module ActiveSupport
texts
end
- # Determines if a document element has text content
- #
- # element::
- # XML element to be checked.
+ # Determines if a document element has text content
+ #
+ # element::
+ # XML element to be checked.
def empty_content?(element)
text = ""
child_nodes = element.child_nodes
diff --git a/activesupport/test/autoload_test.rb b/activesupport/test/autoload_test.rb
index c4d6a69212..6c8aa3e055 100644
--- a/activesupport/test/autoload_test.rb
+++ b/activesupport/test/autoload_test.rb
@@ -31,7 +31,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 +40,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 +51,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 +64,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 +75,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
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
index a669d666be..0df4173a1a 100644
--- a/activesupport/test/caching_test.rb
+++ b/activesupport/test/caching_test.rb
@@ -363,6 +363,12 @@ module CacheStoreBehavior
assert_equal({ foo => "FOO!", bar => "BAM!" }, values)
end
+ def test_fetch_multi_without_block
+ assert_raises(ArgumentError) do
+ @cache.fetch_multi("foo")
+ end
+ end
+
def test_read_and_write_compressed_small_data
@cache.write("foo", "bar", compress: true)
assert_equal "bar", @cache.read("foo")
diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb
index b4e98edd84..783952c8c7 100644
--- a/activesupport/test/callbacks_test.rb
+++ b/activesupport/test/callbacks_test.rb
@@ -56,6 +56,8 @@ 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))
@@ -67,7 +69,9 @@ module CallbacksTest
end
def save
- run_callbacks :save
+ run_callbacks :save do
+ raise "inside save" if save_fails
+ end
end
end
@@ -222,6 +226,7 @@ module CallbacksTest
class AroundPerson < MySuper
attr_reader :history
+ attr_accessor :save_fails
set_callback :save, :before, :nope, if: :no
set_callback :save, :before, :nope, unless: :yes
@@ -285,6 +290,7 @@ module CallbacksTest
def save
run_callbacks :save do
+ raise "inside save" if save_fails
@history << "running"
end
end
@@ -402,6 +408,71 @@ module CallbacksTest
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",
+ ("call" if RUBY_VERSION < "2.3"),
+ "run_callbacks",
+ "save"
+ ].compact, 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
diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb
index 4af9ce8163..95507c815d 100644
--- a/activesupport/test/concern_test.rb
+++ b/activesupport/test/concern_test.rb
@@ -66,7 +66,7 @@ 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
@@ -105,7 +105,7 @@ class ConcernTest < ActiveSupport::TestCase
assert_equal "bar", @klass.new.bar
assert_equal "bar+baz", @klass.new.baz
assert_equal "bar's baz + baz", @klass.baz
- assert @klass.included_modules.include?(ConcernTest::Bar)
+ assert_includes @klass.included_modules, ConcernTest::Bar
end
def test_dependencies_with_multiple_modules
diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb
index c67ea8b222..3cd6d2d4d0 100644
--- a/activesupport/test/configurable_test.rb
+++ b/activesupport/test/configurable_test.rb
@@ -121,11 +121,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/core_ext/array/conversions_test.rb b/activesupport/test/core_ext/array/conversions_test.rb
index ccbdab19f0..469efcd934 100644
--- a/activesupport/test/core_ext/array/conversions_test.rb
+++ b/activesupport/test/core_ext/array/conversions_test.rb
@@ -97,28 +97,28 @@ class ToXmlTest < ActiveSupport::TestCase
].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 = %w[1 2 3].to_xml(skip_instruct: true, indent: 0)
assert_equal '<strings type="array"><string', xml.first(29)
- assert xml.include?(%(<string>2</string>)), xml
+ 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)
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
@@ -135,10 +135,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
@@ -147,10 +147,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
@@ -159,8 +159,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
@@ -169,8 +169,8 @@ 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
@@ -191,7 +191,7 @@ class ToXmlTest < ActiveSupport::TestCase
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/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index e457cb866d..ae35adb19b 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -1127,63 +1127,63 @@ class HashToXmlTest < ActiveSupport::TestCase
def test_one_level
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))
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))
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))
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))
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)
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)
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))
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
@@ -1192,37 +1192,37 @@ class HashToXmlTest < ActiveSupport::TestCase
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)
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)
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)
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>))
+ assert_includes xml, %(<addresses type="array"><address><streets type="array"><street><name>)
end
def test_timezoned_attributes
diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb
index 038cbf1f2f..098036828a 100644
--- a/activesupport/test/core_ext/module/concerning_test.rb
+++ b/activesupport/test/core_ext/module/concerning_test.rb
@@ -4,7 +4,7 @@ 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
@@ -21,10 +21,10 @@ class ModuleConcernTest < ActiveSupport::TestCase
assert klass.const_defined?(:Baz, false)
assert !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")
diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb
index 25e6693e4d..fc0e72e09a 100644
--- a/activesupport/test/core_ext/range_ext_test.rb
+++ b/activesupport/test/core_ext/range_ext_test.rb
@@ -66,15 +66,15 @@ class RangeTest < ActiveSupport::TestCase
end
def test_exclusive_end_should_not_include_identical_with_inclusive_end
- assert !(1...10).include?(1..10)
+ assert_not_includes (1...10), 1..10
end
def test_should_not_include_overlapping_first
- assert !(2..8).include?(1..3)
+ assert_not_includes (2..8), 1..3
end
def test_should_not_include_overlapping_last
- assert !(2..8).include?(5..9)
+ assert_not_includes (2..8), 5..9
end
def test_should_include_identical_exclusive_with_floats
diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb
index 6ab1368e53..e35aa6e154 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -675,6 +675,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
@@ -688,6 +692,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
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
@@ -696,6 +701,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
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
@@ -704,6 +710,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
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
@@ -712,6 +719,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
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
@@ -719,9 +727,11 @@ class TimeWithZoneTest < ActiveSupport::TestCase
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
@@ -730,8 +740,10 @@ class TimeWithZoneTest < ActiveSupport::TestCase
# 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.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
@@ -753,12 +765,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase
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.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.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.in(24.hours).inspect
assert_equal "Sun, 02 Apr 2006 11:30:00 EDT -04:00", twz.advance(hours: 24).inspect
end
@@ -785,8 +801,10 @@ class TimeWithZoneTest < ActiveSupport::TestCase
# 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.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
@@ -808,12 +826,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase
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.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.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.in(24.hours).inspect
assert_equal "Sun, 29 Oct 2006 09:30:00 EST -05:00", twz.advance(hours: 24).inspect
end
@@ -839,6 +861,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
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
@@ -855,6 +878,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
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
@@ -871,6 +895,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
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
@@ -887,6 +912,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
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
@@ -902,6 +928,8 @@ class TimeWithZoneTest < ActiveSupport::TestCase
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.years_ago(1).inspect
@@ -912,6 +940,8 @@ class TimeWithZoneTest < ActiveSupport::TestCase
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.years_ago(1).inspect
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index e0edb4a086..54068f5a08 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -114,25 +114,25 @@ class DependenciesTest < ActiveSupport::TestCase
assert_equal 1, $check_warnings_load_count
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 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 ActiveSupport::Dependencies.loaded.include?(expanded)
+ assert_includes ActiveSupport::Dependencies.loaded, expanded
ActiveSupport::Dependencies.warnings_on_first_load = old_warnings
end
end
@@ -1059,13 +1059,13 @@ 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
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index 39e8c611e5..1c5a4f378c 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -245,12 +245,12 @@ 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
+ # 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|
diff --git a/activesupport/test/logger_test.rb b/activesupport/test/logger_test.rb
index ece4167de9..3f04783401 100644
--- a/activesupport/test/logger_test.rb
+++ b/activesupport/test/logger_test.rb
@@ -67,7 +67,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
@@ -79,25 +79,25 @@ 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)
+ 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)
+ 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)
+ assert_includes @output.string, @integer_message.to_s
end
def test_should_not_evaluate_block_if_message_wont_be_logged
@@ -125,10 +125,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,7 +138,7 @@ 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
@@ -154,12 +154,12 @@ class LoggerTest < ActiveSupport::TestCase
@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
@@ -176,13 +176,13 @@ class LoggerTest < ActiveSupport::TestCase
@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/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb
index 54429f4aab..5ff1543328 100644
--- a/activesupport/test/multibyte_chars_test.rb
+++ b/activesupport/test/multibyte_chars_test.rb
@@ -213,11 +213,11 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
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
diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb
index d2dfc34040..86da9f193a 100644
--- a/activesupport/test/ordered_hash_test.rb
+++ b/activesupport/test/ordered_hash_test.rb
@@ -129,22 +129,22 @@ class OrderedHashTest < ActiveSupport::TestCase
copy = @ordered_hash.dup
copy.delete("pink")
assert_equal copy, @ordered_hash.delete_if { |k, _| k == "pink" }
- assert !@ordered_hash.keys.include?("pink")
+ assert_not_includes @ordered_hash.keys, "pink"
end
def test_reject!
(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" }
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
@@ -191,7 +191,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 +201,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
diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb
index f89b608270..76fee1fdd4 100644
--- a/activesupport/test/time_zone_test.rb
+++ b/activesupport/test/time_zone_test.rb
@@ -505,13 +505,13 @@ 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 ActiveSupport::TimeZone.country_zones("ru").include?(ActiveSupport::TimeZone["Moscow"])
- assert !ActiveSupport::TimeZone.country_zones(:ru).include?(ActiveSupport::TimeZone["Kuala Lumpur"])
+ 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_to_yaml
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..f568a111f6
--- /dev/null
+++ b/guides/bug_report_templates/active_record_migrations_gem.rb
@@ -0,0 +1,65 @@
+begin
+ require "bundler/inline"
+rescue LoadError => e
+ $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler"
+ raise e
+end
+
+gemfile(true) do
+ source "https://rubygems.org"
+ # Activate the gem you are reporting the issue against.
+ gem "activerecord", "5.0.0.1"
+ gem "sqlite3"
+end
+
+require "active_record"
+require "minitest/autorun"
+require "logger"
+
+# Ensure backward compatibility with Minitest 4
+Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
+
+# 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.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
+ migrator = ActiveRecord::Migrator.new(:up, [ChangeAmountToAddScale])
+ migrator.run
+ Payment.reset_column_information
+
+ assert_equal "decimal(10,2)", Payment.columns.last.sql_type
+ end
+
+ def test_migration_down
+ migrator = ActiveRecord::Migrator.new(:down, [ChangeAmountToAddScale])
+ migrator.run
+ Payment.reset_column_information
+
+ assert_equal "decimal(10,0)", Payment.columns.last.sql_type
+ end
+end \ No newline at end of file
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..ef7b42e0a6
--- /dev/null
+++ b/guides/bug_report_templates/active_record_migrations_master.rb
@@ -0,0 +1,64 @@
+begin
+ require "bundler/inline"
+rescue LoadError => e
+ $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler"
+ raise e
+end
+
+gemfile(true) do
+ source "https://rubygems.org"
+ gem "rails", github: "rails/rails"
+ gem "sqlite3"
+end
+
+require "active_record"
+require "minitest/autorun"
+require "logger"
+
+# Ensure backward compatibility with Minitest 4
+Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
+
+# 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.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
+ migrator = ActiveRecord::Migrator.new(:up, [ChangeAmountToAddScale])
+ migrator.run
+ Payment.reset_column_information
+
+ assert_equal "decimal(10,2)", Payment.columns.last.sql_type
+ end
+
+ def test_migration_down
+ migrator = ActiveRecord::Migrator.new(:down, [ChangeAmountToAddScale])
+ migrator.run
+ Payment.reset_column_information
+
+ assert_equal "decimal(10,0)", Payment.columns.last.sql_type
+ end
+end \ No newline at end of file
diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb
index a3e7620444..e947150364 100644
--- a/guides/rails_guides/levenshtein.rb
+++ b/guides/rails_guides/levenshtein.rb
@@ -1,6 +1,8 @@
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)
s = str1
diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md
index c6bac34d18..ac5833e069 100644
--- a/guides/source/2_2_release_notes.md
+++ b/guides/source/2_2_release_notes.md
@@ -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.
diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md
index 6538629972..cc332cbf97 100644
--- a/guides/source/5_0_release_notes.md
+++ b/guides/source/5_0_release_notes.md
@@ -583,7 +583,7 @@ Please refer to the [Changelog][active-record] for detailed changes.
* Removed support for the legacy `mysql` database adapter from core. Most users should
be able to use `mysql2`. It will be converted to a separate gem when when we find someone
- to maintain it. ([Pull Request 1](https://github.com/rails/rails/pull/22642)],
+ 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.
@@ -595,6 +595,9 @@ Please refer to the [Changelog][active-record] for detailed changes.
* 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
@@ -802,7 +805,8 @@ Please refer to the [Changelog][active-record] for detailed changes.
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, as the other callbacks.
+ 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
diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md
index e11466e79f..ff0127522b 100644
--- a/guides/source/action_view_overview.md
+++ b/guides/source/action_view_overview.md
@@ -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.
diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md
index 493fd526fb..38b1ffc4c8 100644
--- a/guides/source/active_record_querying.md
+++ b/guides/source/active_record_querying.md
@@ -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, including MySQL, MariaDB, PostgreSQL and SQLite. 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
------------------------------------
@@ -81,7 +81,6 @@ The methods are:
* `reorder`
* `reverse_order`
* `select`
-* `distinct`
* `where`
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.
@@ -1020,7 +1019,7 @@ Author.joins("INNER JOIN posts ON posts.author_id = authors.id AND posts.publish
This will result in the following SQL:
```sql
-SELECT clients.* FROM clients INNER JOIN posts ON posts.author_id = authors.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
diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md
index 03af3cf819..3fc9d9bfa9 100644
--- a/guides/source/active_support_instrumentation.md
+++ b/guides/source/active_support_instrumentation.md
@@ -231,12 +231,13 @@ 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` | `self.object_id` |
+| `:binds` | Bind parameters |
+| `:cached` | `true` is added when cached queries used |
INFO. The adapters will add their own data as well.
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 9cec5ec452..fbf3c27957 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -71,7 +71,7 @@ These configuration methods are to be called on a `Rails::Railtie` object, such
* `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`, 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`.
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index 5df16e68c9..4f938f5deb 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -40,6 +40,7 @@ Then, don't get your hopes up! Unless you have a "Code Red, Mission Critical, th
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)
diff --git a/guides/source/engines.md b/guides/source/engines.md
index 83c0a7f337..0020112a1c 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -184,7 +184,7 @@ 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
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index 2722789c49..7e4ec5ba7e 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -630,6 +630,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 +751,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`
diff --git a/guides/source/testing.md b/guides/source/testing.md
index 8f9246dea2..0ac5121b12 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -761,8 +761,8 @@ 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 can accept up to 6 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 URI of the controller action you are requesting.
+ This can be in the form of a string or a route helper (e.g. `articles_url`).
* `params`: option with a hash of request parameters to pass into the action
(e.g. query string parameters or article variables).
* `headers`: for setting the headers that will be passed with the request.
@@ -775,13 +775,13 @@ All of these keyword arguments are optional.
Example: Calling the `:show` action, passing an `id` of 12 as the `params` and setting `HTTP_REFERER` header:
```ruby
-get :show, params: { id: 12 }, headers: { "HTTP_REFERER" => "http://example.com/home" }
+get article_url, params: { id: 12 }, headers: { "HTTP_REFERER" => "http://example.com/home" }
```
Another example: Calling the `:update` action, passing an `id` of 12 as the `params` as an Ajax request.
```ruby
-patch update_url, params: { id: 12 }, xhr: true
+patch article_url, params: { id: 12 }, 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.
@@ -1317,8 +1317,8 @@ end
This test is pretty simple and only asserts that the job get the work done
as expected.
-By default, `ActiveJob::TestCase` will set the queue adapter to `:async` so that
-your jobs are performed in an async fashion. It will also ensure that all previously performed
+By default, `ActiveJob::TestCase` will set the queue adapter to `:test` so that
+your jobs are performed inline. It will also ensure that all previously performed
and enqueued jobs are cleared before any test run so you can safely assume that
no jobs have already been executed in the scope of each test.
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 594d239290..b03c87e9ba 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Run `Minitest.after_run` hooks when running `rails test`.
+
+ *Michael Grosser*
+
* Run `before_configuration` callbacks as soon as application constant
inherits from `Rails::Application`.
diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb
index e9c96c7b43..5d862e3fec 100644
--- a/railties/lib/rails.rb
+++ b/railties/lib/rails.rb
@@ -46,7 +46,7 @@ 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
+ # 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
diff --git a/railties/lib/rails/cli.rb b/railties/lib/rails/cli.rb
index 26ef3822ba..973b746068 100644
--- a/railties/lib/rails/cli.rb
+++ b/railties/lib/rails/cli.rb
@@ -7,9 +7,11 @@ Rails::AppLoader.exec_app
require "rails/ruby_version_check"
Signal.trap("INT") { puts; exit(1) }
+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/command.rb b/railties/lib/rails/command.rb
new file mode 100644
index 0000000000..6065e78fd1
--- /dev/null
+++ b/railties/lib/rails/command.rb
@@ -0,0 +1,99 @@
+require "active_support"
+require "active_support/dependencies/autoload"
+require "active_support/core_ext/enumerable"
+require "active_support/core_ext/object/blank"
+require "active_support/core_ext/hash/transform_values"
+
+require "thor"
+
+module Rails
+ module Command
+ extend ActiveSupport::Autoload
+
+ autoload :Behavior
+ autoload :Base
+
+ include Behavior
+
+ class << self
+ def hidden_commands # :nodoc:
+ @hidden_commands ||= []
+ end
+
+ def environment # :nodoc:
+ ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
+ end
+
+ # Receives a namespace, arguments and the behavior to invoke the command.
+ def invoke(namespace, args = [], **config)
+ namespace = namespace.to_s
+ namespace = "help" if namespace.blank? || Thor::HELP_MAPPINGS.include?(namespace)
+ namespace = "version" if %w( -v --version ).include? namespace
+
+ if command = find_by_namespace(namespace)
+ command.perform(namespace, args, config)
+ else
+ find_by_namespace("rake").perform(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(name) # :nodoc:
+ lookups = [ name, "rails:#{name}" ]
+
+ 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:
+ sorted_groups.each { |b, n| print_list(b, n) }
+ end
+
+ def sorted_groups # :nodoc:
+ lookup!
+
+ groups = (subclasses - hidden_commands).group_by { |c| c.namespace.split(":").first }
+ groups.transform_values! { |commands| commands.flat_map(&:printing_commands).sort }
+
+ rails = groups.delete("rails")
+ [[ "rails", rails ]] + groups.sort.to_a
+ end
+
+ protected
+ def command_type
+ @command_type ||= "command"
+ end
+
+ def lookup_paths
+ @lookup_paths ||= %w( rails/commands commands )
+ end
+
+ def file_lookup_paths
+ @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..31b656ec31
--- /dev/null
+++ b/railties/lib/rails/command/actions.rb
@@ -0,0 +1,42 @@
+module Rails
+ module Command
+ module Actions
+ # 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
+
+ if defined?(ENGINE_PATH)
+ def require_application_and_environment!
+ require ENGINE_PATH
+ end
+
+ 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 require_application_and_environment!
+ require APP_PATH
+ Rails.application.require_environment!
+ end
+
+ 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..1efcd69e63
--- /dev/null
+++ b/railties/lib/rails/command/base.rb
@@ -0,0 +1,135 @@
+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)
+ 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:
+ command = nil if Thor::HELP_MAPPINGS.include?(args.first)
+
+ dispatch(command, args.dup, nil, config)
+ end
+
+ def printing_commands
+ namespace.sub(/^rails:/, "")
+ end
+
+ def executable
+ "bin/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 `rails/command/test_command.rb`
+ # would return `rails/test`.
+ def default_command_root
+ path = File.expand_path(File.join("../commands", command_name), __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
+ 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..ce994746a4
--- /dev/null
+++ b/railties/lib/rails/command/behavior.rb
@@ -0,0 +1,123 @@
+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
+
+ protected
+
+ # 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)
+ s = str1
+ t = str2
+ n = s.length
+ m = t.length
+
+ return m if (0 == n)
+ return n if (0 == m)
+
+ d = (0..m).to_a
+ x = nil
+
+ # avoid duplicating an enumerable object in the loop
+ str2_codepoint_enumerable = str2.each_codepoint
+
+ str1.each_codepoint.with_index do |char1, i|
+ e = i+1
+
+ str2_codepoint_enumerable.with_index do |char2, j|
+ cost = (char1 == char2) ? 0 : 1
+ x = [
+ d[j+1] + 1, # insertion
+ e + 1, # deletion
+ d[j] + cost # substitution
+ ].min
+ d[j] = e
+ e = x
+ end
+
+ d[m] = x
+ end
+
+ x
+ end
+
+ # Prints a list of generators.
+ def print_list(base, namespaces) #:nodoc:
+ 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) #:nodoc:
+ 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! #:nodoc:
+ $LOAD_PATH.each do |base|
+ Dir[File.join(base, *file_lookup_paths)].each do |path|
+ begin
+ path = path.sub("#{base}/", "")
+ require path
+ rescue Exception
+ # No problem
+ end
+ end
+ end
+ end
+
+ # Convert namespaces to paths by replacing ":" for "/" and adding
+ # an extra lookup. For example, "rails:model" should be searched
+ # in both: "rails/model/model_generator" and "rails/model_generator".
+ def namespaces_to_paths(namespaces) #:nodoc:
+ paths = []
+ namespaces.each do |namespace|
+ pieces = namespace.split(":")
+ paths << pieces.dup.push(pieces.last).join("/")
+ paths << pieces.join("/")
+ end
+ paths.uniq!
+ paths
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/command/environment_argument.rb b/railties/lib/rails/command/environment_argument.rb
new file mode 100644
index 0000000000..05eac34155
--- /dev/null
+++ b/railties/lib/rails/command/environment_argument.rb
@@ -0,0 +1,34 @@
+require "active_support"
+
+module Rails
+ module Command
+ module EnvironmentArgument #:nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ argument :environment, optional: true, banner: "environment"
+ end
+
+ private
+ def extract_environment_option_from_argument
+ if environment
+ self.options = options.merge(environment: acceptable_environment(environment))
+ elsif !options[:environment]
+ self.options = options.merge(environment: Rails::Command.environment)
+ end
+ end
+
+ def acceptable_environment(env = nil)
+ if available_environments.include? env
+ env
+ else
+ %w( production development test ).detect { |e| e =~ /^#{env}/ } || env
+ end
+ end
+
+ def available_environments
+ Dir["config/environments/*.rb"].map { |fname| File.basename(fname, ".*") }
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb
index d64b355aec..fff0119c65 100644
--- a/railties/lib/rails/commands.rb
+++ b/railties/lib/rails/commands.rb
@@ -1,4 +1,4 @@
-ARGV << "--help" if ARGV.empty?
+require "rails/command"
aliases = {
"g" => "generate",
@@ -13,6 +13,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/application_command.rb
index f6e7771cf3..7e3a2b011d 100644
--- a/railties/lib/rails/commands/application.rb
+++ b/railties/lib/rails/commands/application/application_command.rb
@@ -11,7 +11,19 @@ module Rails
end
end
end
-end
-args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!
-Rails::Generators::AppGenerator.start args
+ module Command
+ class ApplicationCommand < Base
+ 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 43f9dd38f3..0000000000
--- a/railties/lib/rails/commands/commands_tasks.rb
+++ /dev/null
@@ -1,136 +0,0 @@
-require "rails/commands/rake_proxy"
-require "rails/commands/common_commands_tasks"
-require "active_support/core_ext/string/strip"
-
-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
- include Rails::CommonCommandsTasks
-
- attr_reader :argv
-
- 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")' ]
- ]
-
- def initialize(argv)
- @argv = argv
- end
-
- def plugin
- require_command!("plugin")
- 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 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
-
- 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
-
- # 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 commands
- ADDITIONAL_COMMANDS + formatted_rake_tasks
- end
-
- def command_whitelist
- %w(plugin generate destroy console server dbconsole runner new version help test)
- end
-
- def help_message
- <<-EOT.strip_heredoc
- 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
- end
-
- def require_application_and_environment!
- require APP_PATH
- Rails.application.require_environment!
- end
-
- def load_tasks
- Rails.application.load_tasks
- end
-
- def load_generators
- Rails.application.load_generators
- end
- end
-end
diff --git a/railties/lib/rails/commands/common_commands_tasks.rb b/railties/lib/rails/commands/common_commands_tasks.rb
deleted file mode 100644
index c1484d7ae2..0000000000
--- a/railties/lib/rails/commands/common_commands_tasks.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-module Rails
- module CommonCommandsTasks # :nodoc:
- 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(commands)
- end
-
- private
-
- def generate_or_destroy(command)
- require "rails/generators"
- require_application_and_environment!
- load_generators
- require_command!(command)
- end
-
- def require_command!(command)
- require "rails/commands/#{command}"
- 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/console_command.rb
index e00887323e..617066f575 100644
--- a/railties/lib/rails/commands/console.rb
+++ b/railties/lib/rails/commands/console/console_command.rb
@@ -1,12 +1,10 @@
-require "optparse"
require "irb"
require "irb/completion"
-require "rails/commands/console_helper"
+
+require "rails/command/environment_argument"
module Rails
class Console
- include ConsoleHelper
-
module BacktraceCleaner
def filter_backtrace(bt)
if result = super
@@ -15,26 +13,13 @@ module Rails
end
end
- 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
+ def self.start(*args)
+ new(*args).start
end
attr_reader :options, :app, :console
- def initialize(app, options={})
+ def initialize(app, options = {})
@app = app
@options = options
@@ -53,7 +38,7 @@ module Rails
end
def environment
- options[:environment] ||= super
+ options[:environment]
end
alias_method :environment?, :environment
@@ -77,4 +62,28 @@ module Rails
console.start
end
end
+
+ module Command
+ class ConsoleCommand < Base
+ include EnvironmentArgument
+
+ class_option :sandbox, aliases: "-s", type: :boolean, default: false,
+ desc: "Rollback database modifications on exit."
+
+ class_option :environment, aliases: "-e", type: :string,
+ desc: "Specifies the environment to run this console under (test/development/production)."
+
+ def perform
+ extract_environment_option_from_argument
+
+ # RAILS_ENV needs to be set before config/application is required.
+ ENV["RAILS_ENV"] = options[:environment]
+
+ ARGV.clear # Clear ARGV so IRB doesn't freak.
+
+ 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 0b7f1c4249..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
diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
index 66b7a14f16..d3c80da89b 100644
--- a/railties/lib/rails/commands/dbconsole.rb
+++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb
@@ -1,58 +1,20 @@
require "erb"
require "yaml"
-require "optparse"
-require "rails/commands/console_helper"
+
+require "rails/command/environment_argument"
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
+ def self.start(*args)
+ new(*args).start
end
- def initialize(arguments = ARGV)
- @arguments = arguments
+ def initialize(options = {})
+ @options = options
end
def start
- options = self.class.parse_arguments(arguments)
- ENV["RAILS_ENV"] = options[:environment] || environment
+ ENV["RAILS_ENV"] = @options[:environment] || environment
case config["adapter"]
when /^(jdbc)?mysql/
@@ -69,7 +31,7 @@ module Rails
"sslkey" => "--ssl-key"
}.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact
- if config["password"] && options["include_password"]
+ if config["password"] && @options["include_password"]
args << "--password=#{config['password']}"
elsif config["password"] && !config["password"].to_s.empty?
args << "-p"
@@ -83,14 +45,14 @@ module Rails
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"]
+ 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 << "-#{@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)
@@ -100,7 +62,7 @@ module Rails
if config["username"]
logon = config["username"]
- logon << "/#{config['password']}" if config["password"] && options["include_password"]
+ logon << "/#{config['password']}" if config["password"] && @options["include_password"]
logon << "@#{config['database']}" if config["database"]
end
@@ -137,7 +99,7 @@ module Rails
end
def environment
- Rails.respond_to?(:env) ? Rails.env : super
+ Rails.respond_to?(:env) ? Rails.env : Rails::Command.environment
end
protected
@@ -170,4 +132,27 @@ module Rails
end
end
end
+
+ module Command
+ class DbconsoleCommand < Base
+ 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: :string
+
+ class_option :environment, aliases: "-e", type: :string,
+ desc: "Specifies the environment to run this console under (test/development/production)."
+
+ def perform
+ extract_environment_option_from_argument
+
+ 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 71c8c5e526..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..5e6b7f9371
--- /dev/null
+++ b/railties/lib/rails/commands/destroy/destroy_command.rb
@@ -0,0 +1,21 @@
+require "rails/generators"
+
+module Rails
+ module Command
+ class DestroyCommand < Base
+ def help # :nodoc:
+ Rails::Generators.help self.class.command_name
+ end
+
+ def perform(*)
+ generator = args.shift
+ return help unless generator
+
+ require_application_and_environment!
+ Rails.application.load_generators
+
+ Rails::Generators.invoke generator, args, behavior: :revoke, destination_root: Rails.root
+ 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 ba6f14073e..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..b381ca85b9
--- /dev/null
+++ b/railties/lib/rails/commands/generate/generate_command.rb
@@ -0,0 +1,21 @@
+require "rails/generators"
+
+module Rails
+ module Command
+ class GenerateCommand < Base
+ def help # :nodoc:
+ Rails::Generators.help self.class.command_name
+ end
+
+ def perform(*)
+ generator = args.shift
+ return help unless generator
+
+ require_application_and_environment!
+ load_generators
+
+ 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..348f41861f
--- /dev/null
+++ b/railties/lib/rails/commands/help/USAGE
@@ -0,0 +1,27 @@
+Usage: bin/rails COMMAND [args] [options]
+<% if engine? %>
+The common Rails commands available for engines are:
+ generate Generate new code (short-cut alias: "g")
+ destroy Undo code generated with "generate" (short-cut alias: "d")
+ test Run tests (short-cut alias: "t")
+
+All commands can be run with -h for more information.
+
+If you want to run any commands that need to be run in context
+of the application, like `bin/rails server` or `bin/rails console`,
+you should do it from the application's directory (typically test/dummy).
+<% else %>
+The most common rails commands are:
+ generate Generate new code (short-cut alias: "g")
+ console Start the Rails console (short-cut alias: "c")
+ 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.
+<% end %>
+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..5bcc4c8eee
--- /dev/null
+++ b/railties/lib/rails/commands/help/help_command.rb
@@ -0,0 +1,13 @@
+module Rails
+ module Command
+ class HelpCommand < Base
+ hide_command!
+
+ def help(*)
+ puts self.class.desc
+
+ Rails::Command.print_commands
+ 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..13eedfc479
--- /dev/null
+++ b/railties/lib/rails/commands/new/new_command.rb
@@ -0,0 +1,15 @@
+module Rails
+ module Command
+ class NewCommand < Base
+ def help
+ Rails::Command.invoke :application, [ "--help" ]
+ end
+
+ def perform(*)
+ 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
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb
deleted file mode 100644
index 60653a2cee..0000000000
--- a/railties/lib/rails/commands/plugin.rb
+++ /dev/null
@@ -1,24 +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..d6d9fe4400
--- /dev/null
+++ b/railties/lib/rails/commands/plugin/plugin_command.rb
@@ -0,0 +1,43 @@
+module Rails
+ module Command
+ class PluginCommand < Base
+ hide_command!
+
+ def help
+ run_plugin_generator %w( --help )
+ end
+
+ def self.banner(*) # :nodoc:
+ "#{executable} new [options]"
+ end
+
+ class_option :rc, type: :boolean, 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)
+ puts "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..a43c884170
--- /dev/null
+++ b/railties/lib/rails/commands/rake/rake_command.rb
@@ -0,0 +1,51 @@
+module Rails
+ module Command
+ class RakeCommand < Base
+ 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)
+
+ ActiveSupport::Deprecation.silence do
+ require_application_and_environment!
+ end
+
+ 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 f8da71831a..0000000000
--- a/railties/lib/rails/commands/rake_proxy.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-require "active_support"
-
-module Rails
- module RakeProxy #:nodoc:
- private
- def run_rake_task(command)
- require_rake
-
- 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
- require_rake
-
- 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")
- 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
diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb
deleted file mode 100644
index b74addf587..0000000000
--- a/railties/lib/rails/commands/runner.rb
+++ /dev/null
@@ -1,71 +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 => e
- $stderr.puts "Please specify a valid ruby command or the path of a script to run."
- $stderr.puts "Run '#{command} -h' for help."
- $stderr.puts
- $stderr.puts e
- 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..dc47a35ff3
--- /dev/null
+++ b/railties/lib/rails/commands/runner/USAGE
@@ -0,0 +1,17 @@
+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
+
+<% if RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ %>
+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..8db6da8759
--- /dev/null
+++ b/railties/lib/rails/commands/runner/runner_command.rb
@@ -0,0 +1,45 @@
+module Rails
+ module Command
+ class RunnerCommand < Base
+ class_option :environment, aliases: "-e", type: :string,
+ default: Rails::Command.environment.dup,
+ desc: "The environment for the runner to operate under (test/development/production)"
+
+ def help
+ super
+ puts self.class.desc
+ end
+
+ def self.banner(*)
+ "#{super} [<'Some.ruby(code)'> | <filename.rb>]"
+ end
+
+ def perform(code_or_file = nil)
+ unless code_or_file
+ help
+ exit 1
+ end
+
+ ENV["RAILS_ENV"] = options[:environment]
+
+ require_application_and_environment!
+ Rails.application.load_runner
+
+ if 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 => error
+ $stderr.puts "Please specify a valid ruby command or the path of a script to run."
+ $stderr.puts "Run '#{self.class.executable} -h' for help."
+ $stderr.puts
+ $stderr.puts error
+ exit 1
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server/server_command.rb
index 1eabf3fef3..14cf72f483 100644
--- a/railties/lib/rails/commands/server.rb
+++ b/railties/lib/rails/commands/server/server_command.rb
@@ -19,33 +19,35 @@ module Rails
options
end
- private
-
- def option_parser(options)
- OptionParser.new do |opts|
- opts.banner = "Usage: rails server [puma, thin etc] [options]"
- opts.on("-p", "--port=port", Integer,
- "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v }
- 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
+ def option_parser(options) # :nodoc:
+ OptionParser.new do |opts|
+ opts.banner = "Usage: rails server [puma, thin etc] [options]"
+
+ opts.separator ""
+ opts.separator "Options:"
+
+ opts.on("-p", "--port=port", Integer,
+ "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v }
+ opts.on("-b", "--binding=IP", String,
+ "Binds Rails to the specified IP.", "Default: localhost") { |v| options[:Host] = v }
+ opts.on("-c", "--config=file", String,
+ "Uses a custom rackup configuration.") { |v| options[:config] = v }
+ opts.on("-d", "--daemon", "Runs server as a Daemon.") { options[:daemonize] = true }
+ opts.on("-e", "--environment=name", String,
+ "Specifies the environment to run this server under (test/development/production).",
+ "Default: development") { |v| options[:environment] = v }
+ opts.on("-P", "--pid=pid", String,
+ "Specifies the PID file.",
+ "Default: tmp/pids/server.pid") { |v| options[:pid] = v }
+ opts.on("-C", "--[no-]dev-caching",
+ "Specifies whether to perform caching in development.",
+ "true or false") { |v| options[:caching] = v }
+
+ opts.separator ""
+
+ opts.on("-h", "--help", "Shows this help message.") { puts opts; exit }
end
+ end
end
def initialize(*)
@@ -100,7 +102,6 @@ module Rails
end
private
-
def setup_dev_caching
if options[:environment] == "development"
Rails::DevCaching.enable_by_argument(options[:caching])
@@ -136,4 +137,24 @@ module Rails
"bin/rails server #{ARGV.join(' ')}"
end
end
+
+ module Command
+ class ServerCommand < Base
+ def help # :nodoc:
+ puts Rails::Server::Options.new.option_parser(Hash.new)
+ end
+
+ 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
diff --git a/railties/lib/rails/commands/test.rb b/railties/lib/rails/commands/test.rb
deleted file mode 100644
index 219c2fa4e0..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..1b2e3af9cc
--- /dev/null
+++ b/railties/lib/rails/commands/test/test_command.rb
@@ -0,0 +1,20 @@
+require "rails/command"
+require "rails/test_unit/minitest_plugin"
+
+module Rails
+ module Command
+ class TestCommand < Base
+ def help # :nodoc:
+ perform # Hand over help printing to minitest.
+ end
+
+ def perform(*)
+ $LOAD_PATH << Rails::Command.root.join("test")
+
+ Minitest.run_via[:rails] = true
+
+ require "active_support/testing/autorun"
+ 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..4f3fbfca1b
--- /dev/null
+++ b/railties/lib/rails/commands/version/version_command.rb
@@ -0,0 +1,9 @@
+module Rails
+ module Command
+ class VersionCommand < Base
+ def perform
+ Rails::Command.invoke :application, [ "--version" ]
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index 86d66afddb..90e7c04d7c 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -337,7 +337,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
diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb
index dfbeea36b8..a23ae44b0b 100644
--- a/railties/lib/rails/engine/commands.rb
+++ b/railties/lib/rails/engine/commands.rb
@@ -1,6 +1,4 @@
-require "rails/engine/commands_tasks"
-
-ARGV << "--help" if ARGV.empty?
+require "rails/command"
aliases = {
"g" => "generate",
@@ -11,4 +9,4 @@ aliases = {
command = ARGV.shift
command = aliases[command] || command
-Rails::Engine::CommandsTasks.new(ARGV).run_command!(command)
+Rails::Command.invoke command, ARGV
diff --git a/railties/lib/rails/engine/commands_tasks.rb b/railties/lib/rails/engine/commands_tasks.rb
deleted file mode 100644
index d6effdb732..0000000000
--- a/railties/lib/rails/engine/commands_tasks.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-require "rails/commands/rake_proxy"
-require "rails/commands/common_commands_tasks"
-require "active_support/core_ext/string/strip"
-
-module Rails
- class Engine
- class CommandsTasks # :nodoc:
- include Rails::RakeProxy
- include Rails::CommonCommandsTasks
-
- attr_reader :argv
-
- def initialize(argv)
- @argv = argv
- end
-
- private
-
- def commands
- formatted_rake_tasks
- end
-
- def command_whitelist
- %w(generate destroy version help test)
- end
-
- def help_message
- <<-EOT.strip_heredoc
- 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
- end
-
- def require_application_and_environment!
- require ENGINE_PATH
- end
-
- 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
- end
- end
-end
diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb
index 6a112fc710..dd16b44786 100644
--- a/railties/lib/rails/generators.rb
+++ b/railties/lib/rails/generators.rb
@@ -2,6 +2,7 @@ activesupport_path = File.expand_path("../../../../activesupport/lib", __FILE__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
require "thor/group"
+require "rails/command"
require "active_support"
require "active_support/core_ext/object/blank"
@@ -13,6 +14,8 @@ require "active_support/core_ext/string/inflections"
module Rails
module Generators
+ include Rails::Command::Behavior
+
autoload :Actions, "rails/generators/actions"
autoload :ActiveModel, "rails/generators/active_model"
autoload :Base, "rails/generators/base"
@@ -127,67 +130,6 @@ module Rails
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}"
- end
-
- lookup(lookups)
-
- namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }]
-
- lookups.each do |namespace|
- klass = namespaces[namespace]
- return klass if klass
- end
-
- invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name)
- 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
- 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
@@ -260,11 +202,13 @@ module Rails
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
end
+
rails = groups.delete("rails")
rails.map! { |n| n.sub(/^rails:/, "") }
rails.delete("app")
@@ -272,62 +216,69 @@ module Rails
hidden_namespaces.each { |n| groups.delete(n.to_s) }
- [["rails", rails]] + groups.sort.to_a
+ [[ "rails", rails ]] + groups.sort.to_a
end
- protected
+ # 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
- # 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
-
- d[m] = x
+ unless base || context
+ unless name.to_s.include?(?:)
+ lookups << "#{name}:#{name}"
+ lookups << "rails:#{name}"
end
-
- x
+ lookups << "#{name}"
end
- # Prints a list of generators.
- def self.print_list(base, namespaces) #:nodoc:
- namespaces = namespaces.reject do |n|
- hidden_namespaces.include?(n)
- end
+ lookup(lookups)
+
+ namespaces = Hash[subclasses.map { |klass| [klass.namespace, klass] }]
+ lookups.each do |namespace|
- return if namespaces.empty?
- puts "#{base.camelize}:"
+ klass = namespaces[namespace]
+ return klass if klass
+ end
- namespaces.each do |namespace|
- puts(" #{namespace}")
- end
+ invoke_fallbacks_for(name, base) || invoke_fallbacks_for(context, name)
+ end
- puts
+ # 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
+ end
+ end
+
+ protected
+ def self.print_list(base, namespaces)
+ namespaces = namespaces.reject { |n| hidden_namespaces.include?(n) }
+ super
end
# Try fallbacks for the given base.
@@ -346,53 +297,16 @@ module Rails
nil
end
- # 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
- end
- end
+ def self.command_type
+ @command_type ||= "generator"
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
- end
+ def self.lookup_paths
+ @lookup_paths ||= %w( rails/generators generators )
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("/")
- end
- paths.uniq!
- paths
+ def self.file_lookup_paths
+ @file_lookup_paths ||= [ "{#{lookup_paths.join(',')}}", "**", "*_generator.rb" ]
end
end
end
diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb
index d76acbdafc..91342c592c 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -243,7 +243,7 @@ module Rails
] + dev_edge_common
elsif options.edge?
[
- GemfileEntry.github("rails", "rails/rails", "5-0-stable")
+ GemfileEntry.github("rails", "rails/rails")
] + dev_edge_common
else
[GemfileEntry.version("rails",
diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb b/railties/lib/rails/generators/rails/app/templates/config/puma.rb
index 121ad7080e..7ee948002e 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/puma.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/puma.rb
@@ -32,6 +32,14 @@ environment ENV.fetch("RAILS_ENV") { "development" }
#
# preload_app!
+# If you are preloading your application and using Active Record, it's
+# recommended that you close any connections to the database before workers
+# are forked to prevent connection leakage.
+#
+# before_fork do
+# ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
+# end
+
# The code in the `on_worker_boot` will be called if you are using
# clustered mode by specifying a number of `workers`. After each worker
# process is booted this block will be run, if you are using `preload_app!`
@@ -42,6 +50,7 @@ environment ENV.fetch("RAILS_ENV") { "development" }
# on_worker_boot do
# ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
# end
+#
# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart
diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
index 87b8fe3516..2f92168eef 100644
--- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
+++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
@@ -1,4 +1,3 @@
-ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
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..c0fbb84a93 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt
@@ -5,4 +5,6 @@ require 'rails/test_unit/minitest_plugin'
Rails::TestUnitReporter.executable = 'bin/test'
-exit Minitest.run(ARGV)
+Minitest.run_via[:rails] = true
+
+require "active_support/testing/autorun"
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
index a5eebcb19f..e84e403018 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb
@@ -1,6 +1,3 @@
-# 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__)]
diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb
index 88ec2ba85b..1c1810dde6 100644
--- a/railties/lib/rails/paths.rb
+++ b/railties/lib/rails/paths.rb
@@ -30,7 +30,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:
#
diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb
index e15c6b3a38..6e196a32ab 100644
--- a/railties/lib/rails/test_unit/minitest_plugin.rb
+++ b/railties/lib/rails/test_unit/minitest_plugin.rb
@@ -61,19 +61,30 @@ module Minitest
# 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
+ autorun
end
+ module RunRespectingRakeTestopts
+ def run(args = [])
+ if defined?(@rake_patterns)
+ args = Shellwords.split(ENV["TESTOPTS"] || "")
+ end
+
+ super
+ end
+ end
+
+ singleton_class.prepend RunRespectingRakeTestopts
+
# 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"
- ::Rails::TestRequirer.require_files(options[:patterns]) unless run_with_autorun
+ # If run via `ruby` we've been passed the files to run directly.
+ unless run_via[:ruby]
+ ::Rails::TestRequirer.require_files(options[:patterns])
+ end
unless options[:full_backtrace] || ENV["BACKTRACE"]
# Plugin can run without Rails loaded, check before filtering.
@@ -86,8 +97,7 @@ module Minitest
reporter << ::Rails::TestUnitReporter.new(options[:io], options)
end
- mattr_accessor(:run_with_autorun) { false }
- mattr_accessor(:run_with_rails_extension) { false }
+ mattr_accessor(:run_via) { Hash.new }
end
# Put Rails as the first plugin minitest initializes so other plugins
diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb
index 2f076ea2f6..0bbd25db2b 100644
--- a/railties/test/application/bin_setup_test.rb
+++ b/railties/test/application/bin_setup_test.rb
@@ -6,10 +6,17 @@ module ApplicationTests
def setup
build_app
+
+ create_gemfile
+ update_boot_file_to_use_bundler
+ @old_gemfile_env = ENV["BUNDLE_GEMFILE"]
+ ENV["BUNDLE_GEMFILE"] = app_path + "/Gemfile"
end
def teardown
teardown_app
+
+ ENV["BUNDLE_GEMFILE"] = @old_gemfile_env
end
def test_bin_setup
@@ -52,5 +59,16 @@ Created database 'db/test.sqlite3'
OUTPUT
end
end
+
+ private
+ def create_gemfile
+ app_file("Gemfile", "source 'https://rubygems.org'")
+ app_file("Gemfile", "gem 'rails', path: '#{RAILS_FRAMEWORK_ROOT}'", "a")
+ app_file("Gemfile", "gem 'sqlite3'", "a")
+ end
+
+ def update_boot_file_to_use_bundler
+ app_file("config/boot.rb", "require 'bundler/setup'")
+ end
end
end
diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb
index 3b8062f74b..72f340df34 100644
--- a/railties/test/application/console_test.rb
+++ b/railties/test/application/console_test.rb
@@ -126,7 +126,7 @@ class FullStackConsoleTest < ActiveSupport::TestCase
end
end
- assert output.include?(expected), "#{expected.inspect} expected, but got:\n\n#{output}"
+ assert_includes output, expected, "#{expected.inspect} expected, but got:\n\n#{output}"
end
def write_prompt(command, expected_output = nil)
diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb
index d4008fe677..0153f94504 100644
--- a/railties/test/application/generators_test.rb
+++ b/railties/test/application/generators_test.rb
@@ -134,10 +134,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]
diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb
index 0ddaf346e0..206e42703b 100644
--- a/railties/test/application/initializers/i18n_test.rb
+++ b/railties/test/application/initializers/i18n_test.rb
@@ -30,7 +30,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
@@ -62,8 +62,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
@@ -214,7 +214,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
@@ -222,7 +222,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
diff --git a/railties/test/application/initializers/load_path_test.rb b/railties/test/application/initializers/load_path_test.rb
index caab7c0c8a..dbefb22837 100644
--- a/railties/test/application/initializers/load_path_test.rb
+++ b/railties/test/application/initializers/load_path_test.rb
@@ -19,7 +19,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
@@ -36,7 +36,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/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb
index 2873425703..a6019a9db4 100644
--- a/railties/test/application/middleware/session_test.rb
+++ b/railties/test/application/middleware/session_test.rb
@@ -370,7 +370,7 @@ module ApplicationTests
assert_equal 200, last_response.status
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
diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb
index 1cc931b29e..45481dc1b6 100644
--- a/railties/test/application/middleware_test.rb
+++ b/railties/test/application/middleware_test.rb
@@ -73,7 +73,7 @@ module ApplicationTests
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
@@ -81,7 +81,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
@@ -89,13 +89,13 @@ 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
@@ -109,7 +109,7 @@ module ApplicationTests
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
@@ -139,20 +139,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
@@ -160,21 +160,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
diff --git a/railties/test/application/paths_test.rb b/railties/test/application/paths_test.rb
index 8eb1d42b33..515205296c 100644
--- a/railties/test/application/paths_test.rb
+++ b/railties/test/application/paths_test.rb
@@ -55,9 +55,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/rake/restart_test.rb b/railties/test/application/rake/restart_test.rb
index 28da8164a7..6ebd2d5461 100644
--- a/railties/test/application/rake/restart_test.rb
+++ b/railties/test/application/rake/restart_test.rb
@@ -13,32 +13,32 @@ module ApplicationTests
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`
+ `bin/rails restart`
assert File.exist?("tmp/restart.txt")
prev_mtime = File.mtime("tmp/restart.txt")
sleep(1)
- `rake restart`
+ `bin/rails restart`
curr_mtime = File.mtime("tmp/restart.txt")
assert_not_equal prev_mtime, curr_mtime
end
end
- test "rake restart should work even if tmp folder does not exist" do
+ test "rails restart should work even if tmp folder does not exist" do
Dir.chdir(app_path) do
FileUtils.remove_dir("tmp")
- `rake restart`
+ `bin/rails restart`
assert File.exist?("tmp/restart.txt")
end
end
- test "rake restart removes server.pid also" do
+ test "rails restart removes server.pid also" do
Dir.chdir(app_path) do
FileUtils.mkdir_p("tmp/pids")
FileUtils.touch("tmp/pids/server.pid")
- `rake restart`
+ `bin/rails restart`
assert_not File.exist?("tmp/pids/server.pid")
end
end
diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb
index 4dc766dfda..77e7a2cca5 100644
--- a/railties/test/application/runner_test.rb
+++ b/railties/test/application/runner_test.rb
@@ -82,7 +82,7 @@ module ApplicationTests
def test_runner_detects_bad_script_name
output = Dir.chdir(app_path) { `bin/rails runner "iuiqwiourowe" 2>&1` }
assert_not $?.success?
- assert_match "undefined local variable or method `iuiqwiourowe' for main:Object", output
+ assert_match "undefined local variable or method `iuiqwiourowe' for", output
end
def test_environment_with_rails_env
diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb
index ef65c7a893..32d2a6857c 100644
--- a/railties/test/application/test_test.rb
+++ b/railties/test/application/test_test.rb
@@ -12,7 +12,7 @@ module ApplicationTests
teardown_app
end
- test "truth" do
+ test "simple successful test" do
app_file "test/unit/foo_test.rb", <<-RUBY
require 'test_helper'
@@ -26,6 +26,38 @@ module ApplicationTests
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", "Failed assertion"
+ end
+
test "integration test" do
controller "posts", <<-RUBY
class PostsController < ActionController::Base
@@ -101,7 +133,7 @@ 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)")
+ assert_not_includes result, "create_table(:users)"
end
test "sql structure migrations" do
@@ -289,7 +321,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
diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb
index 96ac7c0661..4fc082e4ca 100644
--- a/railties/test/commands/console_test.rb
+++ b/railties/test/commands/console_test.rb
@@ -1,6 +1,7 @@
require "abstract_unit"
require "env_helpers"
-require "rails/commands/console"
+require "rails/command"
+require "rails/commands/console/console_command"
class Rails::ConsoleTest < ActiveSupport::TestCase
include EnvHelpers
@@ -102,13 +103,21 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
end
def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present
- stubbed_console = Class.new(Rails::Console) do
- def available_environments
+ Rails::Command::ConsoleCommand.class_eval do
+ alias_method :old_environments, :available_environments
+
+ define_method :available_environments do
["dev"]
end
end
- options = stubbed_console.parse_arguments(["dev"])
- assert_match("dev", options[:environment])
+
+ assert_match("dev", parse_arguments(["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
@@ -148,6 +157,21 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
end
def parse_arguments(args)
- Rails::Console.parse_arguments(args)
+ Rails::Command::ConsoleCommand.class_eval do
+ alias_method :old_perform, :perform
+ define_method(:perform) do
+ extract_environment_option_from_argument
+
+ options
+ end
+ end
+
+ Rails::Command.invoke(:console, args)
+ ensure
+ Rails::Command::ConsoleCommand.class_eval do
+ undef_method :perform
+ alias_method :perform, :old_perform
+ undef_method :old_perform
+ end
end
end
diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb
index 286e7c16c1..2ddb269eae 100644
--- a/railties/test/commands/dbconsole_test.rb
+++ b/railties/test/commands/dbconsole_test.rb
@@ -1,6 +1,7 @@
require "abstract_unit"
require "minitest/mock"
-require "rails/commands/dbconsole"
+require "rails/command"
+require "rails/commands/dbconsole/dbconsole_command"
class Rails::DBConsoleTest < ActiveSupport::TestCase
def setup
@@ -97,16 +98,14 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
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])
+ stub_available_environments([ "development", "test" ]) do
+ assert_match("development", parse_arguments([ "dev" ])[:environment])
end
end
def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present
- Rails::DBConsole.stub(:available_environments, ["dev"]) do
- options = Rails::DBConsole.send(:parse_arguments, ["dev"])
- assert_match("dev", options[:environment])
+ stub_available_environments([ "dev" ]) do
+ assert_match("dev", parse_arguments([ "dev" ])[:environment])
end
end
@@ -203,20 +202,16 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
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(/bin\/rails dbconsole \[environment\]/, stdout)
end
def test_print_help_long
stdout = capture(:stdout) do
- start({}, ["--help"])
+ Rails::Command.invoke(:dbconsole, ["--help"])
end
- assert aborted
- assert_equal "", output
- assert_match(/Usage:.*dbconsole/, stdout)
+ assert_match(/bin\/rails dbconsole \[environment\]/, stdout)
end
attr_reader :aborted, :output
@@ -230,21 +225,22 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
end
end
- def dbconsole
- @dbconsole ||= Class.new(Rails::DBConsole) do
+ 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
end
- end.new(nil)
+ end
end
+ attr_reader :dbconsole
+
def start(config = {}, argv = [])
- dbconsole.stub(:config, config.stringify_keys) do
- dbconsole.stub(:arguments, argv) do
- capture_abort { dbconsole.start }
- end
+ @dbconsole = make_dbconsole.new(parse_arguments(argv))
+ @dbconsole.stub(:config, config.stringify_keys) do
+ capture_abort { @dbconsole.start }
end
end
@@ -258,4 +254,41 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
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/server_test.rb b/railties/test/commands/server_test.rb
index c1ec73d95c..391886bf33 100644
--- a/railties/test/commands/server_test.rb
+++ b/railties/test/commands/server_test.rb
@@ -1,6 +1,7 @@
require "abstract_unit"
require "env_helpers"
-require "rails/commands/server"
+require "rails/command"
+require "rails/commands/server/server_command"
class Rails::ServerTest < ActiveSupport::TestCase
include EnvHelpers
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index acab83799d..8f7fa1155f 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -606,7 +606,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_edge_option
assert_generates_with_bundler edge: true
- assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["'],\s+branch:\s+["']#{Regexp.escape("5-0-stable")}["']$}
+ assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["']$}
end
def test_spring
diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb
index c58468dfc1..0258b3b9d7 100644
--- a/railties/test/generators/named_base_test.rb
+++ b/railties/test/generators/named_base_test.rb
@@ -106,9 +106,9 @@ class NamedBaseTest < Rails::Generators::TestCase
def test_hide_namespace
g = generator ["Hidden"]
g.class.stub(:namespace, "hidden") do
- assert !Rails::Generators.hidden_namespaces.include?("hidden")
+ 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
diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb
index 0d8e6f8907..15079f2735 100644
--- a/railties/test/generators/plugin_generator_test.rb
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -205,7 +205,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
def test_edge_option
assert_generates_without_bundler(edge: true)
- assert_file "Gemfile", %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["'],\s+branch:\s+["']#{Regexp.escape("5-0-stable")}["']$}
+ 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
diff --git a/railties/test/generators/plugin_test_runner_test.rb b/railties/test/generators/plugin_test_runner_test.rb
index 04b4b10254..7a10a2afa9 100644
--- a/railties/test/generators/plugin_test_runner_test.rb
+++ b/railties/test/generators/plugin_test_runner_test.rb
@@ -86,6 +86,12 @@ class PluginTestRunnerTest < ActiveSupport::TestCase
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
+
private
def plugin_path
"#{@destination_root}/bukkits"
diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb
index 3c22881080..68ba435393 100644
--- a/railties/test/generators_test.rb
+++ b/railties/test/generators_test.rb
@@ -229,7 +229,7 @@ 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
@@ -239,8 +239,8 @@ class GeneratorsTest < Rails::Generators::TestCase
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/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index b9b11504dc..8c1fe43a10 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -117,11 +117,6 @@ module TestHelpers
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|
diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb
index a4f6922cb4..7b2551062a 100644
--- a/railties/test/paths_test.rb
+++ b/railties/test/paths_test.rb
@@ -103,7 +103,7 @@ class PathsTest < ActiveSupport::TestCase
@root.add "app", with: "/app"
@root["app"].autoload_once!
assert @root["app"].autoload_once?
- assert @root.autoload_once.include?(@root["app"].expanded.first)
+ assert_includes @root.autoload_once, @root["app"].expanded.first
end
end
@@ -114,14 +114,14 @@ class PathsTest < ActiveSupport::TestCase
@root["app"].skip_autoload_once!
assert !@root["app"].autoload_once?
- assert !@root.autoload_once.include?(@root["app"].expanded.first)
+ 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_includes @root.autoload_once, "/app"
end
end
@@ -129,8 +129,8 @@ class PathsTest < ActiveSupport::TestCase
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_includes @root.autoload_once, "/app"
+ assert_includes @root.autoload_once, "/app2"
end
end
@@ -157,7 +157,7 @@ class PathsTest < ActiveSupport::TestCase
@root["app"] = "/app"
@root["app"].eager_load!
assert @root["app"].eager_load?
- assert @root.eager_load.include?(@root["app"].to_a.first)
+ assert_includes @root.eager_load, @root["app"].to_a.first
end
end
@@ -168,14 +168,14 @@ class PathsTest < ActiveSupport::TestCase
@root["app"].skip_eager_load!
assert !@root["app"].eager_load?
- assert !@root.eager_load.include?(@root["app"].to_a.first)
+ 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_includes @root.eager_load, "/app"
end
end
@@ -183,8 +183,8 @@ class PathsTest < ActiveSupport::TestCase
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_includes @root.eager_load, "/app"
+ assert_includes @root.eager_load, "/app2"
end
end
@@ -193,8 +193,8 @@ class PathsTest < ActiveSupport::TestCase
@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_includes @root.eager_load, "/app"
+ assert_includes @root.autoload_once, "/app"
end
end
diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb
index fc4175719e..59e79de41a 100644
--- a/railties/test/rails_info_test.rb
+++ b/railties/test/rails_info_test.rb
@@ -48,9 +48,9 @@ class InfoTest < ActiveSupport::TestCase
end
html = Rails::Info.to_html
- assert html.include?('<tr><td class="name">Middleware</td>')
+ assert_includes html, '<tr><td class="name">Middleware</td>'
properties.value_for("Middleware").each do |value|
- assert html.include?("<li>#{CGI.escapeHTML(value)}</li>")
+ assert_includes html, "<li>#{CGI.escapeHTML(value)}</li>"
end
end
diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb
index 755ac23cb1..30cd525266 100644
--- a/railties/test/railties/railtie_test.rb
+++ b/railties/test/railties/railtie_test.rb
@@ -135,7 +135,7 @@ module RailtiesTest
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