aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml5
-rw-r--r--Gemfile10
-rw-r--r--Gemfile.lock37
-rw-r--r--README.md2
-rw-r--r--actionmailer/test/base_test.rb7
-rw-r--r--actionmailer/test/delivery_methods_test.rb6
-rw-r--r--actionmailer/test/mailers/base_mailer.rb6
-rw-r--r--actionpack/CHANGELOG.md18
-rw-r--r--actionpack/lib/abstract_controller/collector.rb6
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb2
-rw-r--r--actionpack/lib/action_controller/metal.rb3
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb16
-rw-r--r--actionpack/lib/action_controller/metal/live.rb31
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb6
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb4
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb14
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb2
-rw-r--r--actionpack/lib/action_controller/metal/testing.rb6
-rw-r--r--actionpack/lib/action_controller/test_case.rb25
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb5
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb20
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb130
-rw-r--r--actionpack/lib/action_dispatch/http/mime_types.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb35
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb11
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb55
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb25
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb77
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb41
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb6
-rw-r--r--actionpack/lib/action_dispatch/routing.rb1
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/test_response.rb2
-rw-r--r--actionpack/test/abstract/collector_test.rb6
-rw-r--r--actionpack/test/abstract_unit.rb1
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb2
-rw-r--r--actionpack/test/controller/content_type_test.rb44
-rw-r--r--actionpack/test/controller/live_stream_test.rb92
-rw-r--r--actionpack/test/controller/new_base/content_type_test.rb4
-rw-r--r--actionpack/test/controller/redirect_test.rb6
-rw-r--r--actionpack/test/controller/render_other_test.rb2
-rw-r--r--actionpack/test/controller/render_xml_test.rb2
-rw-r--r--actionpack/test/controller/send_file_test.rb104
-rw-r--r--actionpack/test/controller/test_case_test.rb11
-rw-r--r--actionpack/test/controller/webservice_test.rb22
-rw-r--r--actionpack/test/dispatch/cookies_test.rb167
-rw-r--r--actionpack/test/dispatch/header_test.rb18
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb97
-rw-r--r--actionpack/test/dispatch/request_test.rb48
-rw-r--r--actionpack/test/dispatch/response_test.rb94
-rw-r--r--actionpack/test/journey/router_test.rb11
-rw-r--r--actionview/CHANGELOG.md5
-rw-r--r--actionview/lib/action_view/dependency_tracker.rb4
-rw-r--r--actionview/lib/action_view/digestor.rb6
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb15
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb22
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/number_helper.rb3
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_check_boxes.rb25
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_helpers.rb26
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb10
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb9
-rw-r--r--actionview/lib/action_view/layouts.rb2
-rw-r--r--actionview/lib/action_view/lookup_context.rb8
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb6
-rw-r--r--actionview/lib/action_view/template/resolver.rb4
-rw-r--r--actionview/test/abstract_unit.rb1
-rw-r--r--actionview/test/actionpack/controller/capture_test.rb2
-rw-r--r--actionview/test/actionpack/controller/render_test.rb2
-rw-r--r--actionview/test/template/dependency_tracker_test.rb1
-rw-r--r--actionview/test/template/form_collections_helper_test.rb42
-rw-r--r--actionview/test/template/form_helper_test.rb36
-rw-r--r--actionview/test/template/form_tag_helper_test.rb34
-rw-r--r--actionview/test/template/lookup_context_test.rb6
-rw-r--r--actionview/test/template/render_test.rb10
-rw-r--r--actionview/test/template/translation_helper_test.rb6
-rw-r--r--activejob/CHANGELOG.md8
-rw-r--r--activejob/lib/active_job/async_job.rb3
-rw-r--r--activejob/lib/active_job/base.rb2
-rw-r--r--activejob/lib/active_job/core.rb8
-rw-r--r--activejob/lib/active_job/enqueuing.rb4
-rw-r--r--activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb4
-rw-r--r--activejob/lib/active_job/queue_adapters/que_adapter.rb4
-rw-r--r--activejob/lib/active_job/queue_priority.rb44
-rw-r--r--activejob/lib/active_job/test_helper.rb12
-rw-r--r--activejob/test/cases/queue_priority_test.rb47
-rw-r--r--activejob/test/cases/test_case_test.rb2
-rw-r--r--activejob/test/cases/test_helper_test.rb18
-rw-r--r--activejob/test/helper.rb1
-rw-r--r--activejob/test/integration/queuing_test.rb12
-rw-r--r--activejob/test/support/integration/adapters/sidekiq.rb2
-rw-r--r--activejob/test/support/integration/dummy_app_template.rb3
-rw-r--r--activejob/test/support/integration/helper.rb2
-rw-r--r--activejob/test/support/integration/test_case_helpers.rb8
-rw-r--r--activejob/test/support/que/inline.rb1
-rw-r--r--activemodel/CHANGELOG.md6
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb4
-rw-r--r--activemodel/lib/active_model/callbacks.rb1
-rw-r--r--activemodel/lib/active_model/dirty.rb4
-rw-r--r--activemodel/lib/active_model/forbidden_attributes_protection.rb5
-rw-r--r--activemodel/lib/active_model/naming.rb2
-rw-r--r--activemodel/lib/active_model/serialization.rb6
-rw-r--r--activemodel/lib/active_model/type.rb57
-rw-r--r--activemodel/lib/active_model/type/big_integer.rb (renamed from activerecord/lib/active_record/type/big_integer.rb)4
-rw-r--r--activemodel/lib/active_model/type/binary.rb (renamed from activerecord/lib/active_record/type/binary.rb)2
-rw-r--r--activemodel/lib/active_model/type/boolean.rb (renamed from activerecord/lib/active_record/type/boolean.rb)6
-rw-r--r--activemodel/lib/active_model/type/date.rb50
-rw-r--r--activemodel/lib/active_model/type/date_time.rb44
-rw-r--r--activemodel/lib/active_model/type/decimal.rb (renamed from activerecord/lib/active_record/type/decimal.rb)4
-rw-r--r--activemodel/lib/active_model/type/decimal_without_scale.rb (renamed from activerecord/lib/active_record/type/decimal_without_scale.rb)4
-rw-r--r--activemodel/lib/active_model/type/float.rb (renamed from activerecord/lib/active_record/type/float.rb)2
-rw-r--r--activemodel/lib/active_model/type/helpers.rb4
-rw-r--r--activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb (renamed from activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb)15
-rw-r--r--activemodel/lib/active_model/type/helpers/mutable.rb (renamed from activerecord/lib/active_record/type/helpers/mutable.rb)2
-rw-r--r--activemodel/lib/active_model/type/helpers/numeric.rb (renamed from activerecord/lib/active_record/type/helpers/numeric.rb)2
-rw-r--r--activemodel/lib/active_model/type/helpers/time_value.rb (renamed from activerecord/lib/active_record/type/helpers/time_value.rb)39
-rw-r--r--activemodel/lib/active_model/type/integer.rb (renamed from activerecord/lib/active_record/type/integer.rb)2
-rw-r--r--activemodel/lib/active_model/type/registry.rb64
-rw-r--r--activemodel/lib/active_model/type/string.rb (renamed from activerecord/lib/active_record/type/string.rb)2
-rw-r--r--activemodel/lib/active_model/type/text.rb (renamed from activerecord/lib/active_record/type/text.rb)4
-rw-r--r--activemodel/lib/active_model/type/time.rb42
-rw-r--r--activemodel/lib/active_model/type/unsigned_integer.rb (renamed from activerecord/lib/active_record/type/unsigned_integer.rb)2
-rw-r--r--activemodel/lib/active_model/type/value.rb (renamed from activerecord/lib/active_record/type/value.rb)5
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb55
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb1
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb4
-rw-r--r--activemodel/test/cases/attribute_assignment_test.rb2
-rw-r--r--activemodel/test/cases/type/decimal_test.rb (renamed from activerecord/test/cases/type/decimal_test.rb)5
-rw-r--r--activemodel/test/cases/type/integer_test.rb108
-rw-r--r--activemodel/test/cases/type/registry_test.rb39
-rw-r--r--activemodel/test/cases/type/string_test.rb20
-rw-r--r--activemodel/test/cases/type/unsigned_integer_test.rb (renamed from activerecord/test/cases/type/unsigned_integer_test.rb)5
-rw-r--r--activemodel/test/cases/types_test.rb122
-rw-r--r--activerecord/CHANGELOG.md104
-rw-r--r--activerecord/README.rdoc2
-rw-r--r--activerecord/lib/active_record/associations.rb6
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb3
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb14
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb2
-rw-r--r--activerecord/lib/active_record/attribute.rb55
-rw-r--r--activerecord/lib/active_record/attribute/user_provided_default.rb13
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb8
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb127
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb2
-rw-r--r--activerecord/lib/active_record/attribute_mutation_tracker.rb70
-rw-r--r--activerecord/lib/active_record/attribute_set.rb13
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb8
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb40
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb80
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb234
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb78
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb59
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb25
-rw-r--r--activerecord/lib/active_record/connection_adapters/statement_pool.rb5
-rw-r--r--activerecord/lib/active_record/connection_handling.rb2
-rw-r--r--activerecord/lib/active_record/core.rb4
-rw-r--r--activerecord/lib/active_record/enum.rb42
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb23
-rw-r--r--activerecord/lib/active_record/fixtures.rb39
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb4
-rw-r--r--activerecord/lib/active_record/migration.rb2
-rw-r--r--activerecord/lib/active_record/model_schema.rb13
-rw-r--r--activerecord/lib/active_record/persistence.rb1
-rw-r--r--activerecord/lib/active_record/railties/databases.rake4
-rw-r--r--activerecord/lib/active_record/reflection.rb32
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb7
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb14
-rw-r--r--activerecord/lib/active_record/sanitization.rb77
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb21
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb27
-rw-r--r--activerecord/lib/active_record/transactions.rb1
-rw-r--r--activerecord/lib/active_record/type.rb34
-rw-r--r--activerecord/lib/active_record/type/adapter_specific_registry.rb36
-rw-r--r--activerecord/lib/active_record/type/date.rb46
-rw-r--r--activerecord/lib/active_record/type/date_time.rb41
-rw-r--r--activerecord/lib/active_record/type/helpers.rb4
-rw-r--r--activerecord/lib/active_record/type/internal/abstract_json.rb4
-rw-r--r--activerecord/lib/active_record/type/internal/timezone.rb15
-rw-r--r--activerecord/lib/active_record/type/serialized.rb10
-rw-r--r--activerecord/lib/active_record/type/time.rb40
-rw-r--r--activerecord/lib/active_record/type/type_map.rb8
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb2
-rw-r--r--activerecord/test/active_record/connection_adapters/fake_adapter.rb6
-rw-r--r--activerecord/test/cases/adapter_test.rb17
-rw-r--r--activerecord/test/cases/adapters/mysql/active_schema_test.rb18
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/explain_test.rb21
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb17
-rw-r--r--activerecord/test/cases/adapters/mysql/statement_pool_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/unsigned_type_test.rb39
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb18
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_test.rb8
-rw-r--r--activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb39
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb14
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb2
-rw-r--r--activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb2
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb4
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb21
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb4
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb13
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb7
-rw-r--r--activerecord/test/cases/attribute_set_test.rb34
-rw-r--r--activerecord/test/cases/attribute_test.rb59
-rw-r--r--activerecord/test/cases/base_test.rb20
-rw-r--r--activerecord/test/cases/calculations_test.rb32
-rw-r--r--activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb4
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb11
-rw-r--r--activerecord/test/cases/dirty_test.rb21
-rw-r--r--activerecord/test/cases/finder_test.rb12
-rw-r--r--activerecord/test/cases/fixture_set/file_test.rb12
-rw-r--r--activerecord/test/cases/fixtures_test.rb35
-rw-r--r--activerecord/test/cases/helper.rb8
-rw-r--r--activerecord/test/cases/integration_test.rb5
-rw-r--r--activerecord/test/cases/migration/helper.rb2
-rw-r--r--activerecord/test/cases/migration/references_foreign_key_test.rb8
-rw-r--r--activerecord/test/cases/primary_keys_test.rb30
-rw-r--r--activerecord/test/cases/relation/where_test.rb26
-rw-r--r--activerecord/test/cases/relation_test.rb29
-rw-r--r--activerecord/test/cases/sanitize_test.rb8
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb14
-rw-r--r--activerecord/test/cases/test_case.rb2
-rw-r--r--activerecord/test/cases/test_fixtures_test.rb2
-rw-r--r--activerecord/test/cases/timestamp_test.rb20
-rw-r--r--activerecord/test/cases/touch_later_test.rb2
-rw-r--r--activerecord/test/cases/type/date_time_test.rb14
-rw-r--r--activerecord/test/cases/type/integer_test.rb100
-rw-r--r--activerecord/test/cases/type/string_test.rb14
-rw-r--r--activerecord/test/cases/types_test.rb105
-rw-r--r--activerecord/test/cases/validations_test.rb11
-rw-r--r--activerecord/test/cases/view_test.rb37
-rw-r--r--activerecord/test/fixtures/bad_posts.yml9
-rw-r--r--activerecord/test/fixtures/other_comments.yml6
-rw-r--r--activerecord/test/fixtures/other_posts.yml7
-rw-r--r--activerecord/test/models/author.rb3
-rw-r--r--activerecord/test/models/company.rb3
-rw-r--r--activerecord/test/models/contact.rb2
-rw-r--r--activerecord/test/models/developer.rb8
-rw-r--r--activerecord/test/models/parrot.rb6
-rw-r--r--activerecord/test/models/person.rb1
-rw-r--r--activerecord/test/models/project.rb2
-rw-r--r--activerecord/test/schema/schema.rb79
-rw-r--r--activesupport/CHANGELOG.md46
-rw-r--r--activesupport/activesupport.gemspec3
-rw-r--r--activesupport/lib/active_support.rb4
-rw-r--r--activesupport/lib/active_support/callbacks.rb55
-rw-r--r--activesupport/lib/active_support/core_ext/array/conversions.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/date.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date/blank.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/date/calculations.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date/conversions.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/date_time.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/blank.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/conversions.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash/keys.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/hash/transform_values.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/marshal.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/module/attribute_accessors.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/conversions.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/object/blank.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/object/inclusion.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/object/try.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/securerandom.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/string/multibyte.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/string/strip.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/time/conversions.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/time/zones.rb2
-rw-r--r--activesupport/lib/active_support/dependencies.rb4
-rw-r--r--activesupport/lib/active_support/inflector/inflections.rb4
-rw-r--r--activesupport/lib/active_support/inflector/transliterate.rb6
-rw-r--r--activesupport/lib/active_support/key_generator.rb4
-rw-r--r--activesupport/lib/active_support/notifications/fanout.rb6
-rw-r--r--activesupport/lib/active_support/number_helper.rb2
-rw-r--r--activesupport/lib/active_support/testing/assertions.rb4
-rw-r--r--activesupport/lib/active_support/testing/file_fixtures.rb2
-rw-r--r--activesupport/lib/active_support/testing/isolation.rb18
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb16
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb12
-rw-r--r--activesupport/test/callbacks_test.rb24
-rw-r--r--activesupport/test/core_ext/hash/transform_keys_test.rb12
-rw-r--r--activesupport/test/core_ext/hash/transform_values_test.rb12
-rw-r--r--activesupport/test/core_ext/module/attribute_accessor_test.rb14
-rw-r--r--activesupport/test/core_ext/object/blank_test.rb1
-rw-r--r--activesupport/test/core_ext/object/try_test.rb2
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb4
-rw-r--r--activesupport/test/inflector_test_cases.rb1
-rw-r--r--activesupport/test/multibyte_conformance_test.rb1
-rw-r--r--activesupport/test/multibyte_proxy_test.rb1
-rw-r--r--activesupport/test/multibyte_test_helpers.rb1
-rw-r--r--activesupport/test/test_case_test.rb8
-rw-r--r--guides/source/_welcome.html.erb10
-rw-r--r--guides/source/action_mailer_basics.md4
-rw-r--r--guides/source/action_view_overview.md33
-rw-r--r--guides/source/active_record_validations.md2
-rw-r--r--guides/source/active_support_core_extensions.md6
-rw-r--r--guides/source/api_app.md7
-rw-r--r--guides/source/api_documentation_guidelines.md2
-rw-r--r--guides/source/association_basics.md37
-rw-r--r--guides/source/command_line.md2
-rw-r--r--guides/source/configuring.md7
-rw-r--r--guides/source/engines.md24
-rw-r--r--guides/source/i18n.md4
-rw-r--r--guides/source/kindle/layout.html.erb4
-rw-r--r--guides/source/kindle/toc.ncx.erb8
-rw-r--r--guides/source/rails_on_rack.md15
-rw-r--r--guides/source/routing.md4
-rw-r--r--guides/source/upgrading_ruby_on_rails.md25
-rw-r--r--railties/CHANGELOG.md69
-rw-r--r--railties/lib/rails/application.rb22
-rw-r--r--railties/lib/rails/application/configuration.rb1
-rw-r--r--railties/lib/rails/application/default_middleware_stack.rb1
-rw-r--r--railties/lib/rails/configuration.rb19
-rw-r--r--railties/lib/rails/engine.rb34
-rw-r--r--railties/lib/rails/engine/configuration.rb9
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt8
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/manifest.js.tt8
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb2
-rw-r--r--railties/lib/rails/generators/rails/plugin/plugin_generator.rb4
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js6
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js6
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb2
-rw-r--r--railties/lib/rails/mailers_controller.rb2
-rw-r--r--railties/lib/rails/templates/rails/mailers/email.html.erb4
-rw-r--r--railties/lib/rails/test_unit/minitest_plugin.rb15
-rw-r--r--railties/lib/rails/test_unit/reporter.rb38
-rw-r--r--railties/test/application/asset_debugging_test.rb18
-rw-r--r--railties/test/application/assets_test.rb111
-rw-r--r--railties/test/application/configuration_test.rb234
-rw-r--r--railties/test/application/middleware_test.rb2
-rw-r--r--railties/test/application/routing_test.rb12
-rw-r--r--railties/test/application/test_runner_test.rb30
-rw-r--r--railties/test/generators/app_generator_test.rb193
-rw-r--r--railties/test/generators/namespaced_generators_test.rb1
-rw-r--r--railties/test/generators/plugin_generator_test.rb1
-rw-r--r--railties/test/generators/shared_generator_tests.rb48
-rw-r--r--railties/test/isolation/abstract_unit.rb33
-rw-r--r--railties/test/test_unit/reporter_test.rb61
353 files changed, 4613 insertions, 2245 deletions
diff --git a/.travis.yml b/.travis.yml
index c648bd2ca7..5d4a9e9c67 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,11 @@ script: 'ci/travis.rb'
before_install:
- gem install bundler
- "rm ${BUNDLE_GEMFILE}.lock"
+ - curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp
+ - cd /tmp/beanstalkd-1.10/
+ - make
+ - ./beanstalkd &
+ - cd $TRAVIS_BUILD_DIR
before_script:
- bundle update
cache: bundler
diff --git a/Gemfile b/Gemfile
index a9a326c0ff..82a992fe03 100644
--- a/Gemfile
+++ b/Gemfile
@@ -17,12 +17,12 @@ gem 'mocha', '~> 0.14', require: false
gem 'rack-cache', '~> 1.2'
gem 'jquery-rails', github: 'rails/jquery-rails', branch: 'master'
gem 'coffee-rails', '~> 4.1.0'
-gem 'turbolinks'
+gem 'turbolinks', github: 'rails/turbolinks', branch: 'master'
gem 'arel', github: 'rails/arel', branch: 'master'
gem 'mail', github: 'mikel/mail', branch: 'master'
-gem 'sprockets', github: 'rails/sprockets', branch: 'master'
-gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master'
+gem 'sprockets', '~> 4.0', github: 'rails/sprockets', branch: 'master'
+gem 'sprockets-rails', '~> 3.0.0.beta3', github: 'rails/sprockets-rails', branch: 'master'
gem 'sass-rails', github: 'rails/sass-rails', branch: 'master'
# require: false so bcrypt is loaded only when has_secure_password is used.
@@ -33,7 +33,9 @@ gem 'bcrypt', '~> 3.1.10', require: false
# This needs to be with require false to avoid
# it being automatically loaded by sprockets
gem 'uglifier', '>= 1.3.0', require: false
-gem 'sass', '>= 3.3', require: false
+
+# Track stable branch of sass because it doesn't have circular require warnings
+gem 'sass', github: 'sass/sass', branch: 'stable', require: false
group :doc do
gem 'sdoc', '~> 0.4.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index da4614db2b..0bc307cf1e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -29,7 +29,7 @@ GIT
GIT
remote: git://github.com/rack/rack.git
- revision: c28f271d0c91f45e13bfa8f07bed445ef91f41de
+ revision: c617ea99c12a5bfe026e00476ff37e714e01891a
branch: master
specs:
rack (2.0.0.alpha)
@@ -73,10 +73,10 @@ GIT
GIT
remote: git://github.com/rails/sprockets-rails.git
- revision: 600f981fe79ff2d4179baf84bd18fac5acb58b5e
+ revision: 77098c5acd9f27613875097ce6587aff9d871d7f
branch: master
specs:
- sprockets-rails (3.0.0.beta2)
+ sprockets-rails (3.0.0.beta3)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
@@ -89,6 +89,21 @@ GIT
sprockets (4.0.0)
rack (> 1, < 3)
+GIT
+ remote: git://github.com/rails/turbolinks.git
+ revision: 4bb563cd777875d3ad73cf007c26334f2aa8dc37
+ branch: master
+ specs:
+ turbolinks (3.0.0)
+ coffee-rails
+
+GIT
+ remote: git://github.com/sass/sass.git
+ revision: 4ef8e3167985ace91b2105916756bd93c5d7bba6
+ branch: stable
+ specs:
+ sass (3.4.18)
+
PATH
remote: .
specs:
@@ -122,12 +137,11 @@ PATH
activesupport (= 5.0.0.alpha)
arel (= 7.0.0.alpha)
activesupport (5.0.0.alpha)
- concurrent-ruby (~> 0.9.1)
+ concurrent-ruby (~> 1.0.0.pre3, < 2.0.0)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
method_source
minitest (~> 5.1)
- thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
rails (5.0.0.alpha)
actionmailer (= 5.0.0.alpha)
@@ -172,7 +186,7 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.9.1.1)
- concurrent-ruby (0.9.1)
+ concurrent-ruby (1.0.0.pre3)
connection_pool (2.2.0)
dalli (2.7.4)
dante (0.2.0)
@@ -244,7 +258,6 @@ GEM
resque (~> 1.25)
rufus-scheduler (~> 3.0)
rufus-scheduler (3.1.4)
- sass (3.4.18)
sdoc (0.4.1)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
@@ -274,8 +287,6 @@ GEM
thread_safe (0.3.5)
timers (4.0.4)
hitimes
- turbolinks (2.5.3)
- coffee-rails
tzinfo (1.2.2)
thread_safe (~> 0.1)
tzinfo-data (1.2015.6)
@@ -331,18 +342,18 @@ DEPENDENCIES
redcarpet (~> 3.2.3)
resque
resque-scheduler
- sass (>= 3.3)
+ sass!
sass-rails!
sdoc (~> 0.4.0)
sequel
sidekiq
sneakers
- sprockets!
- sprockets-rails!
+ sprockets (~> 4.0)!
+ sprockets-rails (~> 3.0.0.beta3)!
sqlite3 (~> 1.3.6)
stackprof
sucker_punch
- turbolinks
+ turbolinks!
tzinfo-data
uglifier (>= 1.3.0)
w3c_validators
diff --git a/README.md b/README.md
index a17b6245f5..f823a49f7d 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ Ruby code (ERB files). Views are typically rendered to generate a controller res
or to generate the body of an email. In Rails, View generation is handled by Action View.
You can read more about Action View in its [README](actionview/README.rdoc).
-Active Record, Action Pack, and Action View can each be used independently outside Rails.
+Active Record, Active Model, Action Pack, and Action View can each be used independently outside Rails.
In addition to them, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library
to generate and send emails; Active Job ([README](activejob/README.md)), a
framework for declaring jobs and making them run on a variety of queueing
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index 6b2e6a531c..50f2c71737 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -449,6 +449,13 @@ class BaseTest < ActiveSupport::TestCase
assert_equal("Format with any!", email.parts[1].body.encoded)
end
+ test 'explicit without specifying format with format.any' do
+ error = assert_raises(ArgumentError) do
+ BaseMailer.explicit_without_specifying_format_with_any.parts
+ end
+ assert_equal "You have to supply at least one format", error.message
+ end
+
test "explicit multipart with format(Hash)" do
email = BaseMailer.explicit_multipart_with_options(true)
email.ready_to_send!
diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb
index ceaa4ca83e..d17e774092 100644
--- a/actionmailer/test/delivery_methods_test.rb
+++ b/actionmailer/test/delivery_methods_test.rb
@@ -165,16 +165,18 @@ class MailDeliveryTest < ActiveSupport::TestCase
test "non registered delivery methods raises errors" do
DeliveryMailer.delivery_method = :unknown
- assert_raise RuntimeError do
+ error = assert_raise RuntimeError do
DeliveryMailer.welcome.deliver_now
end
+ assert_equal "Invalid delivery method :unknown", error.message
end
test "undefined delivery methods raises errors" do
DeliveryMailer.delivery_method = nil
- assert_raise RuntimeError do
+ error = assert_raise RuntimeError do
DeliveryMailer.welcome.deliver_now
end
+ assert_equal "Delivery method cannot be nil", error.message
end
test "does not perform deliveries if requested" do
diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb
index bd991e209e..8c2225ce60 100644
--- a/actionmailer/test/mailers/base_mailer.rb
+++ b/actionmailer/test/mailers/base_mailer.rb
@@ -80,6 +80,12 @@ class BaseMailer < ActionMailer::Base
end
end
+ def explicit_without_specifying_format_with_any(hash = {})
+ mail(hash) do |format|
+ format.any
+ end
+ end
+
def explicit_multipart_with_options(include_html = false)
mail do |format|
format.text(content_transfer_encoding: "base64"){ render "welcome" }
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 73a833a23b..bb15edee63 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,21 @@
+* ActionDispatch::Response#new no longer applies default headers. If you want
+ default headers applied to the response object, then call
+ `ActionDispatch::Response.create`. This change only impacts people who are
+ directly constructing an `ActionDispatch::Response` object.
+
+* Accessing mime types via constants like `Mime::HTML` is deprecated. Please
+ change code like this:
+
+ Mime::HTML
+
+ To this:
+
+ Mime::Type[:HTML]
+
+ This change is so that Rails will not manage a list of constants, and fixes
+ an issue where if a type isn't registered you could possibly get the wrong
+ object.
+
* `url_for` does not modify its arguments when generating polymorphic URLs.
*Bernerd Schaefer*
diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb
index ddd56b354a..3b5128cda5 100644
--- a/actionpack/lib/abstract_controller/collector.rb
+++ b/actionpack/lib/abstract_controller/collector.rb
@@ -7,7 +7,7 @@ module AbstractController
const = sym.upcase
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{sym}(*args, &block) # def html(*args, &block)
- custom(Mime::#{const}, *args, &block) # custom(Mime::HTML, *args, &block)
+ custom(Mime::Type[:#{const}], *args, &block) # custom(Mime::Type[:HTML], *args, &block)
end # end
RUBY
end
@@ -25,7 +25,7 @@ module AbstractController
def method_missing(symbol, &block)
const_name = symbol.upcase
- unless Mime.const_defined?(const_name)
+ unless Mime::Type.registered?(const_name)
raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
"http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
"If you meant to respond to a variant like :tablet or :phone, not a custom format, " \
@@ -33,7 +33,7 @@ module AbstractController
"format.html { |html| html.tablet { ... } }"
end
- mime_constant = Mime.const_get(const_name)
+ mime_constant = Mime::Type[const_name]
if Mime::SET.include?(mime_constant)
AbstractController::Collector.generate_method_for_mime(mime_constant)
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index 6c0a072b73..78b43f2fbe 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -55,7 +55,7 @@ module AbstractController
# Returns Content-Type of rendered content
# :api: public
def rendered_format
- Mime::TEXT
+ Mime::Type[:TEXT]
end
DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %i(
diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb
index 0384740fef..beeaae9d0c 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -135,7 +135,7 @@ module ActionController
end
def self.make_response!(request)
- ActionDispatch::Response.new.tap do |res|
+ ActionDispatch::Response.create.tap do |res|
res.request = request
end
end
@@ -187,6 +187,7 @@ module ActionController
set_request!(request)
set_response!(response)
process(name)
+ request.commit_flash
to_a
end
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 15d4562abb..fe470552b0 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -34,7 +34,7 @@ module ActionController
#
# def authenticate
# case request.format
- # when Mime::XML, Mime::ATOM
+ # when Mime::Type[:XML], Mime::Type[:ATOM]
# if user = authenticate_with_http_basic { |u, p| @account.users.authenticate(u, p) }
# @current_user = user
# else
@@ -361,7 +361,7 @@ module ActionController
#
# def authenticate
# case request.format
- # when Mime::XML, Mime::ATOM
+ # when Mime::Type[:XML], Mime::Type[:ATOM]
# if user = authenticate_with_http_token { |t, o| @account.users.authenticate(t, o) }
# @current_user = user
# else
@@ -436,15 +436,17 @@ module ActionController
end
end
- # Parses the token and options out of the token authorization header. If
- # the header looks like this:
+ # Parses the token and options out of the token authorization header.
+ # The value for the Authorization header is expected to have the prefix
+ # <tt>"Token"</tt> or <tt>"Bearer"</tt>. If the header looks like this:
# Authorization: Token token="abc", nonce="def"
- # Then the returned token is "abc", and the options is {nonce: "def"}
+ # Then the returned token is <tt>"abc"</tt>, and the options are
+ # <tt>{nonce: "def"}</tt>
#
# request - ActionDispatch::Request instance with the current headers.
#
- # Returns an Array of [String, Hash] if a token is present.
- # Returns nil if no token is found.
+ # Returns an +Array+ of <tt>[String, Hash]</tt> if a token is present.
+ # Returns +nil+ if no token is found.
def token_and_options(request)
authorization_request = request.authorization.to_s
if authorization_request[TOKEN_REGEX]
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index 667c7f87ca..7db8d13e24 100644
--- a/actionpack/lib/action_controller/metal/live.rb
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -213,29 +213,6 @@ module ActionController
end
class Response < ActionDispatch::Response #:nodoc: all
- class Header < DelegateClass(Hash) # :nodoc:
- def initialize(response, header)
- @response = response
- super(header)
- end
-
- def []=(k,v)
- if @response.committed?
- raise ActionDispatch::IllegalStateError, 'header already sent'
- end
-
- super
- end
-
- def merge(other)
- self.class.new @response, __getobj__.merge(other)
- end
-
- def to_hash
- __getobj__.dup
- end
- end
-
private
def before_committed
@@ -256,14 +233,6 @@ module ActionController
body.each { |part| buf.write part }
buf
end
-
- def merge_default_headers(original, default)
- Header.new self, super
- end
-
- def handle_conditional_get!
- super unless committed?
- end
end
def process(name)
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index 88a4818c16..fc42fe5c07 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -229,14 +229,14 @@ module ActionController #:nodoc:
@responses = {}
@variant = variant
- mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil }
+ mimes.each { |mime| @responses[Mime::Type[mime.upcase.to_sym]] = nil }
end
def any(*args, &block)
if args.any?
args.each { |type| send(type, &block) }
else
- custom(Mime::ALL, &block)
+ custom(Mime::Type[:ALL], &block)
end
end
alias :all :any
@@ -251,7 +251,7 @@ module ActionController #:nodoc:
end
def response
- response = @responses.fetch(format, @responses[Mime::ALL])
+ response = @responses.fetch(format, @responses[Mime::Type[:ALL]])
if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax
response.variant
elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
index e680432127..c38fc40b81 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -276,7 +276,9 @@ module ActionController
# Checks if we should perform parameters wrapping.
def _wrapper_enabled?
- ref = request.content_mime_type.try(:ref)
+ return false unless request.has_content_type?
+
+ ref = request.content_mime_type.ref
_wrapper_formats.include?(ref) && _wrapper_key && !request.request_parameters[_wrapper_key]
end
end
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index cb74c4f0d4..d867c97b46 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -68,11 +68,11 @@ module ActionController
# ActionController::Renderers.add :csv do |obj, options|
# filename = options[:filename] || 'data'
# str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
- # send_data str, type: Mime::CSV,
+ # send_data str, type: Mime::Type[:CSV],
# disposition: "attachment; filename=#{filename}.csv"
# end
#
- # Note that we used Mime::CSV for the csv mime type as it comes with Rails.
+ # Note that we used Mime::Type[:CSV] for the csv mime type as it comes with Rails.
# For a custom renderer, you'll need to register a mime type with
# <tt>Mime::Type.register</tt>.
#
@@ -116,24 +116,24 @@ module ActionController
json = json.to_json(options) unless json.kind_of?(String)
if options[:callback].present?
- if content_type.nil? || content_type == Mime::JSON
- self.content_type = Mime::JS
+ if content_type.nil? || content_type == Mime::Type[:JSON]
+ self.content_type = Mime::Type[:JS]
end
"/**/#{options[:callback]}(#{json})"
else
- self.content_type ||= Mime::JSON
+ self.content_type ||= Mime::Type[:JSON]
json
end
end
add :js do |js, options|
- self.content_type ||= Mime::JS
+ self.content_type ||= Mime::Type[:JS]
js.respond_to?(:to_js) ? js.to_js(options) : js
end
add :xml do |xml, options|
- self.content_type ||= Mime::XML
+ self.content_type ||= Mime::Type[:XML]
xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
end
end
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index 00b551af94..1ecccf9864 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -64,7 +64,7 @@ module ActionController
end
def _set_html_content_type
- self.content_type = Mime::HTML.to_s
+ self.content_type = Mime::Type[:HTML].to_s
end
def _set_rendered_content_type(format)
diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb
index 47d940f692..b2b3b4283f 100644
--- a/actionpack/lib/action_controller/metal/testing.rb
+++ b/actionpack/lib/action_controller/metal/testing.rb
@@ -2,12 +2,6 @@ module ActionController
module Testing
extend ActiveSupport::Concern
- # TODO : Rewrite tests using controller.headers= to use Rack env
- def headers=(new_headers)
- @_response ||= ActionDispatch::Response.new
- @_response.headers.replace(new_headers)
- end
-
# Behavior specific to functional tests
module Functional # :nodoc:
def set_response!(request)
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index fbbaa1a887..cf78688126 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -33,6 +33,9 @@ module ActionController
self.session = session
self.session_options = TestSession::DEFAULT_OPTIONS
+ @custom_param_parsers = {
+ Mime::Type[:XML] => lambda { |raw_post| Hash.from_xml(raw_post)['hash'] }
+ }
end
def query_string=(string)
@@ -74,26 +77,18 @@ module ActionController
set_header k, 'application/x-www-form-urlencoded'
end
- # FIXME: setting `request_parametes` is normally handled by the
- # params parser middleware, and we should remove this roundtripping
- # when we switch to caling `call` on the controller
-
case content_mime_type.to_sym
when nil
raise "Unknown Content-Type: #{content_type}"
when :json
data = ActiveSupport::JSON.encode(non_path_parameters)
- params = ActiveSupport::JSON.decode(data).with_indifferent_access
- self.request_parameters = params
when :xml
data = non_path_parameters.to_xml
- params = Hash.from_xml(data)['hash']
- self.request_parameters = params
when :url_encoded_form
data = non_path_parameters.to_query
else
+ @custom_param_parsers[content_mime_type] = ->(_) { non_path_parameters }
data = non_path_parameters.to_query
- self.request_parameters = non_path_parameters
end
end
@@ -136,6 +131,12 @@ module ActionController
"multipart/form-data; boundary=#{Rack::Test::MULTIPART_BOUNDARY}"
end
end.new
+
+ private
+
+ def params_parsers
+ super.merge @custom_param_parsers
+ end
end
class LiveTestResponse < Live::Response
@@ -401,7 +402,7 @@ module ActionController
MSG
@request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
- @request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
+ @request.env['HTTP_ACCEPT'] ||= [Mime::Type[:JS], Mime::Type[:HTML], Mime::Type[:XML], 'text/xml', Mime::Type[:ALL]].join(', ')
__send__(*args).tap do
@request.env.delete 'HTTP_X_REQUESTED_WITH'
@request.env.delete 'HTTP_ACCEPT'
@@ -504,7 +505,7 @@ module ActionController
if xhr
@request.set_header 'HTTP_X_REQUESTED_WITH', 'XMLHttpRequest'
@request.fetch_header('HTTP_ACCEPT') do |k|
- @request.set_header k, [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
+ @request.set_header k, [Mime::Type[:JS], Mime::Type[:HTML], Mime::Type[:XML], 'text/xml', Mime::Type[:ALL]].join(', ')
end
end
@@ -584,7 +585,7 @@ module ActionController
end
def build_response(klass)
- klass.new
+ klass.create
end
included do
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index 1d0a6b6eb3..6b25ee9a70 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -59,7 +59,7 @@ module ActionDispatch
end
def last_modified?
- have_header? LAST_MODIFIED
+ has_header? LAST_MODIFIED
end
def last_modified=(utc_time)
@@ -73,7 +73,7 @@ module ActionDispatch
end
def date?
- have_header? DATE
+ has_header? DATE
end
def date=(utc_time)
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index 9a3aaca3f0..12f81dc1a5 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -49,6 +49,11 @@ module ActionDispatch
@req.set_header env_name(key), value
end
+ # Add a value to a multivalued header like Vary or Accept-Encoding.
+ def add(key, value)
+ @req.add_header env_name(key), value
+ end
+
def key?(key)
@req.has_header? env_name(key)
end
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index cab60a508a..a966c5e452 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -10,7 +10,7 @@ module ActionDispatch
self.ignore_accept_header = false
end
- # The MIME type of the HTTP request, such as Mime::XML.
+ # The MIME type of the HTTP request, such as Mime::Type[:XML].
#
# For backward compatibility, the post \format is extracted from the
# X-Post-Data-Format HTTP header if present.
@@ -29,6 +29,10 @@ module ActionDispatch
content_mime_type && content_mime_type.to_s
end
+ def has_content_type?
+ has_header? 'CONTENT_TYPE'
+ end
+
# Returns the accepted MIME type for the request.
def accepts
fetch_header("action_dispatch.request.accepts") do |k|
@@ -45,9 +49,9 @@ module ActionDispatch
# Returns the MIME type for the \format used in the request.
#
- # GET /posts/5.xml | request.format => Mime::XML
- # GET /posts/5.xhtml | request.format => Mime::HTML
- # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first
+ # GET /posts/5.xml | request.format => Mime::Type[:XML]
+ # GET /posts/5.xhtml | request.format => Mime::Type[:HTML]
+ # GET /posts/5 | request.format => Mime::Type[:HTML] or Mime::Type[:JS], or request.accepts.first
#
def format(view_path = [])
formats.first || Mime::NullType.instance
@@ -66,9 +70,9 @@ module ActionDispatch
elsif use_accept_header && valid_accept_header
accepts
elsif xhr?
- [Mime::JS]
+ [Mime::Type[:JS]]
else
- [Mime::HTML]
+ [Mime::Type[:HTML]]
end
set_header k, v
end
@@ -134,14 +138,14 @@ module ActionDispatch
#
def negotiate_mime(order)
formats.each do |priority|
- if priority == Mime::ALL
+ if priority == Mime::Type[:ALL]
return order.first
elsif order.include?(priority)
return priority
end
end
- order.include?(Mime::ALL) ? format : nil
+ order.include?(Mime::Type[:ALL]) ? format : nil
end
protected
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index a639f8a8f8..36e90e5855 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -1,23 +1,32 @@
-require 'set'
require 'singleton'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/string/starts_ends_with'
+require 'active_support/deprecation'
module Mime
- class Mimes < Array
- def symbols
- @symbols ||= map(&:to_sym)
+ class Mimes
+ include Enumerable
+
+ def initialize
+ @mimes = []
+ @symbols = nil
end
- %w(<< concat shift unshift push pop []= clear compact! collect!
- delete delete_at delete_if flatten! map! insert reject! reverse!
- replace slice! sort! uniq!).each do |method|
- module_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{method}(*)
- @symbols = nil
- super
- end
- CODE
+ def each
+ @mimes.each { |x| yield x }
+ end
+
+ def <<(type)
+ @mimes << type
+ @symbols = nil
+ end
+
+ def delete_if
+ @mimes.delete_if { |x| yield x }.tap { @symbols = nil }
+ end
+
+ def symbols
+ @symbols ||= map(&:to_sym)
end
end
@@ -35,6 +44,40 @@ module Mime
return type if type.is_a?(Type)
EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k }
end
+
+ def const_missing(sym)
+ if Mime::Type.registered?(sym)
+ ActiveSupport::Deprecation.warn <<-eow
+Accessing mime types via constants is deprecated. Please change:
+
+ `Mime::#{sym}`
+
+to:
+
+ `Mime::Type[:#{sym}]`
+ eow
+ Mime::Type[sym]
+ else
+ super
+ end
+ end
+
+ def const_defined?(sym, inherit = true)
+ if Mime::Type.registered?(sym)
+ ActiveSupport::Deprecation.warn <<-eow
+Accessing mime types via constants is deprecated. Please change:
+
+ `Mime.const_defined?(#{sym})`
+
+to:
+
+ `Mime::Type.registered?(:#{sym})`
+ eow
+ true
+ else
+ super
+ end
+ end
end
# Encapsulates the notion of a mime type. Can be used at render time, for example, with:
@@ -51,9 +94,6 @@ module Mime
# end
# end
class Type
- @@html_types = Set.new [:html, :all]
- cattr_reader :html_types
-
attr_reader :symbol
@register_callbacks = []
@@ -66,7 +106,7 @@ module Mime
def initialize(index, name, q = nil)
@index = index
@name = name
- q ||= 0.0 if @name == Mime::ALL.to_s # default wildcard match to end of list
+ q ||= 0.0 if @name == Mime::Type[:ALL].to_s # default wildcard match to end of list
@q = ((q || 1.0).to_f * 100).to_i
end
@@ -91,7 +131,7 @@ module Mime
exchange_xml_items if app_xml_idx > text_xml_idx # make sure app_xml is ahead of text_xml in the list
delete_at(text_xml_idx) # delete text_xml from the list
elsif text_xml_idx
- text_xml.name = Mime::XML.to_s
+ text_xml.name = Mime::Type[:XML].to_s
end
# Look for more specific XML-based types and sort them ahead of app/xml
@@ -120,7 +160,7 @@ module Mime
end
def app_xml_idx
- @app_xml_idx ||= index(Mime::XML.to_s)
+ @app_xml_idx ||= index(Mime::Type[:XML].to_s)
end
def text_xml
@@ -137,6 +177,8 @@ module Mime
end
end
+ TYPES = {}
+
class << self
TRAILING_STAR_REGEXP = /(text|application)\/\*/
PARAMETER_SEPARATOR_REGEXP = /;\s*\w+="?\w+"?/
@@ -145,6 +187,18 @@ module Mime
@register_callbacks << block
end
+ def registered?(symbol)
+ TYPES.key? symbol
+ end
+
+ def [](symbol)
+ TYPES[symbol]
+ end
+
+ def add_type(symbol, type)
+ TYPES[symbol] = type
+ end
+
def lookup(string)
LOOKUP[string]
end
@@ -160,17 +214,18 @@ module Mime
end
def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
- Mime.const_set(symbol.upcase, Type.new(string, symbol, mime_type_synonyms))
+ new_mime = Type.new(string, symbol, mime_type_synonyms)
+ add_type symbol.upcase, new_mime
- new_mime = Mime.const_get(symbol.upcase)
SET << new_mime
- ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = SET.last } unless skip_lookup
- ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = SET.last }
+ ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = new_mime } unless skip_lookup
+ ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = new_mime }
@register_callbacks.each do |callback|
callback.call(new_mime)
end
+ new_mime
end
def parse(accept_header)
@@ -200,11 +255,11 @@ module Mime
parse_data_with_trailing_star($1) if accept_header =~ TRAILING_STAR_REGEXP
end
- # For an input of <tt>'text'</tt>, returns <tt>[Mime::JSON, Mime::XML, Mime::ICS,
- # Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT]</tt>.
+ # For an input of <tt>'text'</tt>, returns <tt>[Mime::Type[:JSON], Mime::Type[:XML], Mime::Type[:ICS],
+ # Mime::Type[:HTML], Mime::Type[:CSS], Mime::Type[:CSV], Mime::Type[:JS], Mime::Type[:YAML], Mime::Type[:TEXT]</tt>.
#
- # For an input of <tt>'application'</tt>, returns <tt>[Mime::HTML, Mime::JS,
- # Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM]</tt>.
+ # For an input of <tt>'application'</tt>, returns <tt>[Mime::Type[:HTML], Mime::Type[:JS],
+ # Mime::Type[:XML], Mime::Type[:YAML], Mime::Type[:ATOM], Mime::Type[:JSON], Mime::Type[:RSS], Mime::Type[:URL_ENCODED_FORM]</tt>.
def parse_data_with_trailing_star(input)
Mime::SET.select { |m| m =~ input }
end
@@ -216,8 +271,7 @@ module Mime
# Mime::Type.unregister(:mobile)
def unregister(symbol)
symbol = symbol.upcase
- mime = Mime.const_get(symbol)
- Mime.instance_eval { remove_const(symbol) }
+ mime = TYPES.delete symbol
SET.delete_if { |v| v.eql?(mime) }
LOOKUP.delete_if { |_,v| v.eql?(mime) }
@@ -243,7 +297,7 @@ module Mime
end
def ref
- to_sym || to_s
+ symbol || to_s
end
def ===(list)
@@ -255,24 +309,23 @@ module Mime
end
def ==(mime_type)
- return false if mime_type.blank?
+ return false unless mime_type
(@synonyms + [ self ]).any? do |synonym|
synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym
end
end
def =~(mime_type)
- return false if mime_type.blank?
+ return false unless mime_type
regexp = Regexp.new(Regexp.quote(mime_type.to_s))
- (@synonyms + [ self ]).any? do |synonym|
- synonym.to_s =~ regexp
- end
+ @synonyms.any? { |synonym| synonym.to_s =~ regexp } || @string =~ regexp
end
def html?
- @@html_types.include?(to_sym) || @string =~ /html/
+ symbol == :html || @string =~ /html/
end
+ def all?; false; end
private
@@ -290,6 +343,11 @@ module Mime
def respond_to_missing?(method, include_private = false) #:nodoc:
method.to_s.ends_with? '?'
end
+
+ class All < Type
+ def all?; true; end
+ def html?; true; end
+ end
end
class NullType
diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb
index 01a10c693b..04828f7c87 100644
--- a/actionpack/lib/action_dispatch/http/mime_types.rb
+++ b/actionpack/lib/action_dispatch/http/mime_types.rb
@@ -32,5 +32,5 @@ Mime::Type.register "application/json", :json, %w( text/x-json application/jsonr
Mime::Type.register "application/pdf", :pdf, [], %w(pdf)
Mime::Type.register "application/zip", :zip, [], %w(zip)
-# Create Mime::ALL but do not add it to the SET.
-Mime::ALL = Mime::Type.new("*/*", :all, [])
+# Create Mime::Type[:ALL] but do not add it to the SET.
+Mime::Type.add_type :ALL, Mime::Type::All.new("*/*", :all, [])
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index 3c9f8cd9e4..e3c4392760 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -3,6 +3,20 @@ module ActionDispatch
module Parameters
PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
+ DEFAULT_PARSERS = {
+ Mime::Type[:JSON] => lambda { |raw_post|
+ data = ActiveSupport::JSON.decode(raw_post)
+ data.is_a?(Hash) ? data : {:_json => data}
+ }
+ }
+
+ def self.included(klass)
+ class << klass
+ attr_accessor :parameter_parsers
+ end
+
+ klass.parameter_parsers = DEFAULT_PARSERS
+ end
# Returns both GET and POST \parameters in a single hash.
def parameters
params = get_header("action_dispatch.request.parameters")
@@ -31,6 +45,27 @@ module ActionDispatch
def path_parameters
get_header(PARAMETERS_KEY) || {}
end
+
+ private
+
+ def parse_formatted_parameters(parsers)
+ return yield if content_length.zero?
+
+ strategy = parsers.fetch(content_mime_type) { return yield }
+
+ begin
+ strategy.call(raw_post)
+ rescue => e # JSON or Ruby code block errors
+ my_logger = logger || ActiveSupport::Logger.new($stderr)
+ my_logger.debug "Error occurred while parsing request parameters.\nContents:\n\n#{raw_post}"
+
+ raise ParamsParser::ParseError.new(e.message, e)
+ end
+ end
+
+ def params_parsers
+ ActionDispatch::Request.parameter_parsers
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index b2566c4820..bf20a33d36 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -348,8 +348,14 @@ module ActionDispatch
# Override Rack's POST method to support indifferent access
def POST
fetch_header("action_dispatch.request.request_parameters") do
- self.request_parameters = Request::Utils.normalize_encode_params(super || {})
+ pr = parse_formatted_parameters(params_parsers) do |params|
+ super || {}
+ end
+ self.request_parameters = Request::Utils.normalize_encode_params(pr)
end
+ rescue ParamsParser::ParseError # one of the parse strategies blew up
+ self.request_parameters = Request::Utils.normalize_encode_params(super || {})
+ raise
rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new(:request, e)
end
@@ -378,6 +384,9 @@ module ActionDispatch
get_header("action_dispatch.logger".freeze)
end
+ def commit_flash
+ end
+
private
def check_method(name)
HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}")
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index 45ffacd6f5..a27ff67114 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -32,6 +32,29 @@ module ActionDispatch # :nodoc:
# end
# end
class Response
+ class Header < DelegateClass(Hash) # :nodoc:
+ def initialize(response, header)
+ @response = response
+ super(header)
+ end
+
+ def []=(k,v)
+ if @response.committed?
+ raise ActionDispatch::IllegalStateError, 'header already sent'
+ end
+
+ super
+ end
+
+ def merge(other)
+ self.class.new @response, __getobj__.merge(other)
+ end
+
+ def to_hash
+ __getobj__.dup
+ end
+ end
+
# The request that the response is responding to.
attr_accessor :request
@@ -103,14 +126,22 @@ module ActionDispatch # :nodoc:
end
end
+ def self.create(status = 200, header = {}, body = [], default_headers: self.default_headers)
+ header = merge_default_headers(header, default_headers)
+ new status, header, body
+ end
+
+ def self.merge_default_headers(original, default)
+ default.respond_to?(:merge) ? default.merge(original) : original
+ end
+
# The underlying body, as a streamable object.
attr_reader :stream
- def initialize(status = 200, header = {}, body = [], default_headers: self.class.default_headers)
+ def initialize(status = 200, header = {}, body = [])
super()
- header = merge_default_headers(header, default_headers)
- @header = header
+ @header = Header.new(self, header)
self.body, self.status = body, status
@@ -125,7 +156,7 @@ module ActionDispatch # :nodoc:
yield self if block_given?
end
- def have_header?(key); headers.key? key; end
+ def has_header?(key); headers.key? key; end
def get_header(key); headers[key]; end
def set_header(key, v); headers[key] = v; end
def delete_header(key); headers.delete key; end
@@ -340,15 +371,12 @@ module ActionDispatch # :nodoc:
return if committed?
assign_default_content_type_and_charset!
handle_conditional_get!
+ handle_no_content!
end
def before_sending
end
- def merge_default_headers(original, default)
- default.respond_to?(:merge) ? default.merge(original) : original
- end
-
def build_buffer(response, body)
Buffer.new response, body
end
@@ -361,7 +389,7 @@ module ActionDispatch # :nodoc:
return if content_type
ct = parse_content_type
- set_content_type(ct.mime_type || Mime::HTML.to_s,
+ set_content_type(ct.mime_type || Mime::Type[:HTML].to_s,
ct.charset || self.class.default_charset)
end
@@ -401,10 +429,15 @@ module ActionDispatch # :nodoc:
end
end
+ def handle_no_content!
+ if NO_CONTENT_CODES.include?(@status)
+ @header.delete CONTENT_TYPE
+ @header.delete 'Content-Length'
+ end
+ end
+
def rack_response(status, header)
if NO_CONTENT_CODES.include?(status)
- header.delete CONTENT_TYPE
- header.delete 'Content-Length'
[status, header, []]
else
[status, header, RackBody.new(self)]
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index b653e4eacd..2889acaeb8 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -396,17 +396,32 @@ module ActionDispatch
end
def write(headers)
- @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) }
- @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) }
+ if header = make_set_cookie_header(headers[HTTP_HEADER])
+ headers[HTTP_HEADER] = header
+ end
end
mattr_accessor :always_write_cookie
self.always_write_cookie = false
private
- def write_cookie?(cookie)
- request.ssl? || !cookie[:secure] || always_write_cookie
- end
+
+ def make_set_cookie_header(header)
+ header = @set_cookies.inject(header) { |m, (k, v)|
+ if write_cookie?(v)
+ ::Rack::Utils.add_cookie_to_header(m, k, v)
+ else
+ m
+ end
+ }
+ @delete_cookies.inject(header) { |m, (k, v)|
+ ::Rack::Utils.add_remove_cookie_to_header(m, k, v)
+ }
+ end
+
+ def write_cookie?(cookie)
+ request.ssl? || !cookie[:secure] || always_write_cookie
+ end
end
class AbstractCookieJar # :nodoc:
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index 039efc3af8..5fd984cd07 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -61,7 +61,7 @@ module ActionDispatch
end
def traces
- appplication_trace_with_ids = []
+ application_trace_with_ids = []
framework_trace_with_ids = []
full_trace_with_ids = []
@@ -69,7 +69,7 @@ module ActionDispatch
trace_with_id = { id: idx, trace: trace }
if application_trace.include?(trace)
- appplication_trace_with_ids << trace_with_id
+ application_trace_with_ids << trace_with_id
else
framework_trace_with_ids << trace_with_id
end
@@ -78,7 +78,7 @@ module ActionDispatch
end
{
- "Application Trace" => appplication_trace_with_ids,
+ "Application Trace" => application_trace_with_ids,
"Framework Trace" => framework_trace_with_ids,
"Full Trace" => full_trace_with_ids
}
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index c482b1c5e7..c51dcd542a 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -1,25 +1,6 @@
require 'active_support/core_ext/hash/keys'
module ActionDispatch
- class Request
- # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
- # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
- # to put a new one.
- def flash
- flash = flash_hash
- return flash if flash
- self.flash = Flash::FlashHash.from_session_value(session["flash"])
- end
-
- def flash=(flash)
- set_header Flash::KEY, flash
- end
-
- def flash_hash # :nodoc:
- get_header Flash::KEY
- end
- end
-
# The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
# action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
@@ -57,6 +38,40 @@ module ActionDispatch
class Flash
KEY = 'action_dispatch.request.flash_hash'.freeze
+ module RequestMethods
+ # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
+ # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
+ # to put a new one.
+ def flash
+ flash = flash_hash
+ return flash if flash
+ self.flash = Flash::FlashHash.from_session_value(session["flash"])
+ end
+
+ def flash=(flash)
+ set_header Flash::KEY, flash
+ end
+
+ def flash_hash # :nodoc:
+ get_header Flash::KEY
+ end
+
+ def commit_flash # :nodoc:
+ session = self.session || {}
+ flash_hash = self.flash_hash
+
+ if flash_hash && (flash_hash.present? || session.key?('flash'))
+ session["flash"] = flash_hash.to_session_value
+ self.flash = flash_hash.dup
+ end
+
+ if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?)
+ session.key?('flash') && session['flash'].nil?
+ session.delete('flash')
+ end
+ end
+ end
+
class FlashNow #:nodoc:
attr_accessor :flash
@@ -268,26 +283,10 @@ module ActionDispatch
end
end
- def initialize(app)
- @app = app
- end
-
- def call(env)
- req = ActionDispatch::Request.new env
- @app.call(env)
- ensure
- session = Request::Session.find(req) || {}
- flash_hash = req.flash_hash
-
- if flash_hash && (flash_hash.present? || session.key?('flash'))
- session["flash"] = flash_hash.to_session_value
- req.flash = flash_hash.dup
- end
+ def self.new(app) app; end
+ end
- if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?)
- session.key?('flash') && session['flash'].nil?
- session.delete('flash')
- end
- end
+ class Request
+ prepend Flash::RequestMethods
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb
index 9cde9c9b98..18af0a583a 100644
--- a/actionpack/lib/action_dispatch/middleware/params_parser.rb
+++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb
@@ -18,48 +18,13 @@ module ActionDispatch
end
end
- DEFAULT_PARSERS = {
- Mime::JSON => lambda { |raw_post|
- data = ActiveSupport::JSON.decode(raw_post)
- data = {:_json => data} unless data.is_a?(Hash)
- Request::Utils.normalize_encode_params(data)
- }
- }
-
# Create a new +ParamsParser+ middleware instance.
#
# The +parsers+ argument can take Hash of parsers where key is identifying
# content mime type, and value is a lambda that is going to process data.
- def initialize(app, parsers = {})
- @app, @parsers = app, DEFAULT_PARSERS.merge(parsers)
+ def self.new(app, parsers = {})
+ ActionDispatch::Request.parameter_parsers = ActionDispatch::Request::DEFAULT_PARSERS.merge(parsers)
+ app
end
-
- def call(env)
- request = Request.new(env)
-
- parse_formatted_parameters(request, @parsers) do |params|
- request.request_parameters = params
- end
-
- @app.call(env)
- end
-
- private
- def parse_formatted_parameters(request, parsers)
- return if request.content_length.zero?
-
- strategy = parsers.fetch(request.content_mime_type) { return nil }
-
- yield strategy.call(request.raw_post)
-
- rescue => e # JSON or Ruby code block errors
- logger(request).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"
-
- raise ParseError.new(e.message, e)
- end
-
- def logger(request)
- request.logger || ActiveSupport::Logger.new($stderr)
- end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index 02b6cfe727..0e636b8257 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -62,11 +62,7 @@ module ActionDispatch
# would set the session cookie to expire automatically 14 days after creation.
# Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
# <tt>:httponly</tt>.
- class CookieStore < Rack::Session::Abstract::Persisted
- include Compatibility
- include StaleSessionCheck
- include SessionObject
-
+ class CookieStore < AbstractStore
def initialize(app, options={})
super(app, options.merge!(:cookie_only => true))
end
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index 7182ae201c..f3c6be864f 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -146,6 +146,7 @@ module ActionDispatch
# get 'geocode/:postalcode' => :show, constraints: {
# postalcode: /\d{5}(-\d{4})?/
# }
+ # end
#
# Constraints can include the 'ignorecase' and 'extended syntax' regular
# expression modifiers:
diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb
index 21b3b89d22..81fa10a613 100644
--- a/actionpack/lib/action_dispatch/testing/assertions.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions.rb
@@ -12,7 +12,7 @@ module ActionDispatch
include Rails::Dom::Testing::Assertions
def html_document
- @html_document ||= if @response.content_type === Mime::XML
+ @html_document ||= if @response.content_type === Mime::Type[:XML]
Nokogiri::XML::Document.parse(@response.body)
else
Nokogiri::HTML::Document.parse(@response.body)
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index 4dfd4f3f71..753cd2073b 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -354,7 +354,7 @@ module ActionDispatch
if xhr
headers ||= {}
headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
- headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
+ headers['HTTP_ACCEPT'] ||= [Mime::Type[:JS], Mime::Type[:HTML], Mime::Type[:XML], 'text/xml', Mime::Type[:ALL]].join(', ')
end
# this modifies the passed request_env directly
diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb
index 6a31d6243f..4b79a90242 100644
--- a/actionpack/lib/action_dispatch/testing/test_response.rb
+++ b/actionpack/lib/action_dispatch/testing/test_response.rb
@@ -7,7 +7,7 @@ module ActionDispatch
# See Response for more information on controller response objects.
class TestResponse < Response
def self.from_response(response)
- new response.status, response.headers, response.body, default_headers: nil
+ new response.status, response.headers, response.body
end
# Was the response successful?
diff --git a/actionpack/test/abstract/collector_test.rb b/actionpack/test/abstract/collector_test.rb
index fc59bf19c4..3b36e43c0b 100644
--- a/actionpack/test/abstract/collector_test.rb
+++ b/actionpack/test/abstract/collector_test.rb
@@ -53,9 +53,9 @@ module AbstractController
collector.html
collector.text(:foo)
collector.js(:bar) { :baz }
- assert_equal [Mime::HTML, [], nil], collector.responses[0]
- assert_equal [Mime::TEXT, [:foo], nil], collector.responses[1]
- assert_equal [Mime::JS, [:bar]], collector.responses[2][0,2]
+ assert_equal [Mime::Type[:HTML], [], nil], collector.responses[0]
+ assert_equal [Mime::Type[:TEXT], [:foo], nil], collector.responses[1]
+ assert_equal [Mime::Type[:JS], [:bar]], collector.responses[2][0,2]
assert_equal :baz, collector.responses[2][2].call
end
end
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 3c498960e4..ef7aab72c6 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -104,7 +104,6 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
middleware.use ActionDispatch::DebugExceptions
middleware.use ActionDispatch::Callbacks
- middleware.use ActionDispatch::ParamsParser
middleware.use ActionDispatch::Cookies
middleware.use ActionDispatch::Flash
middleware.use Rack::Head
diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb
index beeafc2e53..7dfeadceb0 100644
--- a/actionpack/test/controller/action_pack_assertions_test.rb
+++ b/actionpack/test/controller/action_pack_assertions_test.rb
@@ -65,7 +65,7 @@ class ActionPackAssertionsController < ActionController::Base
end
def render_text_with_custom_content_type
- render body: "Hello!", content_type: Mime::RSS
+ render body: "Hello!", content_type: Mime::Type[:RSS]
end
def session_stuffing
diff --git a/actionpack/test/controller/content_type_test.rb b/actionpack/test/controller/content_type_test.rb
index c5bbc479c9..4a86f1bad3 100644
--- a/actionpack/test/controller/content_type_test.rb
+++ b/actionpack/test/controller/content_type_test.rb
@@ -3,7 +3,7 @@ require 'abstract_unit'
class OldContentTypeController < ActionController::Base
# :ported:
def render_content_type_from_body
- response.content_type = Mime::RSS
+ response.content_type = Mime::Type[:RSS]
render body: "hello world!"
end
@@ -14,7 +14,7 @@ class OldContentTypeController < ActionController::Base
# :ported:
def render_content_type_from_render
- render body: "hello world!", :content_type => Mime::RSS
+ render body: "hello world!", :content_type => Mime::Type[:RSS]
end
# :ported:
@@ -36,7 +36,7 @@ class OldContentTypeController < ActionController::Base
end
def render_change_for_builder
- response.content_type = Mime::HTML
+ response.content_type = Mime::Type[:HTML]
render :action => "render_default_for_builder"
end
@@ -45,7 +45,7 @@ class OldContentTypeController < ActionController::Base
format.html { render body: "hello world!" }
format.xml { render action: "render_default_content_types_for_respond_to" }
format.js { render body: "hello world!" }
- format.rss { render body: "hello world!", content_type: Mime::XML }
+ format.rss { render body: "hello world!", content_type: Mime::Type[:XML] }
end
end
end
@@ -64,68 +64,68 @@ class ContentTypeTest < ActionController::TestCase
def test_render_defaults
get :render_defaults
assert_equal "utf-8", @response.charset
- assert_equal Mime::TEXT, @response.content_type
+ assert_equal Mime::Type[:TEXT], @response.content_type
end
def test_render_changed_charset_default
with_default_charset "utf-16" do
get :render_defaults
assert_equal "utf-16", @response.charset
- assert_equal Mime::TEXT, @response.content_type
+ assert_equal Mime::Type[:TEXT], @response.content_type
end
end
# :ported:
def test_content_type_from_body
get :render_content_type_from_body
- assert_equal Mime::RSS, @response.content_type
+ assert_equal Mime::Type[:RSS], @response.content_type
assert_equal "utf-8", @response.charset
end
# :ported:
def test_content_type_from_render
get :render_content_type_from_render
- assert_equal Mime::RSS, @response.content_type
+ assert_equal Mime::Type[:RSS], @response.content_type
assert_equal "utf-8", @response.charset
end
# :ported:
def test_charset_from_body
get :render_charset_from_body
- assert_equal Mime::TEXT, @response.content_type
+ assert_equal Mime::Type[:TEXT], @response.content_type
assert_equal "utf-16", @response.charset
end
# :ported:
def test_nil_charset_from_body
get :render_nil_charset_from_body
- assert_equal Mime::TEXT, @response.content_type
+ assert_equal Mime::Type[:TEXT], @response.content_type
assert_equal "utf-8", @response.charset, @response.headers.inspect
end
def test_nil_default_for_erb
with_default_charset nil do
get :render_default_for_erb
- assert_equal Mime::HTML, @response.content_type
+ assert_equal Mime::Type[:HTML], @response.content_type
assert_nil @response.charset, @response.headers.inspect
end
end
def test_default_for_erb
get :render_default_for_erb
- assert_equal Mime::HTML, @response.content_type
+ assert_equal Mime::Type[:HTML], @response.content_type
assert_equal "utf-8", @response.charset
end
def test_default_for_builder
get :render_default_for_builder
- assert_equal Mime::XML, @response.content_type
+ assert_equal Mime::Type[:XML], @response.content_type
assert_equal "utf-8", @response.charset
end
def test_change_for_builder
get :render_change_for_builder
- assert_equal Mime::HTML, @response.content_type
+ assert_equal Mime::Type[:HTML], @response.content_type
assert_equal "utf-8", @response.charset
end
@@ -144,24 +144,24 @@ class AcceptBasedContentTypeTest < ActionController::TestCase
tests OldContentTypeController
def test_render_default_content_types_for_respond_to
- @request.accept = Mime::HTML.to_s
+ @request.accept = Mime::Type[:HTML].to_s
get :render_default_content_types_for_respond_to
- assert_equal Mime::HTML, @response.content_type
+ assert_equal Mime::Type[:HTML], @response.content_type
- @request.accept = Mime::JS.to_s
+ @request.accept = Mime::Type[:JS].to_s
get :render_default_content_types_for_respond_to
- assert_equal Mime::JS, @response.content_type
+ assert_equal Mime::Type[:JS], @response.content_type
end
def test_render_default_content_types_for_respond_to_with_template
- @request.accept = Mime::XML.to_s
+ @request.accept = Mime::Type[:XML].to_s
get :render_default_content_types_for_respond_to
- assert_equal Mime::XML, @response.content_type
+ assert_equal Mime::Type[:XML], @response.content_type
end
def test_render_default_content_types_for_respond_to_with_overwrite
- @request.accept = Mime::RSS.to_s
+ @request.accept = Mime::Type[:RSS].to_s
get :render_default_content_types_for_respond_to
- assert_equal Mime::XML, @response.content_type
+ assert_equal Mime::Type[:XML], @response.content_type
end
end
diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb
index e9c19b7acf..4d1c23cbee 100644
--- a/actionpack/test/controller/live_stream_test.rb
+++ b/actionpack/test/controller/live_stream_test.rb
@@ -112,7 +112,7 @@ module ActionController
class TestController < ActionController::Base
include ActionController::Live
- attr_accessor :latch, :tc
+ attr_accessor :latch, :tc, :error_latch
def self.controller_path
'test'
@@ -204,6 +204,12 @@ module ActionController
end
def overfill_buffer_and_die
+ logger = ActionController::Base.logger || Logger.new($stdout)
+ response.stream.on_error do
+ logger.warn 'Error while streaming'
+ error_latch.count_down
+ end
+
# Write until the buffer is full. It doesn't expose that
# information directly, so we must hard-code its size:
10.times do
@@ -256,20 +262,12 @@ module ActionController
end
def test_set_cookie
- @controller = TestController.new
get :set_cookie
assert_equal({'hello' => 'world'}, @response.cookies)
assert_equal "hello world", @response.body
end
- def test_set_response!
- @controller.set_response!(@request)
- assert_kind_of(Live::Response, @controller.response)
- assert_equal @request, @controller.response.request
- end
-
def test_write_to_stream
- @controller = TestController.new
get :basic_stream
assert_equal "helloworld", @response.body
assert_equal 'text/event-stream', @response.headers['Content-Type']
@@ -281,10 +279,9 @@ module ActionController
@controller.latch = Concurrent::CountDownLatch.new
parts = ['hello', 'world']
- @controller.request = @request
- @controller.response = @response
+ get :blocking_stream
- t = Thread.new(@response) { |resp|
+ t = Thread.new(response) { |resp|
resp.await_commit
resp.stream.each do |part|
assert_equal parts.shift, part
@@ -294,38 +291,28 @@ module ActionController
end
}
- @controller.process :blocking_stream
-
assert t.join(3), 'timeout expired before the thread terminated'
end
def test_abort_with_full_buffer
@controller.latch = Concurrent::CountDownLatch.new
-
- @request.parameters[:format] = 'plain'
- @controller.request = @request
- @controller.response = @response
-
- got_error = Concurrent::CountDownLatch.new
- @response.stream.on_error do
- ActionController::Base.logger.warn 'Error while streaming'
- got_error.count_down
- end
-
- t = Thread.new(@response) { |resp|
- resp.await_commit
- _, _, body = resp.to_a
- body.each do
- @controller.latch.wait
- body.close
- break
- end
- }
+ @controller.error_latch = Concurrent::CountDownLatch.new
capture_log_output do |output|
- @controller.process :overfill_buffer_and_die
+ get :overfill_buffer_and_die, :format => 'plain'
+
+ t = Thread.new(response) { |resp|
+ resp.await_commit
+ _, _, body = resp.to_a
+ body.each do
+ @controller.latch.wait
+ body.close
+ break
+ end
+ }
+
t.join
- got_error.wait
+ @controller.error_latch.wait
assert_match 'Error while streaming', output.rewind && output.read
end
end
@@ -333,20 +320,18 @@ module ActionController
def test_ignore_client_disconnect
@controller.latch = Concurrent::CountDownLatch.new
- @controller.request = @request
- @controller.response = @response
+ capture_log_output do |output|
+ get :ignore_client_disconnect
- t = Thread.new(@response) { |resp|
- resp.await_commit
- _, _, body = resp.to_a
- body.each do
- body.close
- break
- end
- }
+ t = Thread.new(response) { |resp|
+ resp.await_commit
+ _, _, body = resp.to_a
+ body.each do
+ body.close
+ break
+ end
+ }
- capture_log_output do |output|
- @controller.process :ignore_client_disconnect
t.join
Timeout.timeout(3) do
@controller.latch.wait
@@ -364,11 +349,8 @@ module ActionController
end
def test_live_stream_default_header
- @controller.request = @request
- @controller.response = @response
- @controller.process :default_header
- _, headers, _ = @response.prepare!
- assert headers['Content-Type']
+ get :default_header
+ assert response.headers['Content-Type']
end
def test_render_text
@@ -437,13 +419,13 @@ module ActionController
def test_stale_without_etag
get :with_stale
- assert_equal 200, @response.status.to_i
+ assert_equal 200, response.status.to_i
end
def test_stale_with_etag
@request.if_none_match = Digest::MD5.hexdigest("123")
get :with_stale
- assert_equal 304, @response.status.to_i
+ assert_equal 304, response.status.to_i
end
end
diff --git a/actionpack/test/controller/new_base/content_type_test.rb b/actionpack/test/controller/new_base/content_type_test.rb
index 0445a837ca..d9899fe01f 100644
--- a/actionpack/test/controller/new_base/content_type_test.rb
+++ b/actionpack/test/controller/new_base/content_type_test.rb
@@ -7,12 +7,12 @@ module ContentType
end
def set_on_response_obj
- response.content_type = Mime::RSS
+ response.content_type = Mime::Type[:RSS]
render body: "Hello world!"
end
def set_on_render
- render body: "Hello world!", content_type: Mime::RSS
+ render body: "Hello world!", content_type: Mime::Type[:RSS]
end
end
diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb
index 91b30ede6a..631ff7d02a 100644
--- a/actionpack/test/controller/redirect_test.rb
+++ b/actionpack/test/controller/redirect_test.rb
@@ -266,15 +266,17 @@ class RedirectTest < ActionController::TestCase
end
def test_redirect_to_nil
- assert_raise(ActionController::ActionControllerError) do
+ error = assert_raise(ActionController::ActionControllerError) do
get :redirect_to_nil
end
+ assert_equal "Cannot redirect to nil!", error.message
end
def test_redirect_to_params
- assert_raise(ActionController::ActionControllerError) do
+ error = assert_raise(ActionController::ActionControllerError) do
get :redirect_to_params
end
+ assert_equal "Cannot redirect to a parameter hash!", error.message
end
def test_redirect_to_with_block
diff --git a/actionpack/test/controller/render_other_test.rb b/actionpack/test/controller/render_other_test.rb
index af50e11261..8891f6177f 100644
--- a/actionpack/test/controller/render_other_test.rb
+++ b/actionpack/test/controller/render_other_test.rb
@@ -12,7 +12,7 @@ class RenderOtherTest < ActionController::TestCase
def test_using_custom_render_option
ActionController.add_renderer :simon do |says, options|
- self.content_type = Mime::TEXT
+ self.content_type = Mime::Type[:TEXT]
self.response_body = "Simon says: #{says}"
end
diff --git a/actionpack/test/controller/render_xml_test.rb b/actionpack/test/controller/render_xml_test.rb
index 7a91577b17..094d3ea1d2 100644
--- a/actionpack/test/controller/render_xml_test.rb
+++ b/actionpack/test/controller/render_xml_test.rb
@@ -92,6 +92,6 @@ class RenderXmlTest < ActionController::TestCase
def test_should_use_implicit_content_type
get :implicit_content_type, format: 'atom'
- assert_equal Mime::ATOM, @response.content_type
+ assert_equal Mime::Type[:ATOM], @response.content_type
end
end
diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb
index c0ddcf7f50..c712c75c88 100644
--- a/actionpack/test/controller/send_file_test.rb
+++ b/actionpack/test/controller/send_file_test.rb
@@ -20,6 +20,47 @@ class SendFileController < ActionController::Base
send_file(file_path, options)
end
+ def test_send_file_headers_bang
+ options = {
+ :type => Mime::Type[:PNG],
+ :disposition => 'disposition',
+ :filename => 'filename'
+ }
+
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_with_disposition_as_a_symbol
+ options = {
+ :type => Mime::Type[:PNG],
+ :disposition => :disposition,
+ :filename => 'filename'
+ }
+
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_with_mime_lookup_with_symbol
+ options = { :type => :png }
+
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_with_bad_symbol
+ options = { :type => :this_type_is_not_registered }
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_with_nil_content_type
+ options = { :type => nil }
+ send_data "foo", options
+ end
+
+ def test_send_file_headers_guess_type_from_extension
+ options = { :filename => params[:filename] }
+ send_data "foo", options
+ end
+
def data
send_data(file_data, options)
end
@@ -88,62 +129,39 @@ class SendFileTest < ActionController::TestCase
# Test that send_file_headers! is setting the correct HTTP headers.
def test_send_file_headers_bang
- options = {
- :type => Mime::PNG,
- :disposition => 'disposition',
- :filename => 'filename'
- }
-
# Do it a few times: the resulting headers should be identical
# no matter how many times you send with the same options.
# Test resolving Ticket #458.
- @controller.headers = {}
- @controller.send(:send_file_headers!, options)
- @controller.send(:send_file_headers!, options)
- @controller.send(:send_file_headers!, options)
-
- h = @controller.headers
- assert_equal 'image/png', @controller.content_type
- assert_equal 'disposition; filename="filename"', h['Content-Disposition']
- assert_equal 'binary', h['Content-Transfer-Encoding']
+ 5.times do
+ get :test_send_file_headers_bang
- # test overriding Cache-Control: no-cache header to fix IE open/save dialog
- @controller.send(:send_file_headers!, options)
- @controller.response.prepare!
- assert_equal 'private', h['Cache-Control']
+ assert_equal 'image/png', response.content_type
+ assert_equal 'disposition; filename="filename"', response.get_header('Content-Disposition')
+ assert_equal 'binary', response.get_header('Content-Transfer-Encoding')
+ assert_equal 'private', response.get_header('Cache-Control')
+ end
end
def test_send_file_headers_with_disposition_as_a_symbol
- options = {
- :type => Mime::PNG,
- :disposition => :disposition,
- :filename => 'filename'
- }
+ get :test_send_file_headers_with_disposition_as_a_symbol
- @controller.headers = {}
- @controller.send(:send_file_headers!, options)
- assert_equal 'disposition; filename="filename"', @controller.headers['Content-Disposition']
+ assert_equal 'disposition; filename="filename"', response.get_header('Content-Disposition')
end
def test_send_file_headers_with_mime_lookup_with_symbol
- options = {
- :type => :png
- }
-
- @controller.headers = {}
- @controller.send(:send_file_headers!, options)
-
- assert_equal 'image/png', @controller.content_type
+ get __method__
+ assert_equal 'image/png', response.content_type
end
def test_send_file_headers_with_bad_symbol
- options = {
- :type => :this_type_is_not_registered
- }
+ error = assert_raise(ArgumentError) { get __method__ }
+ assert_equal "Unknown MIME type this_type_is_not_registered", error.message
+ end
- @controller.headers = {}
- assert_raise(ArgumentError) { @controller.send(:send_file_headers!, options) }
+ def test_send_file_headers_with_nil_content_type
+ error = assert_raise(ArgumentError) { get __method__ }
+ assert_equal ":type option required", error.message
end
def test_send_file_headers_guess_type_from_extension
@@ -158,10 +176,8 @@ class SendFileTest < ActionController::TestCase
'file.unk' => 'application/octet-stream',
'zip' => 'application/octet-stream'
}.each do |filename,expected_type|
- options = { :filename => filename }
- @controller.headers = {}
- @controller.send(:send_file_headers!, options)
- assert_equal expected_type, @controller.content_type
+ get __method__, params: { filename: filename }
+ assert_equal expected_type, response.content_type
end
end
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index 06bf9dec74..40c97abd35 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -974,6 +974,11 @@ class ResponseDefaultHeadersTest < ActionController::TestCase
headers.delete params[:header]
head :ok, 'C' => '3'
end
+
+ # Render a head response, but don't touch default headers
+ def leave_alone
+ head :ok
+ end
end
def before_setup
@@ -999,9 +1004,13 @@ class ResponseDefaultHeadersTest < ActionController::TestCase
end
test "response contains default headers" do
+ get :leave_alone
+
# Response headers start out with the defaults
- assert_equal @defaults, response.headers
+ assert_equal @defaults.merge('Content-Type' => 'text/html'), response.headers
+ end
+ test "response deletes a default header" do
get :remove_header, params: { header: 'A' }
assert_response :ok
diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb
index b26f037c36..2aee914a24 100644
--- a/actionpack/test/controller/webservice_test.rb
+++ b/actionpack/test/controller/webservice_test.rb
@@ -65,7 +65,7 @@ class WebServiceTest < ActionDispatch::IntegrationTest
def test_register_and_use_json_simple
with_test_route_set do
- with_params_parsers Mime::JSON => Proc.new { |data| ActiveSupport::JSON.decode(data)['request'].with_indifferent_access } do
+ with_params_parsers Mime::Type[:JSON] => Proc.new { |data| ActiveSupport::JSON.decode(data)['request'].with_indifferent_access } do
post "/",
params: '{"request":{"summary":"content...","title":"JSON"}}',
headers: { 'CONTENT_TYPE' => 'application/json' }
@@ -97,24 +97,28 @@ class WebServiceTest < ActionDispatch::IntegrationTest
end
def test_parsing_json_doesnot_rescue_exception
- with_test_route_set do
- with_params_parsers Mime::JSON => Proc.new { |data| raise Interrupt } do
- assert_raises(Interrupt) do
- post "/",
- params: '{"title":"JSON"}}',
- headers: { 'CONTENT_TYPE' => 'application/json' }
- end
+ req = Class.new(ActionDispatch::Request) do
+ def params_parsers
+ { Mime::Type[:JSON] => Proc.new { |data| raise Interrupt } }
end
+
+ def content_length; get_header('rack.input').length; end
+ end.new({ 'rack.input' => StringIO.new('{"title":"JSON"}}'), 'CONTENT_TYPE' => 'application/json' })
+
+ assert_raises(Interrupt) do
+ req.request_parameters
end
end
private
def with_params_parsers(parsers = {})
old_session = @integration_session
- @app = ActionDispatch::ParamsParser.new(app.routes, parsers)
+ original_parsers = ActionDispatch::Request.parameter_parsers
+ ActionDispatch::Request.parameter_parsers = original_parsers.merge parsers
reset!
yield
ensure
+ ActionDispatch::Request.parameter_parsers = original_parsers
@integration_session = old_session
end
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index e9b2fe3214..84c244c72a 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -3,6 +3,75 @@ require 'openssl'
require 'active_support/key_generator'
require 'active_support/message_verifier'
+class CookieJarTest < ActiveSupport::TestCase
+ attr_reader :request
+
+ def setup
+ @request = ActionDispatch::Request.new({})
+ end
+
+ def test_fetch
+ x = Object.new
+ assert_not request.cookie_jar.key?('zzzzzz')
+ assert_equal x, request.cookie_jar.fetch('zzzzzz', x)
+ assert_not request.cookie_jar.key?('zzzzzz')
+ end
+
+ def test_fetch_exists
+ x = Object.new
+ request.cookie_jar['foo'] = 'bar'
+ assert_equal 'bar', request.cookie_jar.fetch('foo', x)
+ end
+
+ def test_fetch_block
+ x = Object.new
+ assert_not request.cookie_jar.key?('zzzzzz')
+ assert_equal x, request.cookie_jar.fetch('zzzzzz') { x }
+ end
+
+ def test_key_is_to_s
+ request.cookie_jar['foo'] = 'bar'
+ assert_equal 'bar', request.cookie_jar.fetch(:foo)
+ end
+
+ def test_fetch_type_error
+ assert_raises(KeyError) do
+ request.cookie_jar.fetch(:omglolwut)
+ end
+ end
+
+ def test_each
+ request.cookie_jar['foo'] = :bar
+ list = []
+ request.cookie_jar.each do |k,v|
+ list << [k, v]
+ end
+
+ assert_equal [['foo', :bar]], list
+ end
+
+ def test_enumerable
+ request.cookie_jar['foo'] = :bar
+ actual = request.cookie_jar.map { |k,v| [k.to_s, v.to_s] }
+ assert_equal [['foo', 'bar']], actual
+ end
+
+ def test_key_methods
+ assert !request.cookie_jar.key?(:foo)
+ assert !request.cookie_jar.has_key?("foo")
+
+ request.cookie_jar[:foo] = :bar
+ assert request.cookie_jar.key?(:foo)
+ assert request.cookie_jar.has_key?("foo")
+ end
+
+ def test_write_doesnt_set_a_nil_header
+ headers = {}
+ request.cookie_jar.write(headers)
+ assert !headers.include?('Set-Cookie')
+ end
+end
+
class CookiesTest < ActionController::TestCase
class CustomSerializer
def self.load(value)
@@ -14,16 +83,6 @@ class CookiesTest < ActionController::TestCase
end
end
- class JSONWrapper
- def initialize(obj)
- @obj = obj
- end
-
- def as_json(options = nil)
- "wrapped: #{@obj.as_json(options)}"
- end
- end
-
class TestController < ActionController::Base
def authenticate
cookies["user_name"] = "david"
@@ -88,11 +147,6 @@ class CookiesTest < ActionController::TestCase
head :ok
end
- def set_wrapped_signed_cookie
- cookies.signed[:user_id] = JSONWrapper.new(45)
- head :ok
- end
-
def get_signed_cookie
cookies.signed[:user_id]
head :ok
@@ -103,6 +157,21 @@ class CookiesTest < ActionController::TestCase
head :ok
end
+ class JSONWrapper
+ def initialize(obj)
+ @obj = obj
+ end
+
+ def as_json(options = nil)
+ "wrapped: #{@obj.as_json(options)}"
+ end
+ end
+
+ def set_wrapped_signed_cookie
+ cookies.signed[:user_id] = JSONWrapper.new(45)
+ head :ok
+ end
+
def set_wrapped_encrypted_cookie
cookies.encrypted[:foo] = JSONWrapper.new('bar')
head :ok
@@ -207,68 +276,18 @@ class CookiesTest < ActionController::TestCase
tests TestController
+ SALT = 'b3c631c314c0bbca50c1b2843150fe33'
+
def setup
super
- @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33", iterations: 2)
- @request.env["action_dispatch.signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.encrypted_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
- @request.host = "www.nextangle.com"
- end
-
- def test_fetch
- x = Object.new
- assert_not request.cookie_jar.key?('zzzzzz')
- assert_equal x, request.cookie_jar.fetch('zzzzzz', x)
- assert_not request.cookie_jar.key?('zzzzzz')
- end
-
- def test_fetch_exists
- x = Object.new
- request.cookie_jar['foo'] = 'bar'
- assert_equal 'bar', request.cookie_jar.fetch('foo', x)
- end
-
- def test_fetch_block
- x = Object.new
- assert_not request.cookie_jar.key?('zzzzzz')
- assert_equal x, request.cookie_jar.fetch('zzzzzz') { x }
- end
-
- def test_key_is_to_s
- request.cookie_jar['foo'] = 'bar'
- assert_equal 'bar', request.cookie_jar.fetch(:foo)
- end
-
- def test_fetch_type_error
- assert_raises(KeyError) do
- request.cookie_jar.fetch(:omglolwut)
- end
- end
-
- def test_each
- request.cookie_jar['foo'] = :bar
- list = []
- request.cookie_jar.each do |k,v|
- list << [k, v]
- end
- assert_equal [['foo', :bar]], list
- end
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new(SALT, iterations: 2)
- def test_enumerable
- request.cookie_jar['foo'] = :bar
- actual = request.cookie_jar.map { |k,v| [k.to_s, v.to_s] }
- assert_equal [['foo', 'bar']], actual
- end
+ @request.env["action_dispatch.signed_cookie_salt"] =
+ @request.env["action_dispatch.encrypted_cookie_salt"] =
+ @request.env["action_dispatch.encrypted_signed_cookie_salt"] = SALT
- def test_key_methods
- assert !request.cookie_jar.key?(:foo)
- assert !request.cookie_jar.has_key?("foo")
-
- request.cookie_jar[:foo] = :bar
- assert request.cookie_jar.key?(:foo)
- assert request.cookie_jar.has_key?("foo")
+ @request.host = "www.nextangle.com"
end
def test_setting_cookie
@@ -1083,11 +1102,11 @@ class CookiesTest < ActionController::TestCase
assert_equal "david", cookies[:user_name]
get :noop
- assert_nil @response.headers["Set-Cookie"]
+ assert !@response.headers.include?("Set-Cookie")
assert_equal "david", cookies[:user_name]
get :noop
- assert_nil @response.headers["Set-Cookie"]
+ assert !@response.headers.include?("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 79600b654b..7f1ef121b7 100644
--- a/actionpack/test/dispatch/header_test.rb
+++ b/actionpack/test/dispatch/header_test.rb
@@ -42,6 +42,24 @@ class HeaderTest < ActiveSupport::TestCase
assert_equal "127.0.0.1", @headers["HTTP_HOST"]
end
+ test "add to multivalued headers" do
+ # Sets header when not present
+ @headers.add 'Foo', '1'
+ assert_equal '1', @headers['Foo']
+
+ # Ignores nil values
+ @headers.add 'Foo', nil
+ assert_equal '1', @headers['Foo']
+
+ # Converts value to string
+ @headers.add 'Foo', 1
+ assert_equal '1,1', @headers['Foo']
+
+ # Case-insensitive
+ @headers.add 'fOo', 2
+ assert_equal '1,1,2', @headers['foO']
+ end
+
test "headers can contain numbers" do
@headers["Content-MD5"] = "Q2hlY2sgSW50ZWdyaXR5IQ=="
diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb
index 3017a9c2d6..68083ed747 100644
--- a/actionpack/test/dispatch/mime_type_test.rb
+++ b/actionpack/test/dispatch/mime_type_test.rb
@@ -13,76 +13,75 @@ class MimeTypeTest < ActiveSupport::TestCase
test "unregister" do
begin
Mime::Type.register("text/x-mobile", :mobile)
- assert defined?(Mime::MOBILE)
- assert_equal Mime::MOBILE, Mime::LOOKUP['text/x-mobile']
- assert_equal Mime::MOBILE, Mime::EXTENSION_LOOKUP['mobile']
+ assert Mime::Type.registered?(:MOBILE)
+ assert_equal Mime::Type[:MOBILE], Mime::LOOKUP['text/x-mobile']
+ assert_equal Mime::Type[:MOBILE], Mime::EXTENSION_LOOKUP['mobile']
Mime::Type.unregister(:mobile)
- assert !defined?(Mime::MOBILE), "Mime::MOBILE should not be defined"
+ assert !Mime::Type.registered?(:MOBILE), "Mime::MOBILE should not be defined"
assert !Mime::LOOKUP.has_key?('text/x-mobile'), "Mime::LOOKUP should not have key ['text/x-mobile]"
assert !Mime::EXTENSION_LOOKUP.has_key?('mobile'), "Mime::EXTENSION_LOOKUP should not have key ['mobile]"
ensure
- Mime.module_eval { remove_const :MOBILE if const_defined?(:MOBILE) }
Mime::LOOKUP.reject!{|key,_| key == 'text/x-mobile'}
end
end
test "parse text with trailing star at the beginning" do
accept = "text/*, text/html, application/json, multipart/form-data"
- expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON, Mime::MULTIPART_FORM]
+ expect = [Mime::Type[:HTML], Mime::Type[:TEXT], Mime::Type[:JS], Mime::Type[:CSS], Mime::Type[:ICS], Mime::Type[:CSV], Mime::Type[:VCF], Mime::Type[:XML], Mime::Type[:YAML], Mime::Type[:JSON], Mime::Type[:MULTIPART_FORM]]
parsed = Mime::Type.parse(accept)
assert_equal expect, parsed
end
test "parse text with trailing star in the end" do
accept = "text/html, application/json, multipart/form-data, text/*"
- expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML]
+ expect = [Mime::Type[:HTML], Mime::Type[:JSON], Mime::Type[:MULTIPART_FORM], Mime::Type[:TEXT], Mime::Type[:JS], Mime::Type[:CSS], Mime::Type[:ICS], Mime::Type[:CSV], Mime::Type[:VCF], Mime::Type[:XML], Mime::Type[:YAML]]
parsed = Mime::Type.parse(accept)
assert_equal expect, parsed
end
test "parse text with trailing star" do
accept = "text/*"
- expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON]
+ expect = [Mime::Type[:HTML], Mime::Type[:TEXT], Mime::Type[:JS], Mime::Type[:CSS], Mime::Type[:ICS], Mime::Type[:CSV], Mime::Type[:VCF], Mime::Type[:XML], Mime::Type[:YAML], Mime::Type[:JSON]]
parsed = Mime::Type.parse(accept)
assert_equal expect, parsed
end
test "parse application with trailing star" do
accept = "application/*"
- expect = [Mime::HTML, Mime::JS, Mime::XML, Mime::RSS, Mime::ATOM, Mime::YAML, Mime::URL_ENCODED_FORM, Mime::JSON, Mime::PDF, Mime::ZIP]
+ expect = [Mime::Type[:HTML], Mime::Type[:JS], Mime::Type[:XML], Mime::Type[:RSS], Mime::Type[:ATOM], Mime::Type[:YAML], Mime::Type[:URL_ENCODED_FORM], Mime::Type[:JSON], Mime::Type[:PDF], Mime::Type[:ZIP]]
parsed = Mime::Type.parse(accept)
assert_equal expect, parsed
end
test "parse without q" do
accept = "text/xml,application/xhtml+xml,text/yaml,application/xml,text/html,image/png,text/plain,application/pdf,*/*"
- expect = [Mime::HTML, Mime::XML, Mime::YAML, Mime::PNG, Mime::TEXT, Mime::PDF, Mime::ALL]
+ expect = [Mime::Type[:HTML], Mime::Type[:XML], Mime::Type[:YAML], Mime::Type[:PNG], Mime::Type[:TEXT], Mime::Type[:PDF], Mime::Type[:ALL]]
assert_equal expect, Mime::Type.parse(accept)
end
test "parse with q" do
accept = "text/xml,application/xhtml+xml,text/yaml; q=0.3,application/xml,text/html; q=0.8,image/png,text/plain; q=0.5,application/pdf,*/*; q=0.2"
- expect = [Mime::HTML, Mime::XML, Mime::PNG, Mime::PDF, Mime::TEXT, Mime::YAML, Mime::ALL]
+ expect = [Mime::Type[:HTML], Mime::Type[:XML], Mime::Type[:PNG], Mime::Type[:PDF], Mime::Type[:TEXT], Mime::Type[:YAML], Mime::Type[:ALL]]
assert_equal expect, Mime::Type.parse(accept)
end
test "parse single media range with q" do
accept = "text/html;q=0.9"
- expect = [Mime::HTML]
+ expect = [Mime::Type[:HTML]]
assert_equal expect, Mime::Type.parse(accept)
end
test "parse arbitrary media type parameters" do
accept = 'multipart/form-data; boundary="simple boundary"'
- expect = [Mime::MULTIPART_FORM]
+ expect = [Mime::Type[:MULTIPART_FORM]]
assert_equal expect, Mime::Type.parse(accept)
end
# Accept header send with user HTTP_USER_AGENT: Sunrise/0.42j (Windows XP)
test "parse broken acceptlines" do
accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/*,,*/*;q=0.5"
- expect = [Mime::HTML, Mime::XML, "image/*", Mime::TEXT, Mime::ALL]
+ expect = [Mime::Type[:HTML], Mime::Type[:XML], "image/*", Mime::Type[:TEXT], Mime::Type[:ALL]]
assert_equal expect, Mime::Type.parse(accept).collect(&:to_s)
end
@@ -90,16 +89,15 @@ class MimeTypeTest < ActiveSupport::TestCase
# (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; InfoPath.1)
test "parse other broken acceptlines" do
accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, , pronto/1.00.00, sslvpn/1.00.00.00, */*"
- expect = ['image/gif', 'image/x-xbitmap', 'image/jpeg','image/pjpeg', 'application/x-shockwave-flash', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/msword', 'pronto/1.00.00', 'sslvpn/1.00.00.00', Mime::ALL]
+ expect = ['image/gif', 'image/x-xbitmap', 'image/jpeg','image/pjpeg', 'application/x-shockwave-flash', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/msword', 'pronto/1.00.00', 'sslvpn/1.00.00.00', Mime::Type[:ALL]]
assert_equal expect, Mime::Type.parse(accept).collect(&:to_s)
end
test "custom type" do
begin
- Mime::Type.register("image/foo", :foo)
- assert_nothing_raised do
- assert_equal Mime::FOO, Mime::SET.last
- end
+ type = Mime::Type.register("image/foo", :foo)
+ assert_equal Mime::Type[:FOO], type
+ assert Mime::Type.registered?(:FOO)
ensure
Mime::Type.unregister(:FOO)
end
@@ -109,7 +107,7 @@ class MimeTypeTest < ActiveSupport::TestCase
begin
Mime::Type.register "text/foobar", :foobar, ["text/foo", "text/bar"]
%w[text/foobar text/foo text/bar].each do |type|
- assert_equal Mime::FOOBAR, type
+ assert_equal Mime::Type[:FOOBAR], type
end
ensure
Mime::Type.unregister(:FOOBAR)
@@ -124,7 +122,7 @@ class MimeTypeTest < ActiveSupport::TestCase
end
Mime::Type.register("text/foo", :foo)
- assert_equal [Mime::FOO], registered_mimes
+ assert_equal [Mime::Type[:FOO]], registered_mimes
ensure
Mime::Type.unregister(:FOO)
end
@@ -134,7 +132,7 @@ class MimeTypeTest < ActiveSupport::TestCase
begin
Mime::Type.register "text/foobar", :foobar, [], [:foo, "bar"]
%w[foobar foo bar].each do |extension|
- assert_equal Mime::FOOBAR, Mime::EXTENSION_LOOKUP[extension]
+ assert_equal Mime::Type[:FOOBAR], Mime::EXTENSION_LOOKUP[extension]
end
ensure
Mime::Type.unregister(:FOOBAR)
@@ -144,60 +142,71 @@ class MimeTypeTest < ActiveSupport::TestCase
test "register alias" do
begin
Mime::Type.register_alias "application/xhtml+xml", :foobar
- assert_equal Mime::HTML, Mime::EXTENSION_LOOKUP['foobar']
+ assert_equal Mime::Type[:HTML], Mime::EXTENSION_LOOKUP['foobar']
ensure
Mime::Type.unregister(:FOOBAR)
end
end
test "type should be equal to symbol" do
- assert_equal Mime::HTML, 'application/xhtml+xml'
- assert_equal Mime::HTML, :html
+ assert_equal Mime::Type[:HTML], 'application/xhtml+xml'
+ assert_equal Mime::Type[:HTML], :html
end
test "type convenience methods" do
- # Don't test Mime::ALL, since it Mime::ALL#html? == true
+ # Don't test Mime::Type[:ALL], since it Mime::Type[:ALL].html? == true
types = Mime::SET.symbols.uniq - [:all, :iphone]
- # Remove custom Mime::Type instances set in other tests, like Mime::GIF and Mime::IPHONE
- types.delete_if { |type| !Mime.const_defined?(type.upcase) }
-
+ # Remove custom Mime::Type instances set in other tests, like Mime::Type[:GIF] and Mime::Type[:IPHONE]
+ types.delete_if { |type| !Mime::Type.registered?(type.upcase) }
types.each do |type|
- mime = Mime.const_get(type.upcase)
+ mime = Mime::Type[type.upcase]
assert mime.respond_to?("#{type}?"), "#{mime.inspect} does not respond to #{type}?"
- assert mime.send("#{type}?"), "#{mime.inspect} is not #{type}?"
+ assert_equal type, mime.symbol, "#{mime.inspect} is not #{type}?"
invalid_types = types - [type]
- invalid_types.delete(:html) if Mime::Type.html_types.include?(type)
- invalid_types.each { |other_type| assert !mime.send("#{other_type}?"), "#{mime.inspect} is #{other_type}?" }
+ invalid_types.delete(:html)
+ invalid_types.each { |other_type|
+ assert_not_equal mime.symbol, other_type, "#{mime.inspect} is #{other_type}?"
+ }
end
end
test "mime all is html" do
- assert Mime::ALL.all?, "Mime::ALL is not all?"
- assert Mime::ALL.html?, "Mime::ALL is not html?"
+ assert Mime::Type[:ALL].all?, "Mime::ALL is not all?"
+ assert Mime::Type[:ALL].html?, "Mime::ALL is not html?"
+ end
+
+ test "deprecated lookup" do
+ assert_deprecated do
+ assert Mime::ALL.all?, "Mime::ALL is not all?"
+ end
+ end
+
+ test "deprecated const_defined?" do
+ assert_deprecated { Mime.const_defined?(:ALL) }
end
test "verifiable mime types" do
all_types = Mime::SET.symbols
all_types.uniq!
- # Remove custom Mime::Type instances set in other tests, like Mime::GIF and Mime::IPHONE
- all_types.delete_if { |type| !Mime.const_defined?(type.upcase) }
+ # Remove custom Mime::Type instances set in other tests, like Mime::Type[:GIF] and Mime::Type[:IPHONE]
+ all_types.delete_if { |type| !Mime::Type.registered?(type.upcase) }
end
test "references gives preference to symbols before strings" do
- assert_equal :html, Mime::HTML.ref
+ assert_equal :html, Mime::Type[:HTML].ref
another = Mime::Type.lookup("foo/bar")
assert_nil another.to_sym
assert_equal "foo/bar", another.ref
end
test "regexp matcher" do
- assert Mime::JS =~ "text/javascript"
- assert Mime::JS =~ "application/javascript"
- assert Mime::JS !~ "text/html"
- assert !(Mime::JS !~ "text/javascript")
- assert !(Mime::JS !~ "application/javascript")
- assert Mime::HTML =~ 'application/xhtml+xml'
+ assert Mime::Type[:JS] =~ "text/javascript"
+ assert Mime::Type[:JS] =~ "application/javascript"
+ assert Mime::Type[:JS] !~ "text/html"
+ assert !(Mime::Type[:JS] !~ "text/javascript")
+ assert !(Mime::Type[:JS] !~ "application/javascript")
+ assert Mime::Type[:HTML] =~ 'application/xhtml+xml'
end
end
diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb
index 258d097b7c..40866595ed 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -750,33 +750,33 @@ class RequestFormat < BaseRequestTest
test "xml format" do
request = stub_request
assert_called(request, :parameters, times: 2, returns: {format: :xml}) do
- assert_equal Mime::XML, request.format
+ assert_equal Mime::Type[:XML], request.format
end
end
test "xhtml format" do
request = stub_request
assert_called(request, :parameters, times: 2, returns: {format: :xhtml}) do
- assert_equal Mime::HTML, request.format
+ assert_equal Mime::Type[:HTML], request.format
end
end
test "txt format" do
request = stub_request
assert_called(request, :parameters, times: 2, returns: {format: :txt}) do
- assert_equal Mime::TEXT, request.format
+ assert_equal Mime::Type[:TEXT], request.format
end
end
test "XMLHttpRequest" do
request = stub_request(
'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest',
- 'HTTP_ACCEPT' => [Mime::JS, Mime::HTML, Mime::XML, "text/xml", Mime::ALL].join(",")
+ 'HTTP_ACCEPT' => [Mime::Type[:JS], Mime::Type[:HTML], Mime::Type[:XML], "text/xml", Mime::Type[:ALL]].join(",")
)
assert_called(request, :parameters, times: 1, returns: {}) do
assert request.xhr?
- assert_equal Mime::JS, request.format
+ assert_equal Mime::Type[:JS], request.format
end
end
@@ -796,29 +796,29 @@ class RequestFormat < BaseRequestTest
test "formats text/html with accept header" do
request = stub_request 'HTTP_ACCEPT' => 'text/html'
- assert_equal [Mime::HTML], request.formats
+ assert_equal [Mime::Type[:HTML]], request.formats
end
test "formats blank with accept header" do
request = stub_request 'HTTP_ACCEPT' => ''
- assert_equal [Mime::HTML], request.formats
+ assert_equal [Mime::Type[:HTML]], request.formats
end
test "formats XMLHttpRequest with accept header" do
request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- assert_equal [Mime::JS], request.formats
+ assert_equal [Mime::Type[:JS]], request.formats
end
test "formats application/xml with accept header" do
request = stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8',
'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest")
- assert_equal [Mime::XML], request.formats
+ assert_equal [Mime::Type[:XML]], request.formats
end
test "formats format:text with accept header" do
request = stub_request
assert_called(request, :parameters, times: 2, returns: {format: :txt}) do
- assert_equal [Mime::TEXT], request.formats
+ assert_equal [Mime::Type[:TEXT]], request.formats
end
end
@@ -848,7 +848,7 @@ class RequestFormat < BaseRequestTest
test "formats with xhr request" do
request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [Mime::JS], request.formats
+ assert_equal [Mime::Type[:JS]], request.formats
end
end
@@ -859,35 +859,35 @@ class RequestFormat < BaseRequestTest
begin
request = stub_request 'HTTP_ACCEPT' => 'application/xml'
assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime::HTML ], request.formats
+ assert_equal [ Mime::Type[:HTML] ], request.formats
end
request = stub_request 'HTTP_ACCEPT' => 'koz-asked/something-crazy'
assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime::HTML ], request.formats
+ assert_equal [ Mime::Type[:HTML] ], request.formats
end
request = stub_request 'HTTP_ACCEPT' => '*/*;q=0.1'
assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime::HTML ], request.formats
+ assert_equal [ Mime::Type[:HTML] ], request.formats
end
request = stub_request 'HTTP_ACCEPT' => 'application/jxw'
assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime::HTML ], request.formats
+ assert_equal [ Mime::Type[:HTML] ], request.formats
end
request = stub_request 'HTTP_ACCEPT' => 'application/xml',
'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
assert_called(request, :parameters, times: 1, returns: {}) do
- assert_equal [ Mime::JS ], request.formats
+ assert_equal [ Mime::Type[:JS] ], request.formats
end
request = stub_request 'HTTP_ACCEPT' => 'application/xml',
'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
assert_called(request, :parameters, times: 2, returns: {format: :json}) do
- assert_equal [ Mime::JSON ], request.formats
+ assert_equal [ Mime::Type[:JSON] ], request.formats
end
ensure
ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header
@@ -897,7 +897,7 @@ end
class RequestMimeType < BaseRequestTest
test "content type" do
- assert_equal Mime::HTML, stub_request('CONTENT_TYPE' => 'text/html').content_mime_type
+ assert_equal Mime::Type[:HTML], stub_request('CONTENT_TYPE' => 'text/html').content_mime_type
end
test "no content type" do
@@ -905,11 +905,11 @@ class RequestMimeType < BaseRequestTest
end
test "content type is XML" do
- assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml').content_mime_type
+ assert_equal Mime::Type[:XML], stub_request('CONTENT_TYPE' => 'application/xml').content_mime_type
end
test "content type with charset" do
- assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8').content_mime_type
+ assert_equal Mime::Type[:XML], stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8').content_mime_type
end
test "user agent" do
@@ -922,9 +922,9 @@ class RequestMimeType < BaseRequestTest
'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
)
- assert_equal nil, request.negotiate_mime([Mime::XML, Mime::JSON])
- assert_equal Mime::HTML, request.negotiate_mime([Mime::XML, Mime::HTML])
- assert_equal Mime::HTML, request.negotiate_mime([Mime::XML, Mime::ALL])
+ assert_equal nil, request.negotiate_mime([Mime::Type[:XML], Mime::Type[:JSON]])
+ assert_equal Mime::Type[:HTML], request.negotiate_mime([Mime::Type[:XML], Mime::Type[:HTML]])
+ assert_equal Mime::Type[:HTML], request.negotiate_mime([Mime::Type[:XML], Mime::Type[:ALL]])
end
test "negotiate_mime with content_type" do
@@ -933,7 +933,7 @@ class RequestMimeType < BaseRequestTest
'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
)
- assert_equal Mime::XML, request.negotiate_mime([Mime::XML, Mime::CSV])
+ assert_equal Mime::Type[:XML], request.negotiate_mime([Mime::Type[:XML], Mime::Type[:CSV]])
end
end
diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb
index 5d74424de7..2b2fea7504 100644
--- a/actionpack/test/dispatch/response_test.rb
+++ b/actionpack/test/dispatch/response_test.rb
@@ -4,7 +4,7 @@ require 'rack/content_length'
class ResponseTest < ActiveSupport::TestCase
def setup
- @response = ActionDispatch::Response.new
+ @response = ActionDispatch::Response.create
end
def test_can_wait_until_commit
@@ -149,12 +149,19 @@ class ResponseTest < ActiveSupport::TestCase
status, headers, body = @response.to_a
assert_equal "user_name=david; path=/", headers["Set-Cookie"]
assert_equal({"user_name" => "david"}, @response.cookies)
+ end
+ test "multiple cookies" do
+ @response.set_cookie("user_name", :value => "david", :path => "/")
@response.set_cookie("login", :value => "foo&bar", :path => "/", :expires => Time.utc(2005, 10, 10,5))
status, headers, body = @response.to_a
assert_equal "user_name=david; path=/\nlogin=foo%26bar; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000", headers["Set-Cookie"]
assert_equal({"login" => "foo&bar", "user_name" => "david"}, @response.cookies)
+ end
+ test "delete cookies" do
+ @response.set_cookie("user_name", :value => "david", :path => "/")
+ @response.set_cookie("login", :value => "foo&bar", :path => "/", :expires => Time.utc(2005, 10, 10,5))
@response.delete_cookie("login")
status, headers, body = @response.to_a
assert_equal({"user_name" => "david", "login" => nil}, @response.cookies)
@@ -178,18 +185,28 @@ class ResponseTest < ActiveSupport::TestCase
test "read charset and content type" do
resp = ActionDispatch::Response.new.tap { |response|
response.charset = 'utf-16'
- response.content_type = Mime::XML
+ response.content_type = Mime::Type[:XML]
response.body = 'Hello'
}
resp.to_a
assert_equal('utf-16', resp.charset)
- assert_equal(Mime::XML, resp.content_type)
+ assert_equal(Mime::Type[:XML], resp.content_type)
assert_equal('application/xml; charset=utf-16', resp.headers['Content-Type'])
end
- test "read content type without charset" do
+ test "read content type with default charset utf-8" do
+ original = ActionDispatch::Response.default_charset
+ begin
+ resp = ActionDispatch::Response.new(200, { "Content-Type" => "text/xml" })
+ assert_equal('utf-8', resp.charset)
+ ensure
+ ActionDispatch::Response.default_charset = original
+ end
+ end
+
+ test "read content type with charset utf-16" do
jruby_skip "https://github.com/jruby/jruby/issues/3138"
original = ActionDispatch::Response.default_charset
@@ -210,7 +227,7 @@ class ResponseTest < ActiveSupport::TestCase
'X-Content-Type-Options' => 'nosniff',
'X-XSS-Protection' => '1;'
}
- resp = ActionDispatch::Response.new.tap { |response|
+ resp = ActionDispatch::Response.create.tap { |response|
response.body = 'Hello'
}
resp.to_a
@@ -229,7 +246,7 @@ class ResponseTest < ActiveSupport::TestCase
ActionDispatch::Response.default_headers = {
'X-XX-XXXX' => 'Here is my phone number'
}
- resp = ActionDispatch::Response.new.tap { |response|
+ resp = ActionDispatch::Response.create.tap { |response|
response.body = 'Hello'
}
resp.to_a
@@ -276,6 +293,65 @@ class ResponseTest < ActiveSupport::TestCase
end
end
+class ResponseHeadersTest < ActiveSupport::TestCase
+ def setup
+ @response = ActionDispatch::Response.create
+ @response.set_header 'Foo', '1'
+ end
+
+ test 'has_header?' do
+ assert @response.has_header? 'Foo'
+ assert_not @response.has_header? 'foo'
+ assert_not @response.has_header? nil
+ end
+
+ test 'get_header' do
+ assert_equal '1', @response.get_header('Foo')
+ assert_nil @response.get_header('foo')
+ assert_nil @response.get_header(nil)
+ end
+
+ test 'set_header' do
+ assert_equal '2', @response.set_header('Foo', '2')
+ assert @response.has_header?('Foo')
+ assert_equal '2', @response.get_header('Foo')
+
+ assert_nil @response.set_header('Foo', nil)
+ assert @response.has_header?('Foo')
+ assert_nil @response.get_header('Foo')
+ end
+
+ test 'delete_header' do
+ assert_nil @response.delete_header(nil)
+
+ assert_nil @response.delete_header('foo')
+ assert @response.has_header?('Foo')
+
+ assert_equal '1', @response.delete_header('Foo')
+ assert_not @response.has_header?('Foo')
+ end
+
+ test 'add_header' do
+ # Add a value to an existing header
+ assert_equal '1,2', @response.add_header('Foo', '2')
+ assert_equal '1,2', @response.get_header('Foo')
+
+ # Add nil to an existing header
+ assert_equal '1,2', @response.add_header('Foo', nil)
+ assert_equal '1,2', @response.get_header('Foo')
+
+ # Add nil to a nonexistent header
+ assert_nil @response.add_header('Bar', nil)
+ assert_not @response.has_header?('Bar')
+ assert_nil @response.get_header('Bar')
+
+ # Add a value to a nonexistent header
+ assert_equal '1', @response.add_header('Bar', '1')
+ assert @response.has_header?('Bar')
+ assert_equal '1', @response.get_header('Bar')
+ end
+end
+
class ResponseIntegrationTest < ActionDispatch::IntegrationTest
test "response cache control from railsish app" do
@app = lambda { |env|
@@ -317,7 +393,7 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest
@app = lambda { |env|
ActionDispatch::Response.new.tap { |resp|
resp.charset = 'utf-16'
- resp.content_type = Mime::XML
+ resp.content_type = Mime::Type[:XML]
resp.body = 'Hello'
}.to_a
}
@@ -326,7 +402,7 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal('utf-16', @response.charset)
- assert_equal(Mime::XML, @response.content_type)
+ assert_equal(Mime::Type[:XML], @response.content_type)
assert_equal('application/xml; charset=utf-16', @response.headers['Content-Type'])
end
@@ -342,7 +418,7 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal('utf-16', @response.charset)
- assert_equal(Mime::XML, @response.content_type)
+ assert_equal(Mime::Type[:XML], @response.content_type)
assert_equal('application/xml; charset=utf-16', @response.headers['Content-Type'])
end
diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb
index 6a3af32946..15d51e5d6c 100644
--- a/actionpack/test/journey/router_test.rb
+++ b/actionpack/test/journey/router_test.rb
@@ -497,17 +497,6 @@ module ActionDispatch
private
- def add_routes router, paths, anchor = true
- paths.each do |path|
- if String === path
- path = Path::Pattern.from_string path
- else
- path
- end
- add_route @app, path, {}, [], {}, {}
- end
- end
-
def rails_env env, klass = ActionDispatch::Request
klass.new(rack_env(env))
end
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 9ea250643a..82a88952c3 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,8 @@
+* Add a `hidden_field` on the `collection_radio_buttons` to avoid raising a error
+ when the only input on the form is the `collection_radio_buttons`.
+
+ *Mauro George*
+
* `url_for` does not modify its arguments when generating polymorphic URLs.
*Bernerd Schaefer*
diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb
index 6e8c7f8203..7716955fd9 100644
--- a/actionview/lib/action_view/dependency_tracker.rb
+++ b/actionview/lib/action_view/dependency_tracker.rb
@@ -1,9 +1,9 @@
-require 'thread_safe'
+require 'concurrent'
require 'action_view/path_set'
module ActionView
class DependencyTracker # :nodoc:
- @trackers = ThreadSafe::Cache.new
+ @trackers = Concurrent::Map.new
def self.find_dependencies(name, template, view_paths = nil)
tracker = @trackers[template.handler]
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 9a8a4feb2e..12e9723a02 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -1,11 +1,11 @@
-require 'thread_safe'
+require 'concurrent'
require 'action_view/dependency_tracker'
require 'monitor'
module ActionView
class Digestor
cattr_reader(:cache)
- @@cache = ThreadSafe::Cache.new
+ @@cache = Concurrent::Map.new
@@digest_monitor = Monitor.new
class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc:
@@ -28,7 +28,7 @@ module ActionView
cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.')
# this is a correctly done double-checked locking idiom
- # (ThreadSafe::Cache's lookups have volatile semantics)
+ # (Concurrent::Map's lookups have volatile semantics)
@@cache[cache_key] || @@digest_monitor.synchronize do
@@cache.fetch(cache_key) do # re-check under lock
compute_and_store_digest(cache_key, options)
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index ebf5ea5b77..2a367b85af 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -849,8 +849,8 @@ module ActionView
# file_field(:user, :avatar)
# # => <input type="file" id="user_avatar" name="user[avatar]" />
#
- # file_field(:post, :image, :multiple => true)
- # # => <input type="file" id="post_image" name="post[image]" multiple="true" />
+ # file_field(:post, :image, multiple: true)
+ # # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" />
#
# file_field(:post, :attached, accept: 'text/html')
# # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" />
@@ -1038,7 +1038,7 @@ module ActionView
# date_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="date" />
#
- # The default value is generated by trying to call "to_date"
+ # The default value is generated by trying to call +strftime+ with "%Y-%m-%d"
# on the object's value, which makes it behave as expected for instances
# of DateTime and ActiveSupport::TimeWithZone. You can still override that
# by passing the "value" option explicitly, e.g.
@@ -1617,7 +1617,14 @@ module ActionView
@auto_index
end
- record_name = index ? "#{object_name}[#{index}][#{record_name}]" : "#{object_name}[#{record_name}]"
+ record_name = if index
+ "#{object_name}[#{index}][#{record_name}]"
+ elsif record_name.to_s.end_with?('[]')
+ record_name = record_name.to_s.sub(/(.*)\[\]$/, "[\\1][#{record_object.id}]")
+ "#{object_name}#{record_name}"
+ else
+ "#{object_name}[#{record_name}]"
+ end
fields_options[:child_index] = index
@template.fields_for(record_name, record_object, fields_options, &block)
diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb
index 728e633bbf..430051379d 100644
--- a/actionview/lib/action_view/helpers/form_options_helper.rb
+++ b/actionview/lib/action_view/helpers/form_options_helper.rb
@@ -548,7 +548,7 @@ module ActionView
end
# Returns a string of option tags for pretty much any time zone in the
- # world. Supply a ActiveSupport::TimeZone name as +selected+ to have it
+ # world. Supply an ActiveSupport::TimeZone name as +selected+ to have it
# marked as the selected option tag. You can also supply an array of
# ActiveSupport::TimeZone objects as +priority_zones+, so that they will
# be listed above the rest of the (long) list. (You can use
@@ -556,7 +556,7 @@ module ActionView
# of the US time zones, or a Regexp to select the zones of your choice)
#
# The +selected+ parameter must be either +nil+, or a string that names
- # a ActiveSupport::TimeZone.
+ # an ActiveSupport::TimeZone.
#
# By default, +model+ is the ActiveSupport::TimeZone constant (which can
# be obtained in Active Record as a value object). The only requirement
@@ -644,6 +644,24 @@ module ActionView
# collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
# b.label(:"data-value" => b.value) { b.radio_button + b.text }
# end
+ #
+ # ==== Gotcha
+ #
+ # The HTML specification says when nothing is select on a collection of radio buttons
+ # web browsers do not send any value to server.
+ # Unfortunately this introduces a gotcha:
+ # if a +User+ model has a +category_id+ field, and in the form none category is selected no +category_id+ parameter is sent. So,
+ # any strong parameters idiom like
+ #
+ # params.require(:user).permit(...)
+ #
+ # will raise an error since no +{user: ...}+ will be present.
+ #
+ # To prevent this the helper generates an auxiliary hidden field before
+ # every collection of radio buttons. The hidden field has the same name as collection radio button and blank value.
+ #
+ # In case if you don't want the helper to generate this hidden field you can specify
+ # <tt>include_hidden: false</tt> option.
def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
end
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index af684e05c6..0e8127e29e 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -450,9 +450,9 @@ module ActionView
disable_with_text ||= value.clone
tag_options.deep_merge!("data" => { "disable_with" => disable_with_text })
else
- tag_options.delete("data-disable-with")
tag_options["data"].delete(:disable_with) if tag_options["data"]
end
+ tag_options.delete("data-disable-with")
end
tag :input, tag_options
diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb
index 13effa592d..d7182d1fac 100644
--- a/actionview/lib/action_view/helpers/number_helper.rb
+++ b/actionview/lib/action_view/helpers/number_helper.rb
@@ -1,4 +1,3 @@
-
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/string/output_safety'
require 'active_support/number_helper'
@@ -140,7 +139,7 @@ module ActionView
# number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
# number_to_percentage(1000, locale: :fr) # => 1 000,000%
# number_to_percentage("98a") # => 98a%
- # number_to_percentage(100, format: "%n %") # => 100 %
+ # number_to_percentage(100, format: "%n %") # => 100.000 %
#
# number_to_percentage("98a", raise: true) # => InvalidNumberError
def number_to_percentage(number, options = {})
diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
index 1765fa6558..3256d44e18 100644
--- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
@@ -9,29 +9,13 @@ module ActionView
class CheckBoxBuilder < Builder # :nodoc:
def check_box(extra_html_options={})
html_options = extra_html_options.merge(@input_html_options)
+ html_options[:multiple] = true
@template_object.check_box(@object_name, @method_name, html_options, @value, nil)
end
end
def render(&block)
- rendered_collection = render_collection do |item, value, text, default_html_options|
- default_html_options[:multiple] = true
- builder = instantiate_builder(CheckBoxBuilder, item, value, text, default_html_options)
-
- if block_given?
- @template_object.capture(builder, &block)
- else
- render_component(builder)
- end
- end
-
- # Append a hidden field to make sure something will be sent back to the
- # server if all check boxes are unchecked.
- if @options.fetch(:include_hidden, true)
- rendered_collection + hidden_field
- else
- rendered_collection
- end
+ render_collection_for(CheckBoxBuilder, &block)
end
private
@@ -39,11 +23,6 @@ module ActionView
def render_component(builder)
builder.check_box + builder.label
end
-
- def hidden_field
- hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]"
- @template_object.hidden_field_tag(hidden_name, "", id: nil)
- end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
index 8050638363..fea4c8d4ec 100644
--- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
@@ -79,6 +79,32 @@ module ActionView
yield item, value, text, default_html_options.merge(additional_html_options)
end.join.html_safe
end
+
+ def render_collection_for(builder_class, &block) #:nodoc:
+ options = @options.stringify_keys
+ rendered_collection = render_collection do |item, value, text, default_html_options|
+ builder = instantiate_builder(builder_class, item, value, text, default_html_options)
+
+ if block_given?
+ @template_object.capture(builder, &block)
+ else
+ render_component(builder)
+ end
+ end
+
+ # Append a hidden field to make sure something will be sent back to the
+ # server if all radio buttons are unchecked.
+ if options.fetch('include_hidden', true)
+ rendered_collection + hidden_field
+ else
+ rendered_collection
+ end
+ end
+
+ def hidden_field #:nodoc:
+ hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]"
+ @template_object.hidden_field_tag(hidden_name, "", id: nil)
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
index 20be34c1f2..21aaf122f8 100644
--- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
@@ -14,15 +14,7 @@ module ActionView
end
def render(&block)
- render_collection do |item, value, text, default_html_options|
- builder = instantiate_builder(RadioButtonBuilder, item, value, text, default_html_options)
-
- if block_given?
- @template_object.capture(builder, &block)
- else
- render_component(builder)
- end
- end
+ render_collection_for(RadioButtonBuilder, &block)
end
private
diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
index 0615bd2e0d..dde1ef22ac 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -88,7 +88,14 @@ module ActionView
raise e if raise_error
keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
- content_tag('span', keys.last.to_s.titleize, :class => 'translation_missing', :title => "translation missing: #{keys.join('.')}")
+ title = "translation missing: #{keys.join('.')}"
+
+ interpolations = options.except(:default)
+ if interpolations.any?
+ title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(', ')
+ end
+
+ content_tag('span', keys.last.to_s.titleize, class: 'translation_missing', title: title)
end
end
alias :t :translate
diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb
index cabba967c9..a74a5e05f3 100644
--- a/actionview/lib/action_view/layouts.rb
+++ b/actionview/lib/action_view/layouts.rb
@@ -401,7 +401,7 @@ module ActionView
# ==== Parameters
# * <tt>formats</tt> - The formats accepted to this layout
# * <tt>require_layout</tt> - If set to true and layout is not found,
- # an ArgumentError exception is raised (defaults to false)
+ # an +ArgumentError+ exception is raised (defaults to false)
#
# ==== Returns
# * <tt>template</tt> - The template object for the default layout (or nil)
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index fba9a44cb1..ec6edfaaa3 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -1,4 +1,4 @@
-require 'thread_safe'
+require 'concurrent'
require 'active_support/core_ext/module/remove_method'
require 'active_support/core_ext/module/attribute_accessors'
require 'action_view/template/resolver'
@@ -20,7 +20,7 @@ module ActionView
mattr_accessor :registered_details
self.registered_details = []
- def self.register_detail(name, options = {}, &block)
+ def self.register_detail(name, &block)
self.registered_details << name
initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" }
@@ -55,14 +55,14 @@ module ActionView
end
register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] }
register_detail(:variants) { [] }
- register_detail(:handlers){ Template::Handlers.extensions }
+ register_detail(:handlers) { Template::Handlers.extensions }
class DetailsKey #:nodoc:
alias :eql? :equal?
alias :object_hash :hash
attr_reader :hash
- @details_keys = ThreadSafe::Cache.new
+ @details_keys = Concurrent::Map.new
def self.get(details)
if details[:formats]
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index 1f9e960488..39c8658ffe 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -1,5 +1,5 @@
require 'action_view/renderer/partial_renderer/collection_caching'
-require 'thread_safe'
+require 'concurrent'
module ActionView
class PartialIteration
@@ -283,8 +283,8 @@ module ActionView
class PartialRenderer < AbstractRenderer
include CollectionCaching
- PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k|
- h[k] = ThreadSafe::Cache.new
+ PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
+ h[k] = Concurrent::Map.new
end
def initialize(*)
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index 28967f40a6..7859c58b43 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -3,7 +3,7 @@ require "active_support/core_ext/class"
require "active_support/core_ext/module/attribute_accessors"
require "action_view/template"
require "thread"
-require "thread_safe"
+require "concurrent"
module ActionView
# = Action View Resolver
@@ -35,7 +35,7 @@ module ActionView
# Threadsafe template cache
class Cache #:nodoc:
- class SmallCache < ThreadSafe::Cache
+ class SmallCache < Concurrent::Map
def initialize(options = {})
super(options.merge(:initial_capacity => 2))
end
diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb
index 12870acaa6..2354e91822 100644
--- a/actionview/test/abstract_unit.rb
+++ b/actionview/test/abstract_unit.rb
@@ -151,7 +151,6 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
middleware.use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
middleware.use ActionDispatch::DebugExceptions
middleware.use ActionDispatch::Callbacks
- middleware.use ActionDispatch::ParamsParser
middleware.use ActionDispatch::Cookies
middleware.use ActionDispatch::Flash
middleware.use Rack::Head
diff --git a/actionview/test/actionpack/controller/capture_test.rb b/actionview/test/actionpack/controller/capture_test.rb
index f8387b27b0..933456ce9d 100644
--- a/actionview/test/actionpack/controller/capture_test.rb
+++ b/actionview/test/actionpack/controller/capture_test.rb
@@ -54,7 +54,7 @@ class CaptureTest < ActionController::TestCase
assert_equal expected_content_for_output, @response.body
end
- def test_should_concatentate_content_for
+ def test_should_concatenate_content_for
get :content_for_concatenated
assert_equal expected_content_for_output, @response.body
end
diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb
index 8d048ddbcb..27150c7d5f 100644
--- a/actionview/test/actionpack/controller/render_test.rb
+++ b/actionview/test/actionpack/controller/render_test.rb
@@ -1122,7 +1122,7 @@ class RenderTest < ActionController::TestCase
assert_equal "<title>Putting stuff in the title!</title>\nGreat stuff!\n", @response.body
end
- def test_overwritting_rendering_relative_file_with_extension
+ def test_overwriting_rendering_relative_file_with_extension
get :hello_world_from_rxml_using_template
assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body
diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb
index 672b4747ec..3ece9e50cd 100644
--- a/actionview/test/template/dependency_tracker_test.rb
+++ b/actionview/test/template/dependency_tracker_test.rb
@@ -1,4 +1,3 @@
-
require 'abstract_unit'
require 'action_view/dependency_tracker'
diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb
index b193d387c3..41932d15ee 100644
--- a/actionview/test/template/form_collections_helper_test.rb
+++ b/actionview/test/template/form_collections_helper_test.rb
@@ -198,6 +198,41 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_select 'input[type=radio][value=false][checked=checked]'
end
+ test 'collection radio buttons generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 1
+ end
+
+ test 'collection radio buttons generates a hidden field using the given :name in :html_options' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, {}, { name: "user[other_category_ids][]" }
+
+ assert_select "input[type=hidden][name='user[other_category_ids][]'][value='']", count: 1
+ end
+
+ test 'collection radio buttons generates a hidden field with index if it was provided' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, { index: 322 }
+
+ assert_select "input[type=hidden][name='user[322][category_ids][]'][value='']", count: 1
+ end
+
+ test 'collection radio buttons does not generate a hidden field if include_hidden option is false' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, include_hidden: false
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0
+ end
+
+ test 'collection radio buttons does not generate a hidden field if include_hidden option is false with key as string' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, 'include_hidden' => false
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0
+ end
+
# COLLECTION CHECK BOXES
test 'collection check boxes accepts a collection and generate a series of checkboxes for value method' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
@@ -235,6 +270,13 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 0
end
+ test 'collection check boxes does not generate a hidden field if include_hidden option is false with key as string' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name, 'include_hidden' => false
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0
+ end
+
test 'collection check boxes accepts a collection and generate a series of checkboxes with labels for label method' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name
diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb
index aef137935a..41f31f1582 100644
--- a/actionview/test/template/form_helper_test.rb
+++ b/actionview/test/template/form_helper_test.rb
@@ -366,7 +366,7 @@ class FormHelperTest < ActionView::TestCase
)
end
- def test_label_with_to_model_and_overriden_model_name
+ def test_label_with_to_model_and_overridden_model_name
with_locale :label do
assert_dom_equal(
%{<label for="post_delegator_title">Delegate model_name title</label>},
@@ -1596,7 +1596,8 @@ class FormHelperTest < ActionView::TestCase
"<input id='post_active_true' name='post[active]' type='radio' value='true' />" +
"<label for='post_active_true'>true</label>" +
"<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
- "<label for='post_active_false'>false</label>"
+ "<label for='post_active_false'>false</label>" +
+ "<input type='hidden' name='post[active][]' value='' />"
end
assert_dom_equal expected, output_buffer
@@ -1619,7 +1620,8 @@ class FormHelperTest < ActionView::TestCase
"true</label>" +
"<label for='post_active_false'>"+
"<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
- "false</label>"
+ "false</label>" +
+ "<input type='hidden' name='post[active][]' value='' />"
end
assert_dom_equal expected, output_buffer
@@ -1645,6 +1647,7 @@ class FormHelperTest < ActionView::TestCase
"<label for='post_active_false'>"+
"<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
"false</label>"+
+ "<input type='hidden' name='post[active][]' value='' />" +
"<input id='post_id' name='post[id]' type='hidden' value='1' />"
end
@@ -1663,7 +1666,8 @@ class FormHelperTest < ActionView::TestCase
"<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" +
"<label for='foo_post_active_true'>true</label>" +
"<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" +
- "<label for='foo_post_active_false'>false</label>"
+ "<label for='foo_post_active_false'>false</label>" +
+ "<input type='hidden' name='post[active][]' value='' />"
end
assert_dom_equal expected, output_buffer
@@ -1681,7 +1685,8 @@ class FormHelperTest < ActionView::TestCase
"<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" +
"<label for='post_1_active_true'>true</label>" +
"<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" +
- "<label for='post_1_active_false'>false</label>"
+ "<label for='post_1_active_false'>false</label>" +
+ "<input type='hidden' name='post[1][active][]' value='' />"
end
assert_dom_equal expected, output_buffer
@@ -2292,6 +2297,27 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_deep_nested_fields_for
+ @comment.save
+ form_for(:posts) do |f|
+ f.fields_for('post[]', @post) do |f2|
+ f2.text_field(:id)
+ @post.comments.each do |comment|
+ concat f2.fields_for('comment[]', comment) { |c|
+ concat c.text_field(:name)
+ }
+ end
+ end
+ end
+
+ expected = whole_form do
+ "<input name='posts[post][0][comment][1][name]' type='text' id='posts_post_0_comment_1_name' value='comment #1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+
def test_nested_fields_for_with_nested_collections
form_for(@post, as: 'post[]') do |f|
concat f.text_field(:title)
diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb
index a9d9562580..de1eb89dc5 100644
--- a/actionview/test/template/form_tag_helper_test.rb
+++ b/actionview/test/template/form_tag_helper_test.rb
@@ -64,6 +64,18 @@ class FormTagHelperTest < ActionView::TestCase
assert_dom_equal expected, actual
end
+ def test_check_box_tag_disabled
+ actual = check_box_tag "admin","1", false, disabled: true
+ expected = %(<input id="admin" disabled="disabled" name="admin" type="checkbox" value="1" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_check_box_tag_default_checked
+ actual = check_box_tag "admin","1", true
+ expected = %(<input id="admin" checked="checked" name="admin" type="checkbox" value="1" />)
+ assert_dom_equal expected, actual
+ end
+
def test_check_box_tag_id_sanitized
label_elem = root_elem(check_box_tag("project[2][admin]"))
assert_match VALID_HTML_ID, label_elem['id']
@@ -351,12 +363,18 @@ class FormTagHelperTest < ActionView::TestCase
assert_dom_equal expected, actual
end
- def test_text_field_disabled
+ def test_text_field_tag_disabled
actual = text_field_tag "title", "Hello!", disabled: true
expected = %(<input id="title" name="title" disabled="disabled" type="text" value="Hello!" />)
assert_dom_equal expected, actual
end
+ def test_text_field_tag_with_placeholder_option
+ actual = text_field_tag "title", "Hello!", placeholder: 'Enter search term...'
+ expected = %(<input id="title" name="title" placeholder="Enter search term..." type="text" value="Hello!" />)
+ assert_dom_equal expected, actual
+ end
+
def test_text_field_tag_with_multiple_options
actual = text_field_tag "title", "Hello!", :size => 70, :maxlength => 80
expected = %(<input id="title" name="title" size="70" maxlength="80" type="text" value="Hello!" />)
@@ -450,21 +468,21 @@ class FormTagHelperTest < ActionView::TestCase
ActionView::Base.automatically_disable_submit_tag = true
end
- def test_data_disable_with_string
+ def test_submit_tag_having_data_disable_with_string
assert_dom_equal(
%(<input data-disable-with="Processing..." data-confirm="Are you sure?" name='commit' type="submit" value="Save" />),
submit_tag("Save", { "data-disable-with" => "Processing...", "data-confirm" => "Are you sure?" })
)
end
- def test_data_disable_with_boolean
+ def test_submit_tag_having_data_disable_with_boolean
assert_dom_equal(
%(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />),
submit_tag("Save", { "data-disable-with" => false, "data-confirm" => "Are you sure?" })
)
end
- def test_data_hash_disable_with_boolean
+ def test_submit_tag_having_data_hash_disable_with_boolean
assert_dom_equal(
%(<input data-confirm="Are you sure?" name='commit' type="submit" value="Save" />),
submit_tag("Save", { :data => { :confirm => "Are you sure?", :disable_with => false } })
@@ -485,6 +503,14 @@ class FormTagHelperTest < ActionView::TestCase
)
end
+ def test_submit_tag_doesnt_have_data_disable_with_twice
+ assert_equal(
+ %(<input type="submit" name="commit" value="Save" data-confirm="Are you sure?" data-disable-with="Processing..." />),
+ submit_tag("Save", { "data-disable-with" => "Processing...", "data-confirm" => "Are you sure?" })
+ )
+ end
+
+
def test_button_tag
assert_dom_equal(
%(<button name="button" type="submit">Button</button>),
diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb
index 14dafd7d63..1184cf7da8 100644
--- a/actionview/test/template/lookup_context_test.rb
+++ b/actionview/test/template/lookup_context_test.rb
@@ -27,7 +27,7 @@ class LookupContextTest < ActiveSupport::TestCase
end
test "normalizes details on initialization" do
- assert_equal Mime::SET, @lookup_context.formats
+ assert_equal Mime::SET.to_a, @lookup_context.formats
assert_equal :en, @lookup_context.locale
end
@@ -48,11 +48,11 @@ class LookupContextTest < ActiveSupport::TestCase
test "handles */* formats" do
@lookup_context.formats = ["*/*"]
- assert_equal Mime::SET, @lookup_context.formats
+ assert_equal Mime::SET.to_a, @lookup_context.formats
end
test "handles explicitly defined */* formats fallback to :js" do
- @lookup_context.formats = [:js, Mime::ALL]
+ @lookup_context.formats = [:js, Mime::Type[:ALL]]
assert_equal [:js, *Mime::SET.symbols], @lookup_context.formats
end
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index 9c2c9507b7..00fc28a522 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -627,6 +627,16 @@ class CachedCollectionViewRenderTest < CachedViewRenderTest
@view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] })
end
+ test "with caching with custom key and rendering with different key" do
+ customer = Customer.new("david")
+ key = cache_key([customer, 'key'], "test/_customer")
+
+ ActionView::PartialRenderer.collection_cache.write(key, 'Hello')
+
+ assert_equal "Hello: david",
+ @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'another_key'] })
+ end
+
test "automatic caching with inferred cache name" do
customer = CachedCustomer.new("david")
key = cache_key(customer, "test/_cached_customer")
diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb
index 749d0dd7fd..261576bead 100644
--- a/actionview/test/template/translation_helper_test.rb
+++ b/actionview/test/template/translation_helper_test.rb
@@ -60,6 +60,12 @@ class TranslationHelperTest < ActiveSupport::TestCase
assert_equal true, translate(:"translations.missing").html_safe?
end
+ def test_returns_missing_translation_message_with_unescaped_interpolation
+ expected = '<span class="translation_missing" title="translation missing: en.translations.missing, name: Kir, year: 2015, vulnerable: &amp;quot; onclick=&amp;quot;alert()&amp;quot;">Missing</span>'
+ assert_equal expected, translate(:"translations.missing", name: "Kir", year: "2015", vulnerable: %{" onclick="alert()"})
+ assert translate(:"translations.missing").html_safe?
+ end
+
def test_raises_missing_translation_message_with_raise_config_option
ActionView::Base.raise_on_missing_translations = true
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md
index 965b556fab..988815fee2 100644
--- a/activejob/CHANGELOG.md
+++ b/activejob/CHANGELOG.md
@@ -1,3 +1,11 @@
+* Support passing array to `assert_enqueued_jobs` in `:only` option.
+
+ *Wojciech Wnętrzak*
+
+* Add job priorities to Active Job.
+
+ *wvengen*
+
* Implement a simple `AsyncJob` processor and associated `AsyncAdapter` that
queue jobs to a `concurrent-ruby` thread pool.
diff --git a/activejob/lib/active_job/async_job.rb b/activejob/lib/active_job/async_job.rb
index 7fcffc4c24..6c1c070994 100644
--- a/activejob/lib/active_job/async_job.rb
+++ b/activejob/lib/active_job/async_job.rb
@@ -1,5 +1,4 @@
require 'concurrent'
-require 'thread_safe'
module ActiveJob
# == Active Job Async Job
@@ -31,7 +30,7 @@ module ActiveJob
fallback_policy: :caller_runs # shouldn't matter -- 0 max queue
}.freeze
- QUEUES = ThreadSafe::Cache.new do |hash, queue_name| #:nodoc:
+ QUEUES = Concurrent::Map.new do |hash, queue_name| #:nodoc:
hash.compute_if_absent(queue_name) { ActiveJob::AsyncJob.create_thread_pool }
end
diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb
index 5d7c4cfb91..e5f09f65fb 100644
--- a/activejob/lib/active_job/base.rb
+++ b/activejob/lib/active_job/base.rb
@@ -1,6 +1,7 @@
require 'active_job/core'
require 'active_job/queue_adapter'
require 'active_job/queue_name'
+require 'active_job/queue_priority'
require 'active_job/enqueuing'
require 'active_job/execution'
require 'active_job/callbacks'
@@ -57,6 +58,7 @@ module ActiveJob #:nodoc:
include Core
include QueueAdapter
include QueueName
+ include QueuePriority
include Enqueuing
include Execution
include Callbacks
diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb
index eac7279309..19b900a285 100644
--- a/activejob/lib/active_job/core.rb
+++ b/activejob/lib/active_job/core.rb
@@ -18,6 +18,9 @@ module ActiveJob
# Queue in which the job will reside.
attr_writer :queue_name
+ # Priority that the job will have (lower is more priority).
+ attr_writer :priority
+
# ID optionally provided by adapter
attr_accessor :provider_job_id
@@ -43,6 +46,7 @@ module ActiveJob
# * <tt>:wait</tt> - Enqueues the job with the specified delay
# * <tt>:wait_until</tt> - Enqueues the job at the time specified
# * <tt>:queue</tt> - Enqueues the job on the specified queue
+ # * <tt>:priority</tt> - Enqueues the job with the specified priority
#
# ==== Examples
#
@@ -51,6 +55,7 @@ module ActiveJob
# VideoJob.set(wait_until: Time.now.tomorrow).perform_later(Video.last)
# VideoJob.set(queue: :some_queue, wait: 5.minutes).perform_later(Video.last)
# VideoJob.set(queue: :some_queue, wait_until: Time.now.tomorrow).perform_later(Video.last)
+ # VideoJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later(Video.last)
def set(options={})
ConfiguredJob.new(self, options)
end
@@ -62,6 +67,7 @@ module ActiveJob
@arguments = arguments
@job_id = SecureRandom.uuid
@queue_name = self.class.queue_name
+ @priority = self.class.priority
end
# Returns a hash with the job data that can safely be passed to the
@@ -71,6 +77,7 @@ module ActiveJob
'job_class' => self.class.name,
'job_id' => job_id,
'queue_name' => queue_name,
+ 'priority' => priority,
'arguments' => serialize_arguments(arguments),
'locale' => I18n.locale
}
@@ -99,6 +106,7 @@ module ActiveJob
def deserialize(job_data)
self.job_id = job_data['job_id']
self.queue_name = job_data['queue_name']
+ self.priority = job_data['priority']
self.serialized_arguments = job_data['arguments']
self.locale = job_data['locale'] || I18n.locale
end
diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb
index 98d92385dd..22154457fd 100644
--- a/activejob/lib/active_job/enqueuing.rb
+++ b/activejob/lib/active_job/enqueuing.rb
@@ -32,6 +32,7 @@ module ActiveJob
# * <tt>:wait</tt> - Enqueues the job with the specified delay
# * <tt>:wait_until</tt> - Enqueues the job at the time specified
# * <tt>:queue</tt> - Enqueues the job on the specified queue
+ # * <tt>:priority</tt> - Enqueues the job with the specified priority
#
# ==== Examples
#
@@ -54,6 +55,7 @@ module ActiveJob
# * <tt>:wait</tt> - Enqueues the job with the specified delay
# * <tt>:wait_until</tt> - Enqueues the job at the time specified
# * <tt>:queue</tt> - Enqueues the job on the specified queue
+ # * <tt>:priority</tt> - Enqueues the job with the specified priority
#
# ==== Examples
#
@@ -61,10 +63,12 @@ module ActiveJob
# my_job_instance.enqueue wait: 5.minutes
# my_job_instance.enqueue queue: :important
# my_job_instance.enqueue wait_until: Date.tomorrow.midnight
+ # my_job_instance.enqueue priority: 10
def enqueue(options={})
self.scheduled_at = options[:wait].seconds.from_now.to_f if options[:wait]
self.scheduled_at = options[:wait_until].to_f if options[:wait_until]
self.queue_name = self.class.queue_name_from_part(options[:queue]) if options[:queue]
+ self.priority = options[:priority].to_i if options[:priority]
run_callbacks :enqueue do
if self.scheduled_at
self.class.queue_adapter.enqueue_at self, self.scheduled_at
diff --git a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
index ac83da2b9c..0a785fad3b 100644
--- a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
@@ -14,13 +14,13 @@ module ActiveJob
# Rails.application.config.active_job.queue_adapter = :delayed_job
class DelayedJobAdapter
def enqueue(job) #:nodoc:
- delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name)
+ delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, priority: job.priority)
job.provider_job_id = delayed_job.id
delayed_job
end
def enqueue_at(job, timestamp) #:nodoc:
- delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, run_at: Time.at(timestamp))
+ delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, priority: job.priority, run_at: Time.at(timestamp))
job.provider_job_id = delayed_job.id
delayed_job
end
diff --git a/activejob/lib/active_job/queue_adapters/que_adapter.rb b/activejob/lib/active_job/queue_adapters/que_adapter.rb
index 90947aa98d..ab13689747 100644
--- a/activejob/lib/active_job/queue_adapters/que_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/que_adapter.rb
@@ -16,13 +16,13 @@ module ActiveJob
# Rails.application.config.active_job.queue_adapter = :que
class QueAdapter
def enqueue(job) #:nodoc:
- que_job = JobWrapper.enqueue job.serialize
+ que_job = JobWrapper.enqueue job.serialize, priority: job.priority
job.provider_job_id = que_job.attrs["job_id"]
que_job
end
def enqueue_at(job, timestamp) #:nodoc:
- que_job = JobWrapper.enqueue job.serialize, run_at: Time.at(timestamp)
+ que_job = JobWrapper.enqueue job.serialize, priority: job.priority, run_at: Time.at(timestamp)
job.provider_job_id = que_job.attrs["job_id"]
que_job
end
diff --git a/activejob/lib/active_job/queue_priority.rb b/activejob/lib/active_job/queue_priority.rb
new file mode 100644
index 0000000000..01d84910ff
--- /dev/null
+++ b/activejob/lib/active_job/queue_priority.rb
@@ -0,0 +1,44 @@
+module ActiveJob
+ module QueuePriority
+ extend ActiveSupport::Concern
+
+ # Includes the ability to override the default queue priority.
+ module ClassMethods
+ mattr_accessor(:default_priority)
+
+ # Specifies the priority of the queue to create the job with.
+ #
+ # class PublishToFeedJob < ActiveJob::Base
+ # queue_with_priority 50
+ #
+ # def perform(post)
+ # post.to_feed!
+ # end
+ # end
+ #
+ # Specify either an argument or a block.
+ def queue_with_priority(priority=nil, &block)
+ if block_given?
+ self.priority = block
+ else
+ self.priority = priority
+ end
+ end
+ end
+
+ included do
+ class_attribute :priority, instance_accessor: false
+
+ self.priority = default_priority
+ end
+
+ # Returns the priority that the job will be created with
+ def priority
+ if @priority.is_a?(Proc)
+ @priority = instance_exec(&@priority)
+ end
+ @priority
+ end
+
+ end
+end
diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb
index 200d82838e..666e984fe0 100644
--- a/activejob/lib/active_job/test_helper.rb
+++ b/activejob/lib/active_job/test_helper.rb
@@ -290,23 +290,23 @@ module ActiveJob
to: :queue_adapter
private
- def clear_enqueued_jobs
+ def clear_enqueued_jobs # :nodoc:
enqueued_jobs.clear
end
- def clear_performed_jobs
+ def clear_performed_jobs # :nodoc:
performed_jobs.clear
end
- def enqueued_jobs_size(only: nil)
+ def enqueued_jobs_size(only: nil) # :nodoc:
if only
- enqueued_jobs.select { |job| job.fetch(:job) == only }.size
+ enqueued_jobs.select { |job| Array(only).include?(job.fetch(:job)) }.size
else
enqueued_jobs.size
end
end
- def serialize_args_for_assertion(args)
+ def serialize_args_for_assertion(args) # :nodoc:
serialized_args = args.dup
if job_args = serialized_args.delete(:args)
serialized_args[:args] = ActiveJob::Arguments.serialize(job_args)
@@ -314,7 +314,7 @@ module ActiveJob
serialized_args
end
- def instantiate_job(payload)
+ def instantiate_job(payload) # :nodoc:
job = payload[:job].new(*payload[:args])
job.scheduled_at = Time.at(payload[:at]) if payload.key?(:at)
job.queue_name = payload[:queue]
diff --git a/activejob/test/cases/queue_priority_test.rb b/activejob/test/cases/queue_priority_test.rb
new file mode 100644
index 0000000000..ca17b51dad
--- /dev/null
+++ b/activejob/test/cases/queue_priority_test.rb
@@ -0,0 +1,47 @@
+require 'helper'
+require 'jobs/hello_job'
+
+class QueuePriorityTest < ActiveSupport::TestCase
+ test 'priority unset by default' do
+ assert_equal nil, HelloJob.priority
+ end
+
+ test 'uses given priority' do
+ original_priority = HelloJob.priority
+
+ begin
+ HelloJob.queue_with_priority 90
+ assert_equal 90, HelloJob.new.priority
+ ensure
+ HelloJob.priority = original_priority
+ end
+ end
+
+ test 'evals block given to priority to determine priority' do
+ original_priority = HelloJob.priority
+
+ begin
+ HelloJob.queue_with_priority { 25 }
+ assert_equal 25, HelloJob.new.priority
+ ensure
+ HelloJob.priority = original_priority
+ end
+ end
+
+ test 'can use arguments to determine priority in priority block' do
+ original_priority = HelloJob.priority
+
+ begin
+ HelloJob.queue_with_priority { self.arguments.first=='1' ? 99 : 11 }
+ assert_equal 99, HelloJob.new('1').priority
+ assert_equal 11, HelloJob.new('3').priority
+ ensure
+ HelloJob.priority = original_priority
+ end
+ end
+
+ test 'uses priority passed to #set' do
+ job = HelloJob.set(priority: 123).perform_later
+ assert_equal 123, job.priority
+ end
+end
diff --git a/activejob/test/cases/test_case_test.rb b/activejob/test/cases/test_case_test.rb
index ee816e1dd5..616454a4b6 100644
--- a/activejob/test/cases/test_case_test.rb
+++ b/activejob/test/cases/test_case_test.rb
@@ -9,7 +9,7 @@ class ActiveJobTestCaseTest < ActiveJob::TestCase
# the `class_attribute` inheritance
class TestClassAttributeInheritanceJob < ActiveJob::Base
def self.queue_adapter=(*)
- raise 'Attemping to break `class_attribute` inheritance, bad!'
+ raise 'Attempting to break `class_attribute` inheritance, bad!'
end
end
diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb
index 8c60f037d2..18cf35562b 100644
--- a/activejob/test/cases/test_helper_test.rb
+++ b/activejob/test/cases/test_helper_test.rb
@@ -140,6 +140,16 @@ class EnqueuedJobsTest < ActiveJob::TestCase
assert_match(/1 .* but 2/, error.message)
end
+ def test_assert_enqueued_jobs_with_only_option_as_array
+ assert_nothing_raised do
+ assert_enqueued_jobs 2, only: [HelloJob, LoggingJob] do
+ HelloJob.perform_later('jeremy')
+ LoggingJob.perform_later('stewie')
+ RescueJob.perform_later('david')
+ end
+ end
+ end
+
def test_assert_no_enqueued_jobs_with_only_option
assert_nothing_raised do
assert_no_enqueued_jobs only: HelloJob do
@@ -159,6 +169,14 @@ class EnqueuedJobsTest < ActiveJob::TestCase
assert_match(/0 .* but 1/, error.message)
end
+ def test_assert_no_enqueued_jobs_with_only_option_as_array
+ assert_nothing_raised do
+ assert_no_enqueued_jobs only: [HelloJob, RescueJob] do
+ LoggingJob.perform_later
+ end
+ end
+ end
+
def test_assert_enqueued_job
assert_enqueued_with(job: LoggingJob, queue: 'default') do
LoggingJob.set(wait_until: Date.tomorrow.noon).perform_later
diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb
index 55f690bda8..7e86415f48 100644
--- a/activejob/test/helper.rb
+++ b/activejob/test/helper.rb
@@ -11,6 +11,7 @@ GlobalID.app = 'aj'
if ENV['AJ_INTEGRATION_TESTS']
require 'support/integration/helper'
else
+ ActiveJob::Base.logger = Logger.new(nil)
require "adapters/#{@adapter}"
end
diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb
index 125ba54302..e435ed4aa6 100644
--- a/activejob/test/integration/queuing_test.rb
+++ b/activejob/test/integration/queuing_test.rb
@@ -84,4 +84,16 @@ class QueuingTest < ActiveSupport::TestCase
I18n.locale = :en
end
end
+
+ test 'should run job with higher priority first' do
+ skip unless adapter_is?(:delayed_job, :que)
+
+ wait_until = Time.now + 3.seconds
+ TestJob.set(wait_until: wait_until, priority: 20).perform_later "#{@id}.1"
+ TestJob.set(wait_until: wait_until, priority: 10).perform_later "#{@id}.2"
+ wait_for_jobs_to_finish_for(10.seconds)
+ assert job_executed "#{@id}.1"
+ assert job_executed "#{@id}.2"
+ assert job_executed_at("#{@id}.2") < job_executed_at("#{@id}.1")
+ end
end
diff --git a/activejob/test/support/integration/adapters/sidekiq.rb b/activejob/test/support/integration/adapters/sidekiq.rb
index d9938600f2..9aa07bcb52 100644
--- a/activejob/test/support/integration/adapters/sidekiq.rb
+++ b/activejob/test/support/integration/adapters/sidekiq.rb
@@ -58,7 +58,7 @@ module SidekiqJobsManager
timeout: 1,
})
Sidekiq.average_scheduled_poll_interval = 0.5
- Sidekiq::Scheduled.const_set :INITIAL_WAIT, 1
+ Sidekiq.options[:poll_interval_average] = 1
begin
sidekiq.run
continue_write.puts "started"
diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb
index 4ffdb8cffa..0c062a025e 100644
--- a/activejob/test/support/integration/dummy_app_template.rb
+++ b/activejob/test/support/integration/dummy_app_template.rb
@@ -1,8 +1,9 @@
if ENV['AJ_ADAPTER'] == 'delayed_job'
generate "delayed_job:active_record", "--quiet"
- rake("db:migrate")
end
+rake("db:migrate")
+
initializer 'activejob.rb', <<-CODE
require "#{File.expand_path("../jobs_manager.rb", __FILE__)}"
JobsManager.current_manager.setup
diff --git a/activejob/test/support/integration/helper.rb b/activejob/test/support/integration/helper.rb
index 8c2e5a86c2..4a1b0bfbcb 100644
--- a/activejob/test/support/integration/helper.rb
+++ b/activejob/test/support/integration/helper.rb
@@ -1,4 +1,4 @@
-puts "*** rake aj:integration:#{ENV['AJ_ADAPTER']} ***\n"
+puts "\n\n*** rake aj:integration:#{ENV['AJ_ADAPTER']} ***\n"
ENV["RAILS_ENV"] = "test"
ActiveJob::Base.queue_name_prefix = nil
diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb
index 39aee6d407..8319d09520 100644
--- a/activejob/test/support/integration/test_case_helpers.rb
+++ b/activejob/test/support/integration/test_case_helpers.rb
@@ -42,8 +42,12 @@ module TestCaseHelpers
end
end
- def job_executed
- Dummy::Application.root.join("tmp/#{@id}").exist?
+ def job_executed(id=@id)
+ Dummy::Application.root.join("tmp/#{id}").exist?
+ end
+
+ def job_executed_at(id=@id)
+ File.new(Dummy::Application.root.join("tmp/#{id}")).ctime
end
def job_output
diff --git a/activejob/test/support/que/inline.rb b/activejob/test/support/que/inline.rb
index 0232da1370..0950e52d28 100644
--- a/activejob/test/support/que/inline.rb
+++ b/activejob/test/support/que/inline.rb
@@ -6,6 +6,7 @@ Que::Job.class_eval do
if args.last.is_a?(Hash)
options = args.pop
options.delete(:run_at)
+ options.delete(:priority)
args << options unless options.empty?
end
self.run(*args)
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index bf0120122d..a3368cd197 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -119,10 +119,10 @@
The preferred method to halt a callback chain from now on is to explicitly
`throw(:abort)`.
- In the past, returning `false` in an ActiveModel or ActiveModel::Validations
- `before_` callback had the side effect of halting the callback chain.
+ In the past, returning `false` in an Active Model `before_` callback had
+ the side effect of halting the callback chain.
This is not recommended anymore and, depending on the value of the
- `config.active_support.halt_callback_chains_on_return_false` option, will
+ `ActiveSupport.halt_callback_chains_on_return_false` option, will
either not work at all or display a deprecation warning.
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 77e4ce3afe..1963a3fc4e 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -1,4 +1,4 @@
-require 'thread_safe'
+require 'concurrent'
require 'mutex_m'
module ActiveModel
@@ -350,7 +350,7 @@ module ActiveModel
# significantly (in our case our test suite finishes 10% faster with
# this cache).
def attribute_method_matchers_cache #:nodoc:
- @attribute_method_matchers_cache ||= ThreadSafe::Cache.new(initial_capacity: 4)
+ @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
end
def attribute_method_matchers_matching(method_name) #:nodoc:
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb
index 2cf39b68fb..0d6a3dc52d 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -103,6 +103,7 @@ module ActiveModel
def define_model_callbacks(*callbacks)
options = callbacks.extract_options!
options = {
+ terminator: deprecated_false_terminator,
skip_after_callbacks_if_terminated: true,
scope: [:kind, :name],
only: [:before, :around, :after]
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 0169c20e0b..0ab8df42f5 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -203,7 +203,7 @@ module ActiveModel
# Returns +true+ if attr_name were changed before the model was saved,
# +false+ otherwise.
def previous_changes_include?(attr_name)
- @previously_changed.include?(attr_name)
+ previous_changes.include?(attr_name)
end
# Removes current changes and makes them accessible through +previous_changes+.
@@ -225,7 +225,7 @@ module ActiveModel
# Handles <tt>*_previous_change</tt> for +method_missing+.
def attribute_previous_change(attr)
- @previously_changed[attr] if attribute_previously_changed?(attr)
+ previous_changes[attr] if attribute_previously_changed?(attr)
end
# Handles <tt>*_will_change!</tt> for +method_missing+.
diff --git a/activemodel/lib/active_model/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb
index b4fa378601..d2c6a89cc2 100644
--- a/activemodel/lib/active_model/forbidden_attributes_protection.rb
+++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb
@@ -17,8 +17,9 @@ module ActiveModel
module ForbiddenAttributesProtection # :nodoc:
protected
def sanitize_for_mass_assignment(attributes)
- if attributes.respond_to?(:permitted?) && !attributes.permitted?
- raise ActiveModel::ForbiddenAttributesError
+ if attributes.respond_to?(:permitted?)
+ raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
+ attributes.to_h
else
attributes
end
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 213f2d5b6a..de5fb27467 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -164,7 +164,7 @@ module ActiveModel
@route_key << "_index" if @plural == @singular
end
- # Transform the model name into a more humane format, using I18n. By default,
+ # Transform the model name into a more human format, using I18n. By default,
# it will underscore then humanize the class name.
#
# class BlogPost
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
index 341651d00d..70e10fa06d 100644
--- a/activemodel/lib/active_model/serialization.rb
+++ b/activemodel/lib/active_model/serialization.rb
@@ -31,9 +31,9 @@ module ActiveModel
# of the attributes hash's keys. In order to override this behavior, take a look
# at the private method +read_attribute_for_serialization+.
#
- # The JSON serialization is provided by default when you include the
- # <tt>ActiveModel::Serialization</tt> module, so there is no need to explicitly
- # include it.
+ # ActiveModel::Serializers::JSON module automatically includes
+ # the <tt>ActiveModel::Serialization</tt> module, so there is no need to
+ # explicitly include <tt>ActiveModel::Serialization</tt>.
#
# A minimal implementation including JSON would be:
#
diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb
new file mode 100644
index 0000000000..f8ca7d0512
--- /dev/null
+++ b/activemodel/lib/active_model/type.rb
@@ -0,0 +1,57 @@
+require 'active_model/type/helpers'
+require 'active_model/type/value'
+
+require 'active_model/type/big_integer'
+require 'active_model/type/binary'
+require 'active_model/type/boolean'
+require 'active_model/type/date'
+require 'active_model/type/date_time'
+require 'active_model/type/decimal'
+require 'active_model/type/decimal_without_scale'
+require 'active_model/type/float'
+require 'active_model/type/integer'
+require 'active_model/type/string'
+require 'active_model/type/text'
+require 'active_model/type/time'
+require 'active_model/type/unsigned_integer'
+
+require 'active_model/type/registry'
+
+module ActiveModel
+ module Type
+ @registry = Registry.new
+
+ class << self
+ attr_accessor :registry # :nodoc:
+ delegate :add_modifier, to: :registry
+
+ # Add a new type to the registry, allowing it to be referenced as a
+ # symbol by ActiveModel::Attributes::ClassMethods#attribute. If your
+ # type is only meant to be used with a specific database adapter, you can
+ # do so by passing +adapter: :postgresql+. If your type has the same
+ # name as a native type for the current adapter, an exception will be
+ # raised unless you specify an +:override+ option. +override: true+ will
+ # cause your type to be used instead of the native type. +override:
+ # false+ will cause the native type to be used over yours if one exists.
+ def register(type_name, klass = nil, **options, &block)
+ registry.register(type_name, klass, **options, &block)
+ end
+
+ def lookup(*args, **kwargs) # :nodoc:
+ registry.lookup(*args, **kwargs)
+ end
+ end
+
+ register(:big_integer, Type::BigInteger)
+ register(:binary, Type::Binary)
+ register(:boolean, Type::Boolean)
+ register(:date, Type::Date)
+ register(:date_time, Type::DateTime)
+ register(:decimal, Type::Decimal)
+ register(:float, Type::Float)
+ register(:integer, Type::Integer)
+ register(:string, Type::String)
+ register(:text, Type::Text)
+ register(:time, Type::Time)
+ end
+end
diff --git a/activerecord/lib/active_record/type/big_integer.rb b/activemodel/lib/active_model/type/big_integer.rb
index 0c72d8914f..4168cbfce7 100644
--- a/activerecord/lib/active_record/type/big_integer.rb
+++ b/activemodel/lib/active_model/type/big_integer.rb
@@ -1,6 +1,6 @@
-require 'active_record/type/integer'
+require 'active_model/type/integer'
-module ActiveRecord
+module ActiveModel
module Type
class BigInteger < Integer # :nodoc:
private
diff --git a/activerecord/lib/active_record/type/binary.rb b/activemodel/lib/active_model/type/binary.rb
index 0baf8c63ad..a0cc45b4c3 100644
--- a/activerecord/lib/active_record/type/binary.rb
+++ b/activemodel/lib/active_model/type/binary.rb
@@ -1,4 +1,4 @@
-module ActiveRecord
+module ActiveModel
module Type
class Binary < Value # :nodoc:
def type
diff --git a/activerecord/lib/active_record/type/boolean.rb b/activemodel/lib/active_model/type/boolean.rb
index f6a75512fd..c1bce98c87 100644
--- a/activerecord/lib/active_record/type/boolean.rb
+++ b/activemodel/lib/active_model/type/boolean.rb
@@ -1,6 +1,8 @@
-module ActiveRecord
+module ActiveModel
module Type
class Boolean < Value # :nodoc:
+ FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set
+
def type
:boolean
end
@@ -11,7 +13,7 @@ module ActiveRecord
if value == ''
nil
else
- !ConnectionAdapters::Column::FALSE_VALUES.include?(value)
+ !FALSE_VALUES.include?(value)
end
end
end
diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb
new file mode 100644
index 0000000000..f74243a22c
--- /dev/null
+++ b/activemodel/lib/active_model/type/date.rb
@@ -0,0 +1,50 @@
+module ActiveModel
+ module Type
+ class Date < Value # :nodoc:
+ include Helpers::AcceptsMultiparameterTime.new
+
+ def type
+ :date
+ end
+
+ def type_cast_for_schema(value)
+ "'#{value.to_s(:db)}'"
+ end
+
+ private
+
+ def cast_value(value)
+ if value.is_a?(::String)
+ return if value.empty?
+ fast_string_to_date(value) || fallback_string_to_date(value)
+ elsif value.respond_to?(:to_date)
+ value.to_date
+ else
+ value
+ end
+ end
+
+ ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
+ def fast_string_to_date(string)
+ if string =~ ISO_DATE
+ new_date $1.to_i, $2.to_i, $3.to_i
+ end
+ end
+
+ def fallback_string_to_date(string)
+ new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
+ end
+
+ def new_date(year, mon, mday)
+ if year && year != 0
+ ::Date.new(year, mon, mday) rescue nil
+ end
+ end
+
+ def value_from_multiparameter_assignment(*)
+ time = super
+ time && time.to_date
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/type/date_time.rb b/activemodel/lib/active_model/type/date_time.rb
new file mode 100644
index 0000000000..2f2df4320f
--- /dev/null
+++ b/activemodel/lib/active_model/type/date_time.rb
@@ -0,0 +1,44 @@
+module ActiveModel
+ module Type
+ class DateTime < Value # :nodoc:
+ include Helpers::TimeValue
+ include Helpers::AcceptsMultiparameterTime.new(
+ defaults: { 4 => 0, 5 => 0 }
+ )
+
+ def type
+ :datetime
+ end
+
+ private
+
+ def cast_value(value)
+ return apply_seconds_precision(value) unless value.is_a?(::String)
+ return if value.empty?
+
+ fast_string_to_time(value) || fallback_string_to_time(value)
+ end
+
+ # '0.123456' -> 123456
+ # '1.123456' -> 123456
+ def microseconds(time)
+ time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
+ end
+
+ def fallback_string_to_time(string)
+ time_hash = ::Date._parse(string)
+ time_hash[:sec_fraction] = microseconds(time_hash)
+
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
+ end
+
+ def value_from_multiparameter_assignment(values_hash)
+ missing_parameter = (1..3).detect { |key| !values_hash.key?(key) }
+ if missing_parameter
+ raise ArgumentError, missing_parameter
+ end
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb
index f5b145230d..d19d8baada 100644
--- a/activerecord/lib/active_record/type/decimal.rb
+++ b/activemodel/lib/active_model/type/decimal.rb
@@ -1,4 +1,6 @@
-module ActiveRecord
+require "bigdecimal/util"
+
+module ActiveModel
module Type
class Decimal < Value # :nodoc:
include Helpers::Numeric
diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activemodel/lib/active_model/type/decimal_without_scale.rb
index ff5559e300..129baa0c10 100644
--- a/activerecord/lib/active_record/type/decimal_without_scale.rb
+++ b/activemodel/lib/active_model/type/decimal_without_scale.rb
@@ -1,6 +1,6 @@
-require 'active_record/type/big_integer'
+require 'active_model/type/big_integer'
-module ActiveRecord
+module ActiveModel
module Type
class DecimalWithoutScale < BigInteger # :nodoc:
def type
diff --git a/activerecord/lib/active_record/type/float.rb b/activemodel/lib/active_model/type/float.rb
index d88482b85d..0f925bc7e1 100644
--- a/activerecord/lib/active_record/type/float.rb
+++ b/activemodel/lib/active_model/type/float.rb
@@ -1,4 +1,4 @@
-module ActiveRecord
+module ActiveModel
module Type
class Float < Value # :nodoc:
include Helpers::Numeric
diff --git a/activemodel/lib/active_model/type/helpers.rb b/activemodel/lib/active_model/type/helpers.rb
new file mode 100644
index 0000000000..a805a359ab
--- /dev/null
+++ b/activemodel/lib/active_model/type/helpers.rb
@@ -0,0 +1,4 @@
+require 'active_model/type/helpers/accepts_multiparameter_time'
+require 'active_model/type/helpers/numeric'
+require 'active_model/type/helpers/mutable'
+require 'active_model/type/helpers/time_value'
diff --git a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
index be571fc1c7..facea12704 100644
--- a/activerecord/lib/active_record/type/helpers/accepts_multiparameter_time.rb
+++ b/activemodel/lib/active_model/type/helpers/accepts_multiparameter_time.rb
@@ -1,4 +1,4 @@
-module ActiveRecord
+module ActiveModel
module Type
module Helpers
class AcceptsMultiparameterTime < Module # :nodoc:
@@ -11,16 +11,21 @@ module ActiveRecord
end
end
+ define_method(:assert_valid_value) do |value|
+ if value.is_a?(Hash)
+ value_from_multiparameter_assignment(value)
+ else
+ super(value)
+ end
+ end
+
define_method(:value_from_multiparameter_assignment) do |values_hash|
defaults.each do |k, v|
values_hash[k] ||= v
end
return unless values_hash[1] && values_hash[2] && values_hash[3]
values = values_hash.sort.map(&:last)
- ::Time.send(
- ActiveRecord::Base.default_timezone,
- *values
- )
+ ::Time.send(default_timezone, *values)
end
private :value_from_multiparameter_assignment
end
diff --git a/activerecord/lib/active_record/type/helpers/mutable.rb b/activemodel/lib/active_model/type/helpers/mutable.rb
index 88a9099277..4dddbe4e5e 100644
--- a/activerecord/lib/active_record/type/helpers/mutable.rb
+++ b/activemodel/lib/active_model/type/helpers/mutable.rb
@@ -1,4 +1,4 @@
-module ActiveRecord
+module ActiveModel
module Type
module Helpers
module Mutable # :nodoc:
diff --git a/activerecord/lib/active_record/type/helpers/numeric.rb b/activemodel/lib/active_model/type/helpers/numeric.rb
index a755a02a59..c883010506 100644
--- a/activerecord/lib/active_record/type/helpers/numeric.rb
+++ b/activemodel/lib/active_model/type/helpers/numeric.rb
@@ -1,4 +1,4 @@
-module ActiveRecord
+module ActiveModel
module Type
module Helpers
module Numeric # :nodoc:
diff --git a/activerecord/lib/active_record/type/helpers/time_value.rb b/activemodel/lib/active_model/type/helpers/time_value.rb
index 7eb41557cb..63993c0d93 100644
--- a/activerecord/lib/active_record/type/helpers/time_value.rb
+++ b/activemodel/lib/active_model/type/helpers/time_value.rb
@@ -1,16 +1,14 @@
-module ActiveRecord
+require "active_support/core_ext/time/zones"
+
+module ActiveModel
module Type
module Helpers
module TimeValue # :nodoc:
def serialize(value)
- if precision && value.respond_to?(:usec)
- number_of_insignificant_digits = 6 - precision
- round_power = 10 ** number_of_insignificant_digits
- value = value.change(usec: value.usec / round_power * round_power)
- end
+ value = apply_seconds_precision(value)
if value.acts_like?(:time)
- zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
+ zone_conversion_method = is_utc? ? :getutc : :getlocal
if value.respond_to?(zone_conversion_method)
value = value.send(zone_conversion_method)
@@ -20,6 +18,25 @@ module ActiveRecord
value
end
+ def is_utc?
+ ::Time.zone_default.nil? || ::Time.zone_default =~ 'UTC'
+ end
+
+ def default_timezone
+ if is_utc?
+ :utc
+ else
+ :local
+ end
+ end
+
+ def apply_seconds_precision(value)
+ return value unless precision && value.respond_to?(:usec)
+ number_of_insignificant_digits = 6 - precision
+ round_power = 10 ** number_of_insignificant_digits
+ value.change(usec: value.usec / round_power * round_power)
+ end
+
def type_cast_for_schema(value)
"'#{value.to_s(:db)}'"
end
@@ -39,15 +56,17 @@ module ActiveRecord
return unless time
time -= offset
- Base.default_timezone == :utc ? time : time.getlocal
+ is_utc? ? time : time.getlocal
else
- ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ ::Time.public_send(default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
end
end
+ ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
+
# Doesn't handle time zones.
def fast_string_to_time(string)
- if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME
+ if string =~ ISO_DATETIME
microsec = ($7.to_r * 1_000_000).to_i
new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
end
diff --git a/activerecord/lib/active_record/type/integer.rb b/activemodel/lib/active_model/type/integer.rb
index c5040c6d3b..2f73ede009 100644
--- a/activerecord/lib/active_record/type/integer.rb
+++ b/activemodel/lib/active_model/type/integer.rb
@@ -1,4 +1,4 @@
-module ActiveRecord
+module ActiveModel
module Type
class Integer < Value # :nodoc:
include Helpers::Numeric
diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb
new file mode 100644
index 0000000000..adc88eb624
--- /dev/null
+++ b/activemodel/lib/active_model/type/registry.rb
@@ -0,0 +1,64 @@
+module ActiveModel
+ # :stopdoc:
+ module Type
+ class Registry
+ def initialize
+ @registrations = []
+ end
+
+ def register(type_name, klass = nil, **options, &block)
+ block ||= proc { |_, *args| klass.new(*args) }
+ registrations << registration_klass.new(type_name, block, **options)
+ end
+
+ def lookup(symbol, *args)
+ registration = find_registration(symbol, *args)
+
+ if registration
+ registration.call(self, symbol, *args)
+ else
+ raise ArgumentError, "Unknown type #{symbol.inspect}"
+ end
+ end
+
+ protected
+
+ attr_reader :registrations
+
+ private
+
+ def registration_klass
+ Registration
+ end
+
+ def find_registration(symbol, *args)
+ registrations.find { |r| r.matches?(symbol, *args) }
+ end
+ end
+
+ class Registration
+ # Options must be taken because of https://bugs.ruby-lang.org/issues/10856
+ def initialize(name, block, **)
+ @name = name
+ @block = block
+ end
+
+ def call(_registry, *args, **kwargs)
+ if kwargs.any? # https://bugs.ruby-lang.org/issues/10856
+ block.call(*args, **kwargs)
+ else
+ block.call(*args)
+ end
+ end
+
+ def matches?(type_name, *args, **kwargs)
+ type_name == name
+ end
+
+ protected
+
+ attr_reader :name, :block
+ end
+ end
+ # :startdoc:
+end
diff --git a/activerecord/lib/active_record/type/string.rb b/activemodel/lib/active_model/type/string.rb
index 2662b7e874..fd1630c751 100644
--- a/activerecord/lib/active_record/type/string.rb
+++ b/activemodel/lib/active_model/type/string.rb
@@ -1,4 +1,4 @@
-module ActiveRecord
+module ActiveModel
module Type
class String < Value # :nodoc:
def type
diff --git a/activerecord/lib/active_record/type/text.rb b/activemodel/lib/active_model/type/text.rb
index 26f980f060..1ad04daba4 100644
--- a/activerecord/lib/active_record/type/text.rb
+++ b/activemodel/lib/active_model/type/text.rb
@@ -1,6 +1,6 @@
-require 'active_record/type/string'
+require 'active_model/type/string'
-module ActiveRecord
+module ActiveModel
module Type
class Text < String # :nodoc:
def type
diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb
new file mode 100644
index 0000000000..7101bad566
--- /dev/null
+++ b/activemodel/lib/active_model/type/time.rb
@@ -0,0 +1,42 @@
+module ActiveModel
+ module Type
+ class Time < Value # :nodoc:
+ include Helpers::TimeValue
+ include Helpers::AcceptsMultiparameterTime.new(
+ defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
+ )
+
+ def type
+ :time
+ end
+
+ def user_input_in_time_zone(value)
+ return unless value.present?
+
+ case value
+ when ::String
+ value = "2000-01-01 #{value}"
+ when ::Time
+ value = value.change(year: 2000, day: 1, month: 1)
+ end
+
+ super(value)
+ end
+
+ private
+
+ def cast_value(value)
+ return value unless value.is_a?(::String)
+ return if value.empty?
+
+ dummy_time_value = "2000-01-01 #{value}"
+
+ fast_string_to_time(dummy_time_value) || begin
+ time_hash = ::Date._parse(dummy_time_value)
+ return if time_hash[:hour].nil?
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activemodel/lib/active_model/type/unsigned_integer.rb
index ed3e527483..3f49f9f5f7 100644
--- a/activerecord/lib/active_record/type/unsigned_integer.rb
+++ b/activemodel/lib/active_model/type/unsigned_integer.rb
@@ -1,4 +1,4 @@
-module ActiveRecord
+module ActiveModel
module Type
class UnsignedInteger < Integer # :nodoc:
private
diff --git a/activerecord/lib/active_record/type/value.rb b/activemodel/lib/active_model/type/value.rb
index 6b9d147ecc..5fea0561a6 100644
--- a/activerecord/lib/active_record/type/value.rb
+++ b/activemodel/lib/active_model/type/value.rb
@@ -1,4 +1,4 @@
-module ActiveRecord
+module ActiveModel
module Type
class Value
attr_reader :precision, :scale, :limit
@@ -91,6 +91,9 @@ module ActiveRecord
limit == other.limit
end
+ def assert_valid_value(*)
+ end
+
private
# Convenience method for types which do not need separate type casting
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index 1bcfedb35d..c5c0cd4636 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -14,16 +14,63 @@ module ActiveModel
end
private
+
def setup!(klass)
- attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
- attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
- klass.send(:attr_reader, *attr_readers)
- klass.send(:attr_writer, *attr_writers)
+ klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes)))
end
def acceptable_option?(value)
Array(options[:accept]).include?(value)
end
+
+ class LazilyDefineAttributes < Module
+ def initialize(attribute_definition)
+ define_method(:respond_to_missing?) do |method_name, include_private=false|
+ super(method_name, include_private) || attribute_definition.matches?(method_name)
+ end
+
+ define_method(:method_missing) do |method_name, *args, &block|
+ if attribute_definition.matches?(method_name)
+ attribute_definition.define_on(self.class)
+ send(method_name, *args, &block)
+ else
+ super(method_name, *args, &block)
+ end
+ end
+ end
+ end
+
+ class AttributeDefinition
+ def initialize(attributes)
+ @attributes = attributes.map(&:to_s)
+ end
+
+ def matches?(method_name)
+ attr_name = convert_to_reader_name(method_name)
+ attributes.include?(attr_name)
+ end
+
+ def define_on(klass)
+ attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
+ attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
+ klass.send(:attr_reader, *attr_readers)
+ klass.send(:attr_writer, *attr_writers)
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def convert_to_reader_name(method_name)
+ attr_name = method_name.to_s
+ if attr_name.end_with?("=")
+ attr_name = attr_name[0..-2]
+ end
+ attr_name
+ end
+ end
end
module HelperMethods
diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb
index 4b58ef66e3..52111e5442 100644
--- a/activemodel/lib/active_model/validations/callbacks.rb
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -23,6 +23,7 @@ module ActiveModel
included do
include ActiveSupport::Callbacks
define_callbacks :validation,
+ terminator: deprecated_false_terminator,
skip_after_callbacks_if_terminated: true,
scope: [:kind, :name]
end
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
index bb2f0bd064..8f8ade90bb 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -9,7 +9,7 @@ module ActiveModel
def validate_each(record, attribute, value)
if (confirmed = record.send("#{attribute}_confirmation"))
- unless confimation_value_equal?(record, attribute, value, confirmed)
+ unless confirmation_value_equal?(record, attribute, value, confirmed)
human_attribute_name = record.class.human_attribute_name(attribute)
record.errors.add(:"#{attribute}_confirmation", :confirmation, options.except(:case_sensitive).merge!(attribute: human_attribute_name))
end
@@ -27,7 +27,7 @@ module ActiveModel
end.compact)
end
- def confimation_value_equal?(record, attribute, value, confirmed)
+ def confirmation_value_equal?(record, attribute, value, confirmed)
if !options[:case_sensitive] && value.is_a?(String)
value.casecmp(confirmed) == 0
else
diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb
index 64a85e01eb..3336691841 100644
--- a/activemodel/test/cases/attribute_assignment_test.rb
+++ b/activemodel/test/cases/attribute_assignment_test.rb
@@ -70,7 +70,7 @@ class AttributeAssignmentTest < ActiveModel::TestCase
end
end
- test "an ArgumentError is raised if a non-hash-like obejct is passed" do
+ test "an ArgumentError is raised if a non-hash-like object is passed" do
assert_raises(ArgumentError) do
Model.new(1)
end
diff --git a/activerecord/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb
index 01ee36b892..353dbf84ad 100644
--- a/activerecord/test/cases/type/decimal_test.rb
+++ b/activemodel/test/cases/type/decimal_test.rb
@@ -1,8 +1,9 @@
require "cases/helper"
+require "active_model/type"
-module ActiveRecord
+module ActiveModel
module Type
- class DecimalTest < ActiveRecord::TestCase
+ class DecimalTest < ActiveModel::TestCase
def test_type_cast_decimal
type = Decimal.new
assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0"))
diff --git a/activemodel/test/cases/type/integer_test.rb b/activemodel/test/cases/type/integer_test.rb
new file mode 100644
index 0000000000..dac922db42
--- /dev/null
+++ b/activemodel/test/cases/type/integer_test.rb
@@ -0,0 +1,108 @@
+require "cases/helper"
+require "active_model/type"
+
+module ActiveModel
+ module Type
+ class IntegerTest < ActiveModel::TestCase
+ test "simple values" do
+ type = Type::Integer.new
+ assert_equal 1, type.cast(1)
+ assert_equal 1, type.cast('1')
+ assert_equal 1, type.cast('1ignore')
+ assert_equal 0, type.cast('bad1')
+ assert_equal 0, type.cast('bad')
+ assert_equal 1, type.cast(1.7)
+ assert_equal 0, type.cast(false)
+ assert_equal 1, type.cast(true)
+ assert_nil type.cast(nil)
+ end
+
+ test "random objects cast to nil" do
+ type = Type::Integer.new
+ assert_nil type.cast([1,2])
+ assert_nil type.cast({1 => 2})
+ assert_nil type.cast(1..2)
+ end
+
+ test "casting objects without to_i" do
+ type = Type::Integer.new
+ assert_nil type.cast(::Object.new)
+ end
+
+ test "casting nan and infinity" do
+ type = Type::Integer.new
+ assert_nil type.cast(::Float::NAN)
+ assert_nil type.cast(1.0/0.0)
+ end
+
+ test "casting booleans for database" do
+ type = Type::Integer.new
+ assert_equal 1, type.serialize(true)
+ assert_equal 0, type.serialize(false)
+ end
+
+ test "changed?" do
+ type = Type::Integer.new
+
+ assert type.changed?(5, 5, '5wibble')
+ assert_not type.changed?(5, 5, '5')
+ assert_not type.changed?(5, 5, '5.0')
+ assert_not type.changed?(-5, -5, '-5')
+ assert_not type.changed?(-5, -5, '-5.0')
+ assert_not type.changed?(nil, nil, nil)
+ end
+
+ test "values below int min value are out of range" do
+ assert_raises(::RangeError) do
+ Integer.new.serialize(-2147483649)
+ end
+ end
+
+ test "values above int max value are out of range" do
+ assert_raises(::RangeError) do
+ Integer.new.serialize(2147483648)
+ end
+ end
+
+ test "very small numbers are out of range" do
+ assert_raises(::RangeError) do
+ Integer.new.serialize(-9999999999999999999999999999999)
+ end
+ end
+
+ test "very large numbers are out of range" do
+ assert_raises(::RangeError) do
+ Integer.new.serialize(9999999999999999999999999999999)
+ end
+ end
+
+ test "normal numbers are in range" do
+ type = Integer.new
+ assert_equal(0, type.serialize(0))
+ assert_equal(-1, type.serialize(-1))
+ assert_equal(1, type.serialize(1))
+ end
+
+ test "int max value is in range" do
+ assert_equal(2147483647, Integer.new.serialize(2147483647))
+ end
+
+ test "int min value is in range" do
+ assert_equal(-2147483648, Integer.new.serialize(-2147483648))
+ end
+
+ test "columns with a larger limit have larger ranges" do
+ type = Integer.new(limit: 8)
+
+ assert_equal(9223372036854775807, type.serialize(9223372036854775807))
+ assert_equal(-9223372036854775808, type.serialize(-9223372036854775808))
+ assert_raises(::RangeError) do
+ type.serialize(-9999999999999999999999999999999)
+ end
+ assert_raises(::RangeError) do
+ type.serialize(9999999999999999999999999999999)
+ end
+ end
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/registry_test.rb b/activemodel/test/cases/type/registry_test.rb
new file mode 100644
index 0000000000..2a48998a62
--- /dev/null
+++ b/activemodel/test/cases/type/registry_test.rb
@@ -0,0 +1,39 @@
+require "cases/helper"
+require "active_model/type"
+
+module ActiveModel
+ class RegistryTest < ActiveModel::TestCase
+ test "a class can be registered for a symbol" do
+ registry = Type::Registry.new
+ registry.register(:foo, ::String)
+ registry.register(:bar, ::Array)
+
+ assert_equal "", registry.lookup(:foo)
+ assert_equal [], registry.lookup(:bar)
+ end
+
+ test "a block can be registered" do
+ registry = Type::Registry.new
+ registry.register(:foo) do |*args|
+ [*args, "block for foo"]
+ end
+ registry.register(:bar) do |*args|
+ [*args, "block for bar"]
+ end
+
+ assert_equal [:foo, 1, "block for foo"], registry.lookup(:foo, 1)
+ assert_equal [:foo, 2, "block for foo"], registry.lookup(:foo, 2)
+ assert_equal [:bar, 1, 2, 3, "block for bar"], registry.lookup(:bar, 1, 2, 3)
+ end
+
+ test "a reasonable error is given when no type is found" do
+ registry = Type::Registry.new
+
+ e = assert_raises(ArgumentError) do
+ registry.lookup(:foo)
+ end
+
+ assert_equal "Unknown type :foo", e.message
+ end
+ end
+end
diff --git a/activemodel/test/cases/type/string_test.rb b/activemodel/test/cases/type/string_test.rb
new file mode 100644
index 0000000000..8ec771ea42
--- /dev/null
+++ b/activemodel/test/cases/type/string_test.rb
@@ -0,0 +1,20 @@
+require "cases/helper"
+require "active_model/type"
+
+module ActiveModel
+ class StringTypeTest < ActiveModel::TestCase
+ test "type casting" do
+ type = Type::String.new
+ assert_equal "t", type.cast(true)
+ assert_equal "f", type.cast(false)
+ assert_equal "123", type.cast(123)
+ end
+
+ test "values are duped coming out" do
+ s = "foo"
+ type = Type::String.new
+ assert_not_same s, type.cast(s)
+ assert_not_same s, type.deserialize(s)
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/unsigned_integer_test.rb b/activemodel/test/cases/type/unsigned_integer_test.rb
index f2c910eade..16301b3ac0 100644
--- a/activerecord/test/cases/type/unsigned_integer_test.rb
+++ b/activemodel/test/cases/type/unsigned_integer_test.rb
@@ -1,8 +1,9 @@
require "cases/helper"
+require "active_model/type"
-module ActiveRecord
+module ActiveModel
module Type
- class UnsignedIntegerTest < ActiveRecord::TestCase
+ class UnsignedIntegerTest < ActiveModel::TestCase
test "unsigned int max value is in range" do
assert_equal(4294967295, UnsignedInteger.new.serialize(4294967295))
end
diff --git a/activemodel/test/cases/types_test.rb b/activemodel/test/cases/types_test.rb
new file mode 100644
index 0000000000..f937208580
--- /dev/null
+++ b/activemodel/test/cases/types_test.rb
@@ -0,0 +1,122 @@
+require "cases/helper"
+require "active_model/type"
+require "active_support/core_ext/numeric/time"
+
+module ActiveModel
+ class TypesTest < ActiveModel::TestCase
+ def test_type_cast_boolean
+ type = Type::Boolean.new
+ assert type.cast('').nil?
+ assert type.cast(nil).nil?
+
+ assert type.cast(true)
+ assert type.cast(1)
+ assert type.cast('1')
+ assert type.cast('t')
+ assert type.cast('T')
+ assert type.cast('true')
+ assert type.cast('TRUE')
+ assert type.cast('on')
+ assert type.cast('ON')
+ assert type.cast(' ')
+ assert type.cast("\u3000\r\n")
+ assert type.cast("\u0000")
+ assert type.cast('SOMETHING RANDOM')
+
+ # explicitly check for false vs nil
+ assert_equal false, type.cast(false)
+ assert_equal false, type.cast(0)
+ assert_equal false, type.cast('0')
+ assert_equal false, type.cast('f')
+ assert_equal false, type.cast('F')
+ assert_equal false, type.cast('false')
+ assert_equal false, type.cast('FALSE')
+ assert_equal false, type.cast('off')
+ assert_equal false, type.cast('OFF')
+ end
+
+ def test_type_cast_float
+ type = Type::Float.new
+ assert_equal 1.0, type.cast("1")
+ end
+
+ def test_changing_float
+ type = Type::Float.new
+
+ assert type.changed?(5.0, 5.0, '5wibble')
+ assert_not type.changed?(5.0, 5.0, '5')
+ assert_not type.changed?(5.0, 5.0, '5.0')
+ assert_not type.changed?(nil, nil, nil)
+ end
+
+ def test_type_cast_binary
+ type = Type::Binary.new
+ assert_equal nil, type.cast(nil)
+ assert_equal "1", type.cast("1")
+ assert_equal 1, type.cast(1)
+ end
+
+ def test_type_cast_time
+ type = Type::Time.new
+ assert_equal nil, type.cast(nil)
+ assert_equal nil, type.cast('')
+ assert_equal nil, type.cast('ABC')
+
+ time_string = Time.now.utc.strftime("%T")
+ assert_equal time_string, type.cast(time_string).strftime("%T")
+ end
+
+ def test_type_cast_datetime_and_timestamp
+ type = Type::DateTime.new
+ assert_equal nil, type.cast(nil)
+ assert_equal nil, type.cast('')
+ assert_equal nil, type.cast(' ')
+ assert_equal nil, type.cast('ABC')
+
+ datetime_string = Time.now.utc.strftime("%FT%T")
+ assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T")
+ end
+
+ def test_type_cast_date
+ type = Type::Date.new
+ assert_equal nil, type.cast(nil)
+ assert_equal nil, type.cast('')
+ assert_equal nil, type.cast(' ')
+ assert_equal nil, type.cast('ABC')
+
+ date_string = Time.now.utc.strftime("%F")
+ assert_equal date_string, type.cast(date_string).strftime("%F")
+ end
+
+ def test_type_cast_duration_to_integer
+ type = Type::Integer.new
+ assert_equal 1800, type.cast(30.minutes)
+ assert_equal 7200, type.cast(2.hours)
+ end
+
+ def test_string_to_time_with_timezone
+ ["UTC", "US/Eastern"].each do |zone|
+ with_timezone_config default: zone do
+ type = Type::DateTime.new
+ assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT")
+ end
+ end
+ end
+
+ def test_type_equality
+ assert_equal Type::Value.new, Type::Value.new
+ assert_not_equal Type::Value.new, Type::Integer.new
+ assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2)
+ end
+
+ private
+
+ def with_timezone_config(default:)
+ old_zone_default = ::Time.zone_default
+ ::Time.zone_default = ::Time.find_zone(default)
+ yield
+ ensure
+ ::Time.zone_default = old_zone_default
+ end
+ end
+end
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 918097449b..6a40d32ef9 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,105 @@
+* Allow fixtures files to set the model class in the YAML file itself.
+
+ To load the fixtures file `accounts.yml` as the `User` model, use:
+
+ _fixture:
+ model_class: User
+ david:
+ name: David
+
+ Fixes #9516.
+
+ *Roque Pinel*
+
+* Don't require a database connection to load a class which uses acceptance
+ validations.
+
+ *Sean Griffin*
+
+* Correctly apply `unscope` when preloading through associations.
+
+ *Jimmy Bourassa*
+
+* Fixed taking precision into count when assigning a value to timestamp attribute
+
+ Timestamp column can have less precision than ruby timestamp
+ In result in how big a fraction of a second can be stored in the
+ database.
+
+
+ m = Model.create!
+ m.created_at.usec == m.reload.created_at.usec
+ # => false
+ # due to different precision in Time.now and database column
+
+ If the precision is low enough, (mysql default is 0, so it is always low
+ enough by default) the value changes when model is reloaded from the
+ database. This patch fixes that issue ensuring that any timestamp
+ assigned as an attribute is converted to column precision under the
+ attribute.
+
+ *Bogdan Gusiev*
+
+* Introduce `connection.data_sources` and `connection.data_source_exists?`.
+ These methods determine what relations can be used to back Active Record
+ models (usually tables and views).
+
+ Also deprecate `SchemaCache#tables`, `SchemaCache#table_exists?` and
+ `SchemaCache#clear_table_cache!` in favor of their new data source
+ counterparts.
+
+ *Yves Senn*, *Matthew Draper*
+
+* Add `ActiveRecord::Base.ignored_columns` to make some columns
+ invisible from ActiveRecord.
+
+ *Jean Boussier*
+
+* `ActiveRecord::Tasks::MySQLDatabaseTasks` fails if shellout to
+ mysql commands (like `mysqldump`) is not successful.
+
+ *Steve Mitchell*
+
+* Ensure `select` quotes aliased attributes, even when using `from`.
+
+ Fixes #21488
+
+ *Sean Griffin & @johanlunds*
+
+* MySQL: support `unsigned` numeric data types.
+
+ Example:
+
+ create_table :foos do |t|
+ t.unsigned_integer :quantity
+ t.unsigned_bigint :total
+ t.unsigned_float :percentage
+ t.unsigned_decimal :price, precision: 10, scale: 2
+ end
+
+ The `unsigned: true` option may be used for the primary key:
+
+ create_table :foos, id: :bigint, unsigned: true do |t|
+ …
+ end
+
+ *Ryuta Kamizono*
+
+* Add `#views` and `#view_exists?` methods on connection adapters.
+
+ *Ryuta Kamizono*
+
+* Correctly dump composite primary key.
+
+ Example:
+
+ create_table :barcodes, primary_key: ["region", "code"] do |t|
+ t.string :region
+ t.integer :code
+ end
+
+ *Ryuta Kamizono*
+
* Lookup the attribute name for `restrict_with_error` messages on the
model class that defines the association.
@@ -1015,7 +1117,7 @@
In the past, returning `false` in an Active Record `before_` callback had the
side effect of halting the callback chain.
This is not recommended anymore and, depending on the value of the
- `config.active_support.halt_callback_chains_on_return_false` option, will
+ `ActiveSupport.halt_callback_chains_on_return_false` option, will
either not work at all or display a deprecation warning.
*claudiob*
diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc
index 049c5d2b3b..3eac8cc422 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -26,7 +26,7 @@ The Product class is automatically mapped to the table named "products",
which might look like this:
CREATE TABLE products (
- id int(11) NOT NULL auto_increment,
+ id int NOT NULL auto_increment,
name varchar(255),
PRIMARY KEY (id)
);
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 65eb6fba27..cf3e63b4a3 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -446,14 +446,14 @@ module ActiveRecord
# The tables for these classes could look something like:
#
# CREATE TABLE users (
- # id int(11) NOT NULL auto_increment,
- # account_id int(11) default NULL,
+ # id int NOT NULL auto_increment,
+ # account_id int default NULL,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
#
# CREATE TABLE accounts (
- # id int(11) NOT NULL auto_increment,
+ # id int NOT NULL auto_increment,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 2b7e4f28c5..021bc32237 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -2,8 +2,7 @@ require 'active_support/core_ext/string/conversions'
module ActiveRecord
module Associations
- # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
- # ActiveRecord::Associations::ThroughAssociationScope
+ # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency
class AliasTracker # :nodoc:
attr_reader :aliases
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 79b6b80d84..9994b72158 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -127,7 +127,7 @@ module ActiveRecord
# # ]
#
# person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
- # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4
+ # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=4
#
# person.pets.find(2) { |pet| pet.name.downcase! }
# # => #<Pet id: 2, name: "fancy-fancy", person_id: 1>
@@ -443,7 +443,7 @@ module ActiveRecord
# person.pets.delete_all
#
# Pet.find(1, 2, 3)
- # # => ActiveRecord::RecordNotFound
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
#
# If it is set to <tt>:delete_all</tt>, all the objects are deleted
# *without* calling their +destroy+ method.
@@ -463,7 +463,7 @@ module ActiveRecord
# person.pets.delete_all
#
# Pet.find(1, 2, 3)
- # # => ActiveRecord::RecordNotFound
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
def delete_all(dependent = nil)
@association.delete_all(dependent)
end
@@ -557,7 +557,7 @@ module ActiveRecord
# # => [#<Pet id: 2, name: "Spook", person_id: 1>]
#
# Pet.find(1, 3)
- # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3)
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 3)
#
# If it is set to <tt>:delete_all</tt>, all the +records+ are deleted
# *without* calling their +destroy+ method.
@@ -585,7 +585,7 @@ module ActiveRecord
# # ]
#
# Pet.find(1)
- # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1
+ # # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=1
#
# You can pass +Fixnum+ or +String+ values, it finds the records
# responding to the +id+ and executes delete on them.
@@ -649,7 +649,7 @@ module ActiveRecord
# person.pets.size # => 0
# person.pets # => []
#
- # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3)
+ # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
#
# You can pass +Fixnum+ or +String+ values, it finds the records
# responding to the +id+ and then deletes them from the database.
@@ -681,7 +681,7 @@ module ActiveRecord
# person.pets.size # => 0
# person.pets # => []
#
- # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6)
+ # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6)
def destroy(*records)
@association.destroy(*records)
end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 38bda0d2a5..7da20d8eea 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -103,7 +103,7 @@ module ActiveRecord
counter = reflection.counter_cache_column
owner[counter] ||= 0
owner[counter] += difference
- owner.send(:clear_attribute_changes, counter) # eww
+ owner.send(:clear_attribute_change, counter) # eww
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 1dc8bff193..92792a7a15 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -154,7 +154,7 @@ module ActiveRecord
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
- scope.unscope_values = Array(values[:unscope])
+ scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope])
klass.default_scoped.merge(scope)
end
end
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
index 73dd3fa041..3c4c8f10ec 100644
--- a/activerecord/lib/active_record/attribute.rb
+++ b/activerecord/lib/active_record/attribute.rb
@@ -5,8 +5,8 @@ module ActiveRecord
FromDatabase.new(name, value, type)
end
- def from_user(name, value, type)
- FromUser.new(name, value, type)
+ def from_user(name, value, type, original_attribute = nil)
+ FromUser.new(name, value, type, original_attribute)
end
def with_cast_value(name, value, type)
@@ -26,36 +26,46 @@ module ActiveRecord
# This method should not be called directly.
# Use #from_database or #from_user
- def initialize(name, value_before_type_cast, type)
+ def initialize(name, value_before_type_cast, type, original_attribute = nil)
@name = name
@value_before_type_cast = value_before_type_cast
@type = type
+ @original_attribute = original_attribute
end
def value
# `defined?` is cheaper than `||=` when we get back falsy values
- @value = original_value unless defined?(@value)
+ @value = type_cast(value_before_type_cast) unless defined?(@value)
@value
end
def original_value
- type_cast(value_before_type_cast)
+ if assigned?
+ original_attribute.original_value
+ else
+ type_cast(value_before_type_cast)
+ end
end
def value_for_database
type.serialize(value)
end
- def changed_from?(old_value)
- type.changed?(old_value, value, value_before_type_cast)
+ def changed?
+ changed_from_assignment? || changed_in_place?
+ end
+
+ def changed_in_place?
+ has_been_read? && type.changed_in_place?(original_value_for_database, value)
end
- def changed_in_place_from?(old_value)
- has_been_read? && type.changed_in_place?(old_value, value)
+ def forgetting_assignment
+ with_value_from_database(value_for_database)
end
def with_value_from_user(value)
- self.class.from_user(name, value, type)
+ type.assert_valid_value(value)
+ self.class.from_user(name, value, type, self)
end
def with_value_from_database(value)
@@ -67,7 +77,7 @@ module ActiveRecord
end
def with_type(type)
- self.class.new(name, value_before_type_cast, type)
+ self.class.new(name, value_before_type_cast, type, original_attribute)
end
def type_cast(*)
@@ -100,16 +110,39 @@ module ActiveRecord
protected
+ attr_reader :original_attribute
+ alias_method :assigned?, :original_attribute
+
def initialize_dup(other)
if defined?(@value) && @value.duplicable?
@value = @value.dup
end
end
+ def changed_from_assignment?
+ assigned? && type.changed?(original_value, value, value_before_type_cast)
+ end
+
+ def original_value_for_database
+ if assigned?
+ original_attribute.original_value_for_database
+ else
+ _original_value_for_database
+ end
+ end
+
+ def _original_value_for_database
+ value_for_database
+ end
+
class FromDatabase < Attribute # :nodoc:
def type_cast(value)
type.deserialize(value)
end
+
+ def _original_value_for_database
+ value_before_type_cast
+ end
end
class FromUser < Attribute # :nodoc:
diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb
index e0bee8c17e..fb8ad9163e 100644
--- a/activerecord/lib/active_record/attribute/user_provided_default.rb
+++ b/activerecord/lib/active_record/attribute/user_provided_default.rb
@@ -4,8 +4,7 @@ module ActiveRecord
class Attribute # :nodoc:
class UserProvidedDefault < FromUser
def initialize(name, value, type, database_default)
- super(name, value, type)
- @database_default = database_default
+ super(name, value, type, database_default)
end
def type_cast(value)
@@ -16,17 +15,9 @@ module ActiveRecord
end
end
- def changed_in_place_from?(old_value)
- super || changed_from?(database_default.value)
- end
-
def with_type(type)
- self.class.new(name, value_before_type_cast, type, database_default)
+ self.class.new(name, value_before_type_cast, type, original_attribute)
end
-
- protected
-
- attr_reader :database_default
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 0862306749..ca6ba18fe0 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,7 +1,7 @@
require 'active_support/core_ext/enumerable'
require 'active_support/core_ext/string/filters'
require 'mutex_m'
-require 'thread_safe'
+require 'concurrent'
module ActiveRecord
# = Active Record Attribute Methods
@@ -37,7 +37,7 @@ module ActiveRecord
class AttributeMethodCache
def initialize
@module = Module.new
- @method_cache = ThreadSafe::Cache.new
+ @method_cache = Concurrent::Map.new
end
def [](name)
@@ -96,7 +96,7 @@ module ActiveRecord
end
end
- # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
+ # Raises an <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
# \Active \Record method is defined in the model, otherwise +false+.
#
# class Person < ActiveRecord::Base
@@ -106,7 +106,7 @@ module ActiveRecord
# end
#
# Person.instance_method_already_implemented?(:save)
- # # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord
+ # # => ActiveRecord::DangerousAttributeError: save is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name.
#
# Person.instance_method_already_implemented?(:name)
# # => false
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 0171ef3bdf..0bcfa5f00d 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/module/attribute_accessors'
+require 'active_record/attribute_mutation_tracker'
module ActiveRecord
module AttributeMethods
@@ -34,23 +35,43 @@ module ActiveRecord
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- clear_changes_information
+ @mutation_tracker = nil
+ @previous_mutation_tracker = nil
+ @changed_attributes = HashWithIndifferentAccess.new
end
end
def initialize_dup(other) # :nodoc:
super
- calculate_changes_from_defaults
+ @attributes = self.class._default_attributes.map do |attr|
+ attr.with_value_from_user(@attributes.fetch_value(attr.name))
+ end
+ @mutation_tracker = nil
end
def changes_applied
- super
- store_original_raw_attributes
+ @previous_mutation_tracker = mutation_tracker
+ @changed_attributes = HashWithIndifferentAccess.new
+ store_original_attributes
end
def clear_changes_information
+ @previous_mutation_tracker = nil
+ @changed_attributes = HashWithIndifferentAccess.new
+ store_original_attributes
+ end
+
+ def raw_write_attribute(attr_name, *)
+ result = super
+ clear_attribute_change(attr_name)
+ result
+ end
+
+ def clear_attribute_changes(attr_names)
super
- original_raw_attributes.clear
+ attr_names.each do |attr_name|
+ clear_attribute_change(attr_name)
+ end
end
def changed_attributes
@@ -59,7 +80,7 @@ module ActiveRecord
if defined?(@cached_changed_attributes)
@cached_changed_attributes
else
- super.reverse_merge(attributes_changed_in_place).freeze
+ super.reverse_merge(mutation_tracker.changed_values).freeze
end
end
@@ -69,59 +90,29 @@ module ActiveRecord
end
end
+ def previous_changes
+ previous_mutation_tracker.changes
+ end
+
def attribute_changed_in_place?(attr_name)
- old_value = original_raw_attribute(attr_name)
- @attributes[attr_name].changed_in_place_from?(old_value)
+ mutation_tracker.changed_in_place?(attr_name)
end
private
- def changes_include?(attr_name)
- super || attribute_changed_in_place?(attr_name)
- end
-
- def calculate_changes_from_defaults
- @changed_attributes = nil
- self.class.column_defaults.each do |attr, orig_value|
- set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value)
+ def mutation_tracker
+ unless defined?(@mutation_tracker)
+ @mutation_tracker = nil
end
+ @mutation_tracker ||= AttributeMutationTracker.new(@attributes)
end
- # Wrap write_attribute to remember original attribute value.
- def write_attribute(attr, value)
- attr = attr.to_s
-
- old_value = old_attribute_value(attr)
-
- result = super
- store_original_raw_attribute(attr)
- save_changed_attribute(attr, old_value)
- result
- end
-
- def raw_write_attribute(attr, value)
- attr = attr.to_s
-
- result = super
- original_raw_attributes[attr] = value
- result
+ def changes_include?(attr_name)
+ super || mutation_tracker.changed?(attr_name)
end
- def save_changed_attribute(attr, old_value)
- clear_changed_attributes_cache
- if attribute_changed_by_setter?(attr)
- clear_attribute_changes(attr) unless _field_changed?(attr, old_value)
- else
- set_attribute_was(attr, old_value) if _field_changed?(attr, old_value)
- end
- end
-
- def old_attribute_value(attr)
- if attribute_changed?(attr)
- changed_attributes[attr]
- else
- clone_attribute_value(:_read_attribute, attr)
- end
+ def clear_attribute_change(attr_name)
+ mutation_tracker.forget_change(attr_name)
end
def _update_record(*)
@@ -136,41 +127,13 @@ module ActiveRecord
changed & self.class.column_names
end
- def _field_changed?(attr, old_value)
- @attributes[attr].changed_from?(old_value)
- end
-
- def attributes_changed_in_place
- changed_in_place.each_with_object({}) do |attr_name, h|
- orig = @attributes[attr_name].original_value
- h[attr_name] = orig
- end
- end
-
- def changed_in_place
- self.class.attribute_names.select do |attr_name|
- attribute_changed_in_place?(attr_name)
- end
- end
-
- def original_raw_attribute(attr_name)
- original_raw_attributes.fetch(attr_name) do
- read_attribute_before_type_cast(attr_name)
- end
+ def store_original_attributes
+ @attributes = @attributes.map(&:forgetting_assignment)
+ @mutation_tracker = nil
end
- def original_raw_attributes
- @original_raw_attributes ||= {}
- end
-
- def store_original_raw_attribute(attr_name)
- original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database rescue nil
- end
-
- def store_original_raw_attributes
- attribute_names.each do |attr|
- store_original_raw_attribute(attr)
- end
+ def previous_mutation_tracker
+ @previous_mutation_tracker ||= NullMutationTracker.instance
end
def cache_changed_attributes
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 553122a5fc..10498f4322 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -19,7 +19,7 @@ module ActiveRecord
if Numeric === value || value !~ /[^0-9]/
!value.to_i.zero?
else
- return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
+ return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
!value.blank?
end
elsif value.respond_to?(:zero?)
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index f9beb43e4b..9e693b6aee 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -13,7 +13,7 @@ module ActiveRecord
set_time_zone_without_conversion(super)
elsif value.respond_to?(:in_time_zone)
begin
- user_input_in_time_zone(value) || super
+ super(user_input_in_time_zone(value)) || super
rescue ArgumentError
nil
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 07d5e7d38e..bbf2a51a0e 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -45,7 +45,7 @@ module ActiveRecord
write_attribute_with_type_cast(attr_name, value, true)
end
- def raw_write_attribute(attr_name, value)
+ def raw_write_attribute(attr_name, value) # :nodoc:
write_attribute_with_type_cast(attr_name, value, false)
end
diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb
new file mode 100644
index 0000000000..0133b4d0be
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb
@@ -0,0 +1,70 @@
+module ActiveRecord
+ class AttributeMutationTracker # :nodoc:
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def changed_values
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
+ if changed?(attr_name)
+ result[attr_name] = attributes[attr_name].original_value
+ end
+ end
+ end
+
+ def changes
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
+ if changed?(attr_name)
+ result[attr_name] = [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
+ end
+ end
+ end
+
+ def changed?(attr_name)
+ attr_name = attr_name.to_s
+ attributes[attr_name].changed?
+ end
+
+ def changed_in_place?(attr_name)
+ attributes[attr_name].changed_in_place?
+ end
+
+ def forget_change(attr_name)
+ attr_name = attr_name.to_s
+ attributes[attr_name] = attributes[attr_name].forgetting_assignment
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def attr_names
+ attributes.keys
+ end
+ end
+
+ class NullMutationTracker # :nodoc:
+ include Singleton
+
+ def changed_values
+ {}
+ end
+
+ def changes
+ {}
+ end
+
+ def changed?(*)
+ false
+ end
+
+ def changed_in_place?(*)
+ false
+ end
+
+ def forget_change(*)
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
index 013a7d0e01..026b3cf014 100644
--- a/activerecord/lib/active_record/attribute_set.rb
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -60,8 +60,14 @@ module ActiveRecord
super
end
+ def deep_dup
+ dup.tap do |copy|
+ copy.instance_variable_set(:@attributes, attributes.deep_dup)
+ end
+ end
+
def initialize_dup(_)
- @attributes = attributes.deep_dup
+ @attributes = attributes.dup
super
end
@@ -80,6 +86,11 @@ module ActiveRecord
attributes.select { |_, attr| attr.has_been_read? }.keys
end
+ def map(&block)
+ new_attributes = attributes.transform_values(&block)
+ AttributeSet.new(new_attributes)
+ end
+
protected
attr_reader :attributes
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
index 0c730d313f..f974b7a876 100644
--- a/activerecord/lib/active_record/attribute_set/builder.rb
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -47,8 +47,14 @@ module ActiveRecord
delegate_hash[key] = value
end
+ def deep_dup
+ dup.tap do |copy|
+ copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
+ end
+ end
+
def initialize_dup(_)
- @delegate_hash = delegate_hash.transform_values(&:dup)
+ @delegate_hash = Hash[delegate_hash]
super
end
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index 9ea22ed798..2456b8ad8c 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -14,10 +14,7 @@ module ActiveRecord
def dump(obj)
return if obj.nil?
- unless obj.is_a?(object_class)
- raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
- end
+ assert_valid_value(obj)
YAML.dump obj
end
@@ -26,15 +23,19 @@ module ActiveRecord
return yaml unless yaml.is_a?(String) && yaml =~ /^---/
obj = YAML.load(yaml)
- unless obj.is_a?(object_class) || obj.nil?
- raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
- end
+ assert_valid_value(obj)
obj ||= object_class.new if object_class != Object
obj
end
+ def assert_valid_value(obj)
+ unless obj.nil? || obj.is_a?(object_class)
+ raise SerializationTypeMismatch,
+ "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}"
+ end
+ end
+
private
def check_arity_of_constructor
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 6544b64f52..b579bc1e93 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -1,5 +1,5 @@
require 'thread'
-require 'thread_safe'
+require 'concurrent'
require 'monitor'
module ActiveRecord
@@ -337,7 +337,7 @@ module ActiveRecord
# that case +conn.owner+ attr should be consulted.
# Access and modification of +@thread_cached_conns+ does not require
# synchronization.
- @thread_cached_conns = ThreadSafe::Cache.new(:initial_capacity => @size)
+ @thread_cached_conns = Concurrent::Map.new(:initial_capacity => @size)
@connections = []
@automatic_reconnect = true
@@ -824,11 +824,11 @@ module ActiveRecord
# These caches are keyed by klass.name, NOT klass. Keying them by klass
# alone would lead to memory leaks in development mode as all previous
# instances of the class would stay in memory.
- @owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
- h[k] = ThreadSafe::Cache.new(:initial_capacity => 2)
+ @owner_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k|
+ h[k] = Concurrent::Map.new(:initial_capacity => 2)
end
- @class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
- h[k] = ThreadSafe::Cache.new
+ @class_to_pool = Concurrent::Map.new(:initial_capacity => 2) do |h,k|
+ h[k] = Concurrent::Map.new
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
index 18d943f452..0ba4d94e3c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -14,8 +14,10 @@ module ActiveRecord
send m, o
end
- delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, to: :@conn
- private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql
+ delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
+ :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options, to: :@conn
+ private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
+ :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options
private
@@ -38,17 +40,32 @@ module ActiveRecord
end
def visit_TableDefinition(o)
- create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
- create_sql << "#{quote_table_name(o.name)} "
- create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} "
+
+ statements = o.columns.map { |c| accept c }
+ statements << accept(o.primary_keys) if o.primary_keys
+
+ if supports_indexes_in_create?
+ statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) })
+ end
+
+ if supports_foreign_keys?
+ statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
+ end
+
+ create_sql << "(#{statements.join(', ')}) " if statements.present?
create_sql << "#{o.options}"
create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
create_sql
end
- def visit_AddForeignKey(o)
+ def visit_PrimaryKeyDefinition(o)
+ "PRIMARY KEY (#{o.name.join(', ')})"
+ end
+
+ def visit_ForeignKeyDefinition(o)
sql = <<-SQL.strip_heredoc
- ADD CONSTRAINT #{quote_column_name(o.name)}
+ CONSTRAINT #{quote_column_name(o.name)}
FOREIGN KEY (#{quote_column_name(o.column)})
REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)})
SQL
@@ -57,6 +74,10 @@ module ActiveRecord
sql
end
+ def visit_AddForeignKey(o)
+ "ADD #{accept(o)}"
+ end
+
def visit_DropForeignKey(name)
"DROP CONSTRAINT #{quote_column_name(name)}"
end
@@ -89,8 +110,9 @@ module ActiveRecord
sql
end
- def options_include_default?(options)
- options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ def foreign_key_in_create(from_table, to_table, options)
+ options = foreign_key_options(from_table, to_table, options)
+ accept ForeignKeyDefinition.new(from_table, to_table, options)
end
def action_sql(action, dependency)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 2c76dd1518..10329de5f4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -23,6 +23,9 @@ module ActiveRecord
class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc:
end
+ class PrimaryKeyDefinition < Struct.new(:name) # :nodoc:
+ end
+
class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
def name
options[:name]
@@ -207,6 +210,7 @@ module ActiveRecord
@columns_hash = {}
@indexes = {}
@foreign_keys = {}
+ @primary_keys = nil
@native = types
@temporary = temporary
@options = options
@@ -214,6 +218,11 @@ module ActiveRecord
@name = name
end
+ def primary_keys(name = nil) # :nodoc:
+ @primary_keys = PrimaryKeyDefinition.new(name) if name
+ @primary_keys
+ end
+
# Returns an array of ColumnDefinition objects for the columns of the table.
def columns; @columns_hash.values; end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index b944a8631c..a2f58364a4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -18,7 +18,7 @@ module ActiveRecord
spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) })
end
- # This can be overridden on a Adapter level basis to support other
+ # This can be overridden on an Adapter level basis to support other
# extended datatypes (Example: Adding an array option in the
# PostgreSQLAdapter)
def prepare_column_options(column)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 16aefc94ab..7e53f8958a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -23,6 +23,20 @@ module ActiveRecord
table_name[0...table_alias_length].tr('.', '_')
end
+ # Returns the relation names useable to back Active Record models.
+ # For most adapters this means all #tables and #views.
+ def data_sources
+ tables | views
+ end
+
+ # Checks to see if the data source +name+ exists on the database.
+ #
+ # data_source_exists?(:ebooks)
+ #
+ def data_source_exists?(name)
+ data_sources.include?(name.to_s)
+ end
+
# Returns an array of table names defined in the database.
def tables(name = nil)
raise NotImplementedError, "#tables is not implemented"
@@ -36,6 +50,19 @@ module ActiveRecord
tables.include?(table_name.to_s)
end
+ # Returns an array of view names defined in the database.
+ def views
+ raise NotImplementedError, "#views is not implemented"
+ end
+
+ # Checks to see if the view +view_name+ exists on the database.
+ #
+ # view_exists?(:ebooks)
+ #
+ def view_exists?(view_name)
+ views.include?(view_name.to_s)
+ end
+
# Returns an array of indexes for the given table.
# def indexes(table_name, name = nil) end
@@ -93,6 +120,12 @@ module ActiveRecord
(!options.key?(:null) || c.null == options[:null]) }
end
+ # Returns just a table's primary key
+ def primary_key(table_name)
+ pks = primary_keys(table_name)
+ pks.first if pks.one?
+ end
+
# Creates a new table with the name +table_name+. +table_name+ may either
# be a String or a Symbol.
#
@@ -158,7 +191,7 @@ module ActiveRecord
# generates:
#
# CREATE TABLE suppliers (
- # id int(11) auto_increment PRIMARY KEY
+ # id int auto_increment PRIMARY KEY
# ) ENGINE=InnoDB DEFAULT CHARSET=utf8
#
# ====== Rename the primary key column
@@ -170,7 +203,7 @@ module ActiveRecord
# generates:
#
# CREATE TABLE objects (
- # guid int(11) auto_increment PRIMARY KEY,
+ # guid int auto_increment PRIMARY KEY,
# name varchar(80)
# )
#
@@ -220,7 +253,11 @@ module ActiveRecord
Base.get_primary_key table_name.to_s.singularize
end
- td.primary_key pk, options.fetch(:id, :primary_key), options
+ if pk.is_a?(Array)
+ td.primary_keys pk
+ else
+ td.primary_key pk, options.fetch(:id, :primary_key), options
+ end
end
yield td if block_given?
@@ -237,10 +274,6 @@ module ActiveRecord
end
end
- td.foreign_keys.each_pair do |other_table_name, foreign_key_options|
- add_foreign_key(table_name, other_table_name, foreign_key_options)
- end
-
result
end
@@ -320,7 +353,7 @@ module ActiveRecord
# [<tt>:bulk</tt>]
# Set this to true to make this a bulk alter query, such as
#
- # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
+ # ALTER TABLE `users` ADD COLUMN age INT, ADD COLUMN birthdate DATETIME ...
#
# Defaults to false.
#
@@ -474,7 +507,7 @@ module ActiveRecord
raise NotImplementedError, "change_column_default is not implemented"
end
- # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag
+ # Sets or removes a <tt>NOT NULL</tt> constraint on a column. The +null+ flag
# indicates whether the value can be +NULL+. For example
#
# change_column_null(:users, :nickname, false)
@@ -486,7 +519,7 @@ module ActiveRecord
# allows them to be +NULL+ (drops the constraint).
#
# The method accepts an optional fourth argument to replace existing
- # +NULL+s with some other value. Use that one when enabling the
+ # <tt>NULL</tt>s with some other value. Use that one when enabling the
# constraint if needed, since otherwise those rows would not be valid.
#
# Please note the fourth argument does not set a column's default.
@@ -540,6 +573,8 @@ module ActiveRecord
#
# CREATE INDEX by_name ON accounts(name(10))
#
+ # ====== Creating an index with specific key lengths for multiple keys
+ #
# add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15})
#
# generates:
@@ -769,15 +804,7 @@ module ActiveRecord
def add_foreign_key(from_table, to_table, options = {})
return unless supports_foreign_keys?
- options[:column] ||= foreign_key_column_for(to_table)
-
- options = {
- column: options[:column],
- primary_key: options[:primary_key],
- name: foreign_key_name(from_table, options),
- on_delete: options[:on_delete],
- on_update: options[:on_update]
- }
+ options = foreign_key_options(from_table, to_table, options)
at = create_alter_table from_table
at.add_foreign_key to_table, options
@@ -845,6 +872,13 @@ module ActiveRecord
"#{name.singularize}_id"
end
+ def foreign_key_options(from_table, to_table, options) # :nodoc:
+ options = options.dup
+ options[:column] ||= foreign_key_column_for(to_table)
+ options[:name] ||= foreign_key_name(from_table, options)
+ options
+ end
+
def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
@@ -986,6 +1020,10 @@ module ActiveRecord
[index_name, index_type, index_columns, index_options, algorithm, using]
end
+ def options_include_default?(options)
+ options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ end
+
protected
def add_index_sort_order(option_strings, column_names, options = {})
if options.is_a?(Hash) && order = options[:order]
@@ -1012,10 +1050,6 @@ module ActiveRecord
column_names.map {|name| quote_column_name(name) + option_strings[name]}
end
- def options_include_default?(options)
- options.include?(:default) && !(options[:null] == false && options[:default].nil?)
- end
-
def index_name_for_remove(table_name, options = {})
index_name = index_name(table_name, options)
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 597f0d0597..cd8097d1b3 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -14,10 +14,26 @@ module ActiveRecord
def json(*args, **options)
args.each { |name| column(name, :json, options) }
end
+
+ def unsigned_integer(*args, **options)
+ args.each { |name| column(name, :unsigned_integer, options) }
+ end
+
+ def unsigned_bigint(*args, **options)
+ args.each { |name| column(name, :unsigned_bigint, options) }
+ end
+
+ def unsigned_float(*args, **options)
+ args.each { |name| column(name, :unsigned_float, options) }
+ end
+
+ def unsigned_decimal(*args, **options)
+ args.each { |name| column(name, :unsigned_decimal, options) }
+ end
end
class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
- attr_accessor :charset
+ attr_accessor :charset, :unsigned
end
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
@@ -29,7 +45,11 @@ module ActiveRecord
when :primary_key
column.type = :integer
column.auto_increment = true
+ when /\Aunsigned_(?<type>.+)\z/
+ column.type = $~[:type].to_sym
+ column.unsigned = true
end
+ column.unsigned ||= options[:unsigned]
column.charset = options[:charset]
column
end
@@ -52,17 +72,9 @@ module ActiveRecord
"DROP FOREIGN KEY #{name}"
end
- def visit_TableDefinition(o)
- name = o.name
- create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "
-
- statements = o.columns.map { |c| accept c }
- statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) })
-
- create_sql << "(#{statements.join(', ')}) " if statements.present?
- create_sql << "#{o.options}"
- create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
- create_sql
+ def visit_ColumnDefinition(o)
+ o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned)
+ super
end
def visit_AddColumnDefinition(o)
@@ -117,6 +129,7 @@ module ActiveRecord
spec = {}
if column.auto_increment?
spec[:id] = ':bigint' if column.bigint?
+ spec[:unsigned] = 'true' if column.unsigned?
return if spec.empty?
else
spec[:id] = column.type.inspect
@@ -125,6 +138,16 @@ module ActiveRecord
spec
end
+ def prepare_column_options(column)
+ spec = super
+ spec[:unsigned] = 'true' if column.unsigned?
+ spec
+ end
+
+ def migration_keys
+ super + [:unsigned]
+ end
+
private
def schema_limit(column)
@@ -171,6 +194,10 @@ module ActiveRecord
sql_type =~ /blob/i || type == :text
end
+ def unsigned?
+ /unsigned/ === sql_type
+ end
+
def case_sensitive?
collation && !collation.match(/_ci$/)
end
@@ -246,7 +273,7 @@ module ActiveRecord
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
NATIVE_DATABASE_TYPES = {
- primary_key: "int(11) auto_increment PRIMARY KEY",
+ primary_key: "int auto_increment PRIMARY KEY",
string: { name: "varchar", limit: 255 },
text: { name: "text" },
integer: { name: "int", limit: 4 },
@@ -316,6 +343,10 @@ module ActiveRecord
version >= '5.0.0'
end
+ def supports_explain?
+ true
+ end
+
def supports_indexes_in_create?
true
end
@@ -417,6 +448,80 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
#++
+ def explain(arel, binds = [])
+ sql = "EXPLAIN #{to_sql(arel, binds)}"
+ start = Time.now
+ result = exec_query(sql, 'EXPLAIN', binds)
+ elapsed = Time.now - start
+
+ ExplainPrettyPrinter.new.pp(result, elapsed)
+ end
+
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of an EXPLAIN in a way that resembles the output of the
+ # MySQL shell:
+ #
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+ # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # 2 rows in set (0.00 sec)
+ #
+ # This is an exercise in Ruby hyperrealism :).
+ def pp(result, elapsed)
+ widths = compute_column_widths(result)
+ separator = build_separator(widths)
+
+ pp = []
+
+ pp << separator
+ pp << build_cells(result.columns, widths)
+ pp << separator
+
+ result.rows.each do |row|
+ pp << build_cells(row, widths)
+ end
+
+ pp << separator
+ pp << build_footer(result.rows.length, elapsed)
+
+ pp.join("\n") + "\n"
+ end
+
+ private
+
+ def compute_column_widths(result)
+ [].tap do |widths|
+ result.columns.each_with_index do |column, i|
+ cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s}
+ widths << cells_in_column.map(&:length).max
+ end
+ end
+ end
+
+ def build_separator(widths)
+ padding = 1
+ '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
+ end
+
+ def build_cells(items, widths)
+ cells = []
+ items.each_with_index do |item, i|
+ item = 'NULL' if item.nil?
+ justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
+ cells << item.to_s.send(justifier, widths[i])
+ end
+ '| ' + cells.join(' | ') + ' |'
+ end
+
+ def build_footer(nrows, elapsed)
+ rows_label = nrows == 1 ? 'row' : 'rows'
+ "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
+ end
+ end
+
def clear_cache!
super
reload_type_map
@@ -520,33 +625,42 @@ module ActiveRecord
show_variable 'collation_database'
end
- def tables(name = nil, database = nil, like = nil) #:nodoc:
- sql = "SHOW TABLES "
- sql << "IN #{quote_table_name(database)} " if database
- sql << "LIKE #{quote(like)}" if like
-
- execute_and_free(sql, 'SCHEMA') do |result|
- result.collect(&:first)
- end
+ def tables(name = nil) # :nodoc:
+ select_values("SHOW FULL TABLES", 'SCHEMA')
end
+ alias data_sources tables
def truncate(table_name, name = nil)
execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
end
- def table_exists?(name)
- return false unless name.present?
- return true if tables(nil, nil, name).any?
+ def table_exists?(table_name)
+ return false unless table_name.present?
- name = name.to_s
- schema, table = name.split('.', 2)
+ schema, name = table_name.to_s.split('.', 2)
+ schema, name = @config[:database], schema unless name # A table was provided without a schema
- unless table # A table was provided without a schema
- table = schema
- schema = nil
- end
+ sql = "SELECT table_name FROM information_schema.tables "
+ sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}"
+
+ select_values(sql, 'SCHEMA').any?
+ end
+ alias data_source_exists? table_exists?
- tables(nil, schema, table).any?
+ def views # :nodoc:
+ select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA')
+ end
+
+ def view_exists?(view_name) # :nodoc:
+ return false unless view_name.present?
+
+ schema, name = view_name.to_s.split('.', 2)
+ schema, name = @config[:database], schema unless name # A view was provided without a schema
+
+ sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'"
+ sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}"
+
+ select_values(sql, 'SCHEMA').any?
end
# Returns an array of indexes for the given table.
@@ -685,7 +799,7 @@ module ActiveRecord
AND fk.table_name = '#{table_name}'
SQL
- create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+ create_table_info = create_table_info(table_name)
fk_info.map do |row|
options = {
@@ -702,7 +816,7 @@ module ActiveRecord
end
def table_options(table_name)
- create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+ create_table_info = create_table_info(table_name)
# strip create_definitions and partition_options
raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip
@@ -712,8 +826,8 @@ module ActiveRecord
end
# Maps logical Rails types to MySQL-specific data types.
- def type_to_sql(type, limit = nil, precision = nil, scale = nil)
- case type.to_s
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil)
+ sql = case type.to_s
when 'binary'
binary_to_sql(limit)
when 'integer'
@@ -721,8 +835,11 @@ module ActiveRecord
when 'text'
text_to_sql(limit)
else
- super
+ super(type, limit, precision, scale)
end
+
+ sql << ' unsigned' if unsigned && type != :primary_key
+ sql
end
# SHOW VARIABLES LIKE 'name'
@@ -735,21 +852,25 @@ module ActiveRecord
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table)
- execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result|
- create_table = each_hash(result).first[:"Create Table"]
- if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/
- keys = $1.split(",").map { |key| key.delete('`"') }
- keys.length == 1 ? [keys.first, nil] : nil
- else
- nil
- end
+ if pk = primary_key(table)
+ [ pk, nil ]
end
end
- # Returns just a table's primary key
- def primary_key(table)
- pk_and_sequence = pk_and_sequence_for(table)
- pk_and_sequence && pk_and_sequence.first
+ def primary_keys(table_name) # :nodoc:
+ raise ArgumentError unless table_name.present?
+
+ schema, name = table_name.to_s.split('.', 2)
+ schema, name = @config[:database], schema unless name # A table was provided without a schema
+
+ select_values(<<-SQL.strip_heredoc, 'SCHEMA')
+ SELECT column_name
+ FROM information_schema.key_column_usage
+ WHERE constraint_name = 'PRIMARY'
+ AND table_schema = #{quote(schema)}
+ AND table_name = #{quote(name)}
+ ORDER BY ordinal_position
+ SQL
end
def case_sensitive_modifier(node, table_attribute)
@@ -807,7 +928,6 @@ module ActiveRecord
register_integer_type m, %r(^tinyint)i, limit: 1
m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans
- m.alias_type %r(set)i, 'varchar'
m.alias_type %r(year)i, 'integer'
m.alias_type %r(bit)i, 'binary'
@@ -816,6 +936,12 @@ module ActiveRecord
.split(',').map{|enum| enum.strip.length - 2}.max
MysqlString.new(limit: limit)
end
+
+ m.register_type(%r(^set)i) do |sql_type|
+ limit = sql_type[/^set\((.+)\)/i, 1]
+ .split(',').map{|set| set.strip.length - 1}.sum - 1
+ MysqlString.new(limit: limit)
+ end
end
def register_integer_type(mapping, key, options) # :nodoc:
@@ -1018,6 +1144,11 @@ module ActiveRecord
end
end
+ def create_table_info(table_name) # :nodoc:
+ @create_table_info_cache = {}
+ @create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+ end
+
def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
TableDefinition.new(native_database_types, name, temporary, options, as)
end
@@ -1036,8 +1167,9 @@ module ActiveRecord
when 1; 'tinyint'
when 2; 'smallint'
when 3; 'mediumint'
- when nil, 4, 11; 'int(11)' # compatibility with MySQL default
+ when nil, 4; 'int'
when 5..8; 'bigint'
+ when 11; 'int(11)' # backward compatibility with Rails 2.0
else raise(ActiveRecordError, "No integer type has byte size #{limit}")
end
end
@@ -1084,6 +1216,8 @@ module ActiveRecord
ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2)
ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql)
ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2)
+ ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql)
+ ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 4b95b0681d..5e31efec4a 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -5,20 +5,13 @@ module ActiveRecord
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
- FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].to_set
-
- module Format
- ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
- ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
- end
-
attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation
delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
# Instantiates a new column in the table.
#
- # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
+ # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int</tt>.
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
# +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 734b384e80..4461722bb4 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -37,10 +37,6 @@ module ActiveRecord
configure_connection
end
- def supports_explain?
- true
- end
-
def supports_json?
version >= '5.7.8'
end
@@ -99,80 +95,6 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
#++
- def explain(arel, binds = [])
- sql = "EXPLAIN #{to_sql(arel, binds.dup)}"
- start = Time.now
- result = exec_query(sql, 'EXPLAIN', binds)
- elapsed = Time.now - start
-
- ExplainPrettyPrinter.new.pp(result, elapsed)
- end
-
- class ExplainPrettyPrinter # :nodoc:
- # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
- # MySQL shell:
- #
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
- # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
- # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
- # 2 rows in set (0.00 sec)
- #
- # This is an exercise in Ruby hyperrealism :).
- def pp(result, elapsed)
- widths = compute_column_widths(result)
- separator = build_separator(widths)
-
- pp = []
-
- pp << separator
- pp << build_cells(result.columns, widths)
- pp << separator
-
- result.rows.each do |row|
- pp << build_cells(row, widths)
- end
-
- pp << separator
- pp << build_footer(result.rows.length, elapsed)
-
- pp.join("\n") + "\n"
- end
-
- private
-
- def compute_column_widths(result)
- [].tap do |widths|
- result.columns.each_with_index do |column, i|
- cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s}
- widths << cells_in_column.map(&:length).max
- end
- end
- end
-
- def build_separator(widths)
- padding = 1
- '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
- end
-
- def build_cells(items, widths)
- cells = []
- items.each_with_index do |item, i|
- item = 'NULL' if item.nil?
- justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
- cells << item.to_s.send(justifier, widths[i])
- end
- '| ' + cells.join(' | ') + ' |'
- end
-
- def build_footer(nrows, elapsed)
- rows_label = nrows == 1 ? 'row' : 'rows'
- "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
- end
- end
-
# FIXME: re-enable the following once a "better" query_cache solution is in core
#
# The overrides below perform much better than the originals in AbstractAdapter
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index e4fcff8814..b3894481cc 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -80,8 +80,7 @@ module ActiveRecord
def initialize(connection, logger, connection_options, config)
super
- @statements = StatementPool.new(@connection,
- self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
+ @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@client_encoding = nil
connect
end
@@ -162,6 +161,14 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
#++
+ def select_all(arel, name = nil, binds = [])
+ if ExplainRegistry.collect? && prepared_statements
+ unprepared_statement { super }
+ else
+ super
+ end
+ end
+
def select_rows(sql, name = nil, binds = [])
@connection.query_with_result = true
rows = exec_query(sql, name, binds).rows
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 11d3f5301a..43e18ebb2b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -8,7 +8,7 @@ module ActiveRecord
end
class ExplainPrettyPrinter # :nodoc:
- # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
+ # Pretty prints the result of an EXPLAIN in a way that resembles the output of the
# PostgreSQL shell:
#
# QUERY PLAN
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
index 2c04c46131..424769f765 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
@@ -4,18 +4,14 @@ module ActiveRecord
module OID # :nodoc:
class DateTime < Type::DateTime # :nodoc:
def cast_value(value)
- if value.is_a?(::String)
- case value
- when 'infinity' then ::Float::INFINITY
- when '-infinity' then -::Float::INFINITY
- when / BC$/
- astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
- super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
- else
- super
- end
+ case value
+ when 'infinity' then ::Float::INFINITY
+ when '-infinity' then -::Float::INFINITY
+ when / BC$/
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
else
- value
+ super
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 69aa02ccf4..aaf5b2898b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -73,6 +73,16 @@ module ActiveRecord
select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA')
end
+ def data_sources # :nodoc
+ select_values(<<-SQL, 'SCHEMA')
+ SELECT c.relname
+ FROM pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relkind IN ('r', 'v','m') -- (r)elation/table, (v)iew, (m)aterialized view
+ AND n.nspname = ANY (current_schemas(false))
+ SQL
+ end
+
# Returns true if table exists.
# If the schema is not specified as part of +name+ then it will only find tables within
# the current schema search path (regardless of permissions to access tables in other schemas)
@@ -89,6 +99,31 @@ module ActiveRecord
AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
SQL
end
+ alias data_source_exists? table_exists?
+
+ def views # :nodoc:
+ select_values(<<-SQL, 'SCHEMA')
+ SELECT c.relname
+ FROM pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
+ AND n.nspname = ANY (current_schemas(false))
+ SQL
+ end
+
+ def view_exists?(view_name) # :nodoc:
+ name = Utils.extract_schema_qualified_name(view_name.to_s)
+ return false unless name.identifier
+
+ select_values(<<-SQL, 'SCHEMA').any?
+ SELECT c.relname
+ FROM pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
+ AND c.relname = '#{name.identifier}'
+ AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
+ SQL
+ end
def drop_table(table_name, options = {}) # :nodoc:
execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
@@ -353,17 +388,19 @@ module ActiveRecord
nil
end
- # Returns just a table's primary key
- def primary_key(table)
- pks = query(<<-end_sql, 'SCHEMA')
- SELECT attr.attname
- FROM pg_attribute attr
- INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey)
- WHERE cons.contype = 'p'
- AND cons.conrelid = '#{quote_table_name(table)}'::regclass
- end_sql
- return nil unless pks.count == 1
- pks[0][0]
+ def primary_keys(table_name) # :nodoc:
+ select_values(<<-SQL.strip_heredoc, 'SCHEMA')
+ WITH pk_constraint AS (
+ SELECT conrelid, unnest(conkey) AS connum FROM pg_constraint
+ WHERE contype = 'p'
+ AND conrelid = '#{quote_table_name(table_name)}'::regclass
+ ), cons AS (
+ SELECT conrelid, connum, row_number() OVER() AS rownum FROM pk_constraint
+ )
+ SELECT attr.attname FROM pg_attribute attr
+ INNER JOIN cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.connum
+ ORDER BY cons.rownum
+ SQL
end
# Renames a table.
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 27291bd2ea..25dfda9ef8 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -211,7 +211,8 @@ module ActiveRecord
class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max)
- super
+ super(max)
+ @connection = connection
@counter = 0
end
@@ -760,7 +761,7 @@ module ActiveRecord
end
def extract_table_ref_from_insert_sql(sql) # :nodoc:
- sql[/into\s+([^\(]*).*values\s*\(/im]
+ sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im]
$1.strip if $1
end
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 981d5d7a3c..eee142378c 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -10,7 +10,7 @@ module ActiveRecord
@columns = {}
@columns_hash = {}
@primary_keys = {}
- @tables = {}
+ @data_sources = {}
end
def initialize_dup(other)
@@ -18,33 +18,38 @@ module ActiveRecord
@columns = @columns.dup
@columns_hash = @columns_hash.dup
@primary_keys = @primary_keys.dup
- @tables = @tables.dup
+ @data_sources = @data_sources.dup
end
def primary_keys(table_name)
- @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil
+ @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil
end
# A cached lookup for table existence.
- def table_exists?(name)
- prepare_tables if @tables.empty?
- return @tables[name] if @tables.key? name
+ def data_source_exists?(name)
+ prepare_data_sources if @data_sources.empty?
+ return @data_sources[name] if @data_sources.key? name
- @tables[name] = connection.table_exists?(name)
+ @data_sources[name] = connection.data_source_exists?(name)
end
+ alias table_exists? data_source_exists?
+ deprecate :table_exists? => "use #data_source_exists? instead"
+
# Add internal cache for table with +table_name+.
def add(table_name)
- if table_exists?(table_name)
+ if data_source_exists?(table_name)
primary_keys(table_name)
columns(table_name)
columns_hash(table_name)
end
end
- def tables(name)
- @tables[name]
+ def data_sources(name)
+ @data_sources[name]
end
+ alias tables data_sources
+ deprecate :tables => "use #data_sources instead"
# Get the columns for a table
def columns(table_name)
@@ -64,36 +69,38 @@ module ActiveRecord
@columns.clear
@columns_hash.clear
@primary_keys.clear
- @tables.clear
+ @data_sources.clear
@version = nil
end
def size
- [@columns, @columns_hash, @primary_keys, @tables].map(&:size).inject :+
+ [@columns, @columns_hash, @primary_keys, @data_sources].map(&:size).inject :+
end
- # Clear out internal caches for table with +table_name+.
- def clear_table_cache!(table_name)
- @columns.delete table_name
- @columns_hash.delete table_name
- @primary_keys.delete table_name
- @tables.delete table_name
+ # Clear out internal caches for the data source +name+.
+ def clear_data_source_cache!(name)
+ @columns.delete name
+ @columns_hash.delete name
+ @primary_keys.delete name
+ @data_sources.delete name
end
+ alias clear_table_cache! clear_data_source_cache!
+ deprecate :clear_table_cache! => "use #clear_data_source_cache! instead"
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = ActiveRecord::Migrator.current_version
- [@version, @columns, @columns_hash, @primary_keys, @tables]
+ [@version, @columns, @columns_hash, @primary_keys, @data_sources]
end
def marshal_load(array)
- @version, @columns, @columns_hash, @primary_keys, @tables = array
+ @version, @columns, @columns_hash, @primary_keys, @data_sources = array
end
private
- def prepare_tables
- connection.tables.each { |table| @tables[table] = true }
+ def prepare_data_sources
+ connection.data_sources.each { |source| @data_sources[source] = true }
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 24fc67938d..505a71720f 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -81,8 +81,7 @@ module ActiveRecord
super(connection, logger)
@active = nil
- @statements = StatementPool.new(@connection,
- self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
+ @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@config = config
@visitor = Arel::Visitors::SQLite.new self
@@ -219,7 +218,7 @@ module ActiveRecord
end
class ExplainPrettyPrinter
- # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
+ # Pretty prints the result of an EXPLAIN QUERY PLAN in a way that resembles
# the output of the SQLite shell:
#
# 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
@@ -320,10 +319,25 @@ module ActiveRecord
row['name']
end
end
+ alias data_sources tables
def table_exists?(table_name)
table_name && tables(nil, table_name).any?
end
+ alias data_source_exists? table_exists?
+
+ def views # :nodoc:
+ select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", 'SCHEMA')
+ end
+
+ def view_exists?(view_name) # :nodoc:
+ return false unless view_name.present?
+
+ sql = "SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'"
+ sql << " AND name = #{quote(view_name)}"
+
+ select_values(sql, 'SCHEMA').any?
+ end
# Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name) #:nodoc:
@@ -369,10 +383,9 @@ module ActiveRecord
end
end
- def primary_key(table_name) #:nodoc:
+ def primary_keys(table_name) # :nodoc:
pks = table_structure(table_name).select { |f| f['pk'] > 0 }
- return nil unless pks.count == 1
- pks[0]['name']
+ pks.sort_by { |f| f['pk'] }.map { |f| f['name'] }
end
def remove_index(table_name, options = {}) #:nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
index 82e9ef3d3d..57463dd749 100644
--- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb
@@ -3,10 +3,9 @@ module ActiveRecord
class StatementPool
include Enumerable
- def initialize(connection, max = 1000)
+ def initialize(max = 1000)
@cache = Hash.new { |h,pid| h[pid] = {} }
- @connection = connection
- @max = max
+ @max = max
end
def each(&block)
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index d6b661ff76..2fc5e410f9 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -42,7 +42,7 @@ module ActiveRecord
#
# ActiveRecord::Base.establish_connection(:production)
#
- # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
+ # The exceptions +AdapterNotSpecified+, +AdapterNotFound+ and +ArgumentError+
# may be returned on an error.
def establish_connection(spec = nil)
spec ||= DEFAULT_ENV.call.to_sym
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index ffce2173ec..894d18b79e 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -296,7 +296,7 @@ module ActiveRecord
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
def initialize(attributes = nil)
- @attributes = self.class._default_attributes.dup
+ @attributes = self.class._default_attributes.deep_dup
self.class.define_attribute_methods
init_internals
@@ -366,7 +366,7 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
- @attributes = @attributes.dup
+ @attributes = @attributes.deep_dup
@attributes.reset(self.class.primary_key)
_run_initialize_callbacks
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index 4902fcb1a2..10b5fcab24 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -118,7 +118,7 @@ module ActiveRecord
elsif mapping.has_value?(value)
mapping.key(value)
else
- raise ArgumentError, "'#{value}' is not a valid #{name}"
+ assert_valid_value(value)
end
end
@@ -131,6 +131,12 @@ module ActiveRecord
mapping.fetch(value, value)
end
+ def assert_valid_value(value)
+ unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
+ end
+ end
+
protected
attr_reader :name, :mapping
@@ -204,30 +210,22 @@ module ActiveRecord
def detect_enum_conflict!(enum_name, method_name, klass_method = false)
if klass_method && dangerous_class_method?(method_name)
- raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
- enum: enum_name,
- klass: self.name,
- type: 'class',
- method: method_name,
- source: 'Active Record'
- }
+ raise_conflict_error(enum_name, method_name, type: 'class')
elsif !klass_method && dangerous_attribute_method?(method_name)
- raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
- enum: enum_name,
- klass: self.name,
- type: 'instance',
- method: method_name,
- source: 'Active Record'
- }
+ raise_conflict_error(enum_name, method_name)
elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
- raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
- enum: enum_name,
- klass: self.name,
- type: 'instance',
- method: method_name,
- source: 'another enum'
- }
+ raise_conflict_error(enum_name, method_name, source: 'another enum')
end
end
+
+ def raise_conflict_error(enum_name, method_name, type: 'instance', source: 'Active Record')
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: type,
+ method: method_name,
+ source: source
+ }
+ end
end
end
diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb
index 8132310c91..f969556c50 100644
--- a/activerecord/lib/active_record/fixture_set/file.rb
+++ b/activerecord/lib/active_record/fixture_set/file.rb
@@ -17,24 +17,39 @@ module ActiveRecord
def initialize(file)
@file = file
- @rows = nil
end
def each(&block)
rows.each(&block)
end
+ def model_class
+ config_row['model_class']
+ end
private
def rows
- return @rows if @rows
+ @rows ||= raw_rows.reject { |fixture_name, _| fixture_name == '_fixture' }
+ end
+
+ def config_row
+ @config_row ||= begin
+ row = raw_rows.find { |fixture_name, _| fixture_name == '_fixture' }
+ if row
+ row.last
+ else
+ {'model_class': nil}
+ end
+ end
+ end
- begin
+ def raw_rows
+ @raw_rows ||= begin
data = YAML.load(render(IO.read(@file)))
+ data ? validate(data).to_a : []
rescue ArgumentError, Psych::SyntaxError => error
raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
end
- @rows = data ? validate(data).to_a : []
end
def render(content)
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index f1dc56df63..59df3c78f3 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -395,6 +395,20 @@ module ActiveRecord
# <<: *DEFAULTS
#
# Any fixture labeled "DEFAULTS" is safely ignored.
+ #
+ # == Configure the fixture model class
+ #
+ # It's possible to set the fixture's model class directly in the YAML file.
+ # This is helpful when fixtures are loaded outside tests and
+ # +set_fixture_class+ is not available (e.g.
+ # when running <tt>rake db:fixtures:load</tt>).
+ #
+ # _fixture:
+ # model_class: User
+ # david:
+ # name: David
+ #
+ # Any fixtures labeled "_fixture" are safely ignored.
class FixtureSet
#--
# An instance of FixtureSet is normally stored in a single YAML file and
@@ -578,21 +592,16 @@ module ActiveRecord
@name = name
@path = path
@config = config
- @model_class = nil
- if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
- @model_class = class_name
- else
- @model_class = class_name.safe_constantize if class_name
- end
+ self.model_class = class_name
+
+ @fixtures = read_fixture_files(path)
@connection = connection
@table_name = ( model_class.respond_to?(:table_name) ?
model_class.table_name :
self.class.default_fixture_table_name(name, config) )
-
- @fixtures = read_fixture_files path, @model_class
end
def [](x)
@@ -761,13 +770,25 @@ module ActiveRecord
@column_names ||= @connection.columns(@table_name).collect(&:name)
end
- def read_fixture_files(path, model_class)
+ def model_class=(class_name)
+ if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
+ @model_class = class_name
+ else
+ @model_class = class_name.safe_constantize if class_name
+ end
+ end
+
+ # Loads the fixtures from the YAML file at +path+.
+ # If the file sets the +model_class+ and current instance value is not set,
+ # it uses the file value.
+ def read_fixture_files(path)
yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f|
::File.file?(f)
} + [yaml_file_path(path)]
yaml_files.each_with_object({}) do |file, fixtures|
FixtureSet::File.open(file) do |fh|
+ self.model_class ||= fh.model_class if fh.model_class
fh.each do |fixture_name, row|
fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
end
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index a09437b4b0..2336d23a1c 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -22,7 +22,7 @@ module ActiveRecord
# p1.save
#
# p2.first_name = "should fail"
- # p2.save # Raises a ActiveRecord::StaleObjectError
+ # p2.save # Raises an ActiveRecord::StaleObjectError
#
# Optimistic locking will also check for stale data when objects are destroyed. Example:
#
@@ -32,7 +32,7 @@ module ActiveRecord
# p1.first_name = "Michael"
# p1.save
#
- # p2.destroy # Raises a ActiveRecord::StaleObjectError
+ # p2.destroy # Raises an ActiveRecord::StaleObjectError
#
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 1aa297bcd0..90b8a29869 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -231,7 +231,7 @@ module ActiveRecord
# * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index
# specified by +index_name+.
# * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column
- # +reference_name_id+ by default a integer. See
+ # +reference_name_id+ by default an integer. See
# ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details.
#
# == Irreversible transformations
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 2b0c755ef4..a9bd094a66 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -50,6 +50,13 @@ module ActiveRecord
class_attribute :pluralize_table_names, instance_writer: false
self.pluralize_table_names = true
+ ##
+ # :singleton-method:
+ # Accessor for the list of columns names the model should ignore. Ignored columns won't have attribute
+ # accessors defined, and won't be referenced in SQL queries.
+ class_attribute :ignored_columns, instance_accessor: false
+ self.ignored_columns = [].freeze
+
self.inheritance_column = 'type'
delegate :type_for_attribute, to: :class
@@ -213,7 +220,7 @@ module ActiveRecord
# Indicates whether the table associated with this class exists
def table_exists?
- connection.schema_cache.table_exists?(table_name)
+ connection.schema_cache.data_source_exists?(table_name)
end
def attributes_builder # :nodoc:
@@ -290,7 +297,7 @@ module ActiveRecord
def reset_column_information
connection.clear_cache!
undefine_attribute_methods
- connection.schema_cache.clear_table_cache!(table_name)
+ connection.schema_cache.clear_data_source_cache!(table_name)
reload_schema_from_cache
end
@@ -308,7 +315,7 @@ module ActiveRecord
end
def load_schema!
- @columns_hash = connection.schema_cache.columns_hash(table_name)
+ @columns_hash = connection.schema_cache.columns_hash(table_name).except(*ignored_columns)
@columns_hash.each do |name, column|
warn_if_deprecated_type(column)
define_attribute(
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 09c36d7b4d..3f02f73a5a 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -211,6 +211,7 @@ module ActiveRecord
def becomes(klass)
became = klass.new
became.instance_variable_set("@attributes", @attributes)
+ became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker)
became.instance_variable_set("@changed_attributes", attributes_changed_by_setter)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index d543a84ff1..d940ac244a 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -255,7 +255,7 @@ db_namespace = namespace :db do
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
con.schema_cache.clear!
- con.tables.each { |table| con.schema_cache.add(table) }
+ con.data_sources.each { |table| con.schema_cache.add(table) }
open(filename, 'wb') { |f| f.write(Marshal.dump(con.schema_cache)) }
end
@@ -353,7 +353,7 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end
- # desc 'Check for pending migrations and load the test schema'
+ # desc 'Load the test schema'
task :prepare => %w(environment load_config) do
unless ActiveRecord::Base.configurations.blank?
db_namespace['test:load'].invoke
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 4e0fc407bc..5b9d45d871 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -62,7 +62,7 @@ module ActiveRecord
aggregate_reflections[aggregation.to_s]
end
- # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value.
+ # Returns a Hash of name of the reflection as the key and an AssociationReflection as the value.
#
# Account.reflections # => {"balance" => AggregateReflection}
#
@@ -175,6 +175,20 @@ module ActiveRecord
end
end
+ def inverse_of
+ return unless inverse_name
+
+ @inverse_of ||= klass._reflect_on_association inverse_name
+ end
+
+ def check_validity_of_inverse!
+ unless polymorphic?
+ if has_inverse? && inverse_of.nil?
+ raise InverseOfAssociationNotFoundError.new(self)
+ end
+ end
+ end
+
# This shit is nasty. We need to avoid the following situation:
#
# * An associated record is deleted via record.destroy
@@ -377,14 +391,6 @@ module ActiveRecord
check_validity_of_inverse!
end
- def check_validity_of_inverse!
- unless polymorphic?
- if has_inverse? && inverse_of.nil?
- raise InverseOfAssociationNotFoundError.new(self)
- end
- end
- end
-
def check_preloadable!
return unless scope
@@ -436,12 +442,6 @@ module ActiveRecord
inverse_name
end
- def inverse_of
- return unless inverse_name
-
- @inverse_of ||= klass._reflect_on_association inverse_name
- end
-
def polymorphic_inverse_of(associated_class)
if has_inverse?
if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of])
@@ -924,6 +924,8 @@ module ActiveRecord
klass.primary_key || raise(UnknownPrimaryKey.new(klass))
end
+ def inverse_name; delegate_reflection.send(:inverse_name); end
+
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 0f6015fa93..69f39e5ba9 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -70,9 +70,9 @@ module ActiveRecord
# +calculate+ for examples with options.
#
# Person.sum(:age) # => 4562
- def sum(*args)
- return super if block_given?
- calculate(:sum, *args)
+ def sum(column_name = nil, &block)
+ return super(&block) if block_given?
+ calculate(:sum, column_name)
end
# This calculates aggregate values in the given column. Methods for count, sum, average,
@@ -338,7 +338,6 @@ module ActiveRecord
# column_alias_for("sum(id)") # => "sum_id"
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
# column_alias_for("count(*)") # => "count_all"
- # column_alias_for("count", "id") # => "count_id"
def column_alias_for(keys)
if keys.respond_to? :name
keys = "#{keys.relation.name}.#{keys.name}"
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index e232516b0c..39e7b42629 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -67,7 +67,7 @@ module ActiveRecord
# Arel::Nodes::And.new([range.start, range.end])
# )
# end
- # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler)
+ # ActiveRecord::PredicateBuilder.new("users").register_handler(MyCustomDateRange, handler)
def register_handler(klass, handler)
@handlers.unshift([klass, handler])
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index e25b889851..ccb0ab18ae 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -250,7 +250,7 @@ module ActiveRecord
def _select!(*fields) # :nodoc:
fields.flatten!
fields.map! do |field|
- klass.attribute_alias?(field) ? klass.attribute_alias(field) : field
+ klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
end
self.select_values += fields
self
@@ -264,7 +264,7 @@ module ActiveRecord
# Returns an array with distinct records based on the +group+ attribute:
#
# User.select([:id, :name])
- # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">
+ # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">]
#
# User.group(:name)
# => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
@@ -558,11 +558,8 @@ module ActiveRecord
end
def where!(opts, *rest) # :nodoc:
- if Hash === opts
- opts = sanitize_forbidden_attributes(opts)
- references!(PredicateBuilder.references(opts))
- end
-
+ opts = sanitize_forbidden_attributes(opts)
+ references!(PredicateBuilder.references(opts)) if Hash === opts
self.where_clause += where_clause_factory.build(opts, rest)
self
end
@@ -619,6 +616,7 @@ module ActiveRecord
end
def having!(opts, *rest) # :nodoc:
+ opts = sanitize_forbidden_attributes(opts)
references!(PredicateBuilder.references(opts)) if Hash === opts
self.having_clause += having_clause_factory.build(opts, rest)
@@ -713,7 +711,7 @@ module ActiveRecord
#
# users = User.readonly
# users.first.save
- # => ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
+ # => ActiveRecord::ReadOnlyRecord: User is marked as readonly
def readonly(value = true)
spawn.readonly!(value)
end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index ba75ffa5a1..7f6664ea50 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -4,7 +4,7 @@ module ActiveRecord
module ClassMethods
# Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
- def sanitize(object) #:nodoc:
+ def sanitize(object) # :nodoc:
connection.quote(object)
end
alias_method :quote_value, :sanitize
@@ -13,9 +13,16 @@ module ActiveRecord
# Accepts an array or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a WHERE clause.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
- # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
- def sanitize_sql_for_conditions(condition, table_name = self.table_name)
+ #
+ # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
+ # # => "name='foo''bar' and group_id='4'"
+ def sanitize_sql_for_conditions(condition)
return nil if condition.blank?
case condition
@@ -28,7 +35,15 @@ module ActiveRecord
# Accepts an array, hash, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a SET clause.
- # { name: nil, group_id: 4 } returns "name = NULL , group_id='4'"
+ #
+ # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 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)
@@ -40,14 +55,18 @@ module ActiveRecord
# Accepts a hash of SQL conditions and replaces those attributes
# that correspond to a +composed_of+ relationship with their expanded
# aggregate attribute values.
+ #
# Given:
- # class Person < ActiveRecord::Base
- # composed_of :address, class_name: "Address",
- # mapping: [%w(address_street street), %w(address_city city)]
- # end
+ #
+ # 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" }
+ #
+ # { 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|
@@ -68,8 +87,9 @@ module ActiveRecord
end
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
- # { status: nil, group_id: 1 }
- # # => "status = NULL , group_id = 1"
+ #
+ # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
+ # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
def sanitize_sql_hash_for_assignment(attrs, table)
c = connection
attrs.map do |attr, value|
@@ -79,7 +99,19 @@ module ActiveRecord
end
# Sanitizes a +string+ so that it is safe to use within an SQL
- # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%"
+ # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
+ #
+ # sanitize_sql_like("100%")
+ # # => "100\\%"
+ #
+ # sanitize_sql_like("snake_cased_string")
+ # # => "snake\\_cased\\_string"
+ #
+ # sanitize_sql_like("100%", "!")
+ # # => "100!%"
+ #
+ # sanitize_sql_like("snake_cased_string", "!")
+ # # => "snake!_cased!_string"
def sanitize_sql_like(string, escape_character = "\\")
pattern = Regexp.union(escape_character, "%", "_")
string.gsub(pattern) { |x| [escape_character, x].join }
@@ -87,7 +119,12 @@ module ActiveRecord
# Accepts an array of conditions. The array has each value
# sanitized and interpolated into the SQL statement.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
def sanitize_sql_array(ary)
statement, *values = ary
if values.first.is_a?(Hash) && statement =~ /:\w+/
@@ -101,7 +138,7 @@ module ActiveRecord
end
end
- def replace_bind_variables(statement, values) #:nodoc:
+ def replace_bind_variables(statement, values) # :nodoc:
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
bound = values.dup
c = connection
@@ -110,7 +147,7 @@ module ActiveRecord
end
end
- def replace_bind_variable(value, c = connection) #:nodoc:
+ def replace_bind_variable(value, c = connection) # :nodoc:
if ActiveRecord::Relation === value
value.to_sql
else
@@ -118,7 +155,7 @@ module ActiveRecord
end
end
- def replace_named_bind_variables(statement, bind_vars) #:nodoc:
+ def replace_named_bind_variables(statement, bind_vars) # :nodoc:
statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
if $1 == ':' # skip postgresql casts
match # return the whole match
@@ -130,7 +167,7 @@ module ActiveRecord
end
end
- def quote_bound_value(value, c = connection) #:nodoc:
+ def quote_bound_value(value, c = connection) # :nodoc:
if value.respond_to?(:map) && !value.acts_like?(:string)
if value.respond_to?(:empty?) && value.empty?
c.quote(nil)
@@ -142,7 +179,7 @@ module ActiveRecord
end
end
- def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
+ def raise_if_bind_arity_mismatch(statement, expected, provided) # :nodoc:
unless expected == provided
raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index fd48ea402a..2362dae9fc 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -89,7 +89,7 @@ HEADER
end
def tables(stream)
- sorted_tables = @connection.tables.sort
+ sorted_tables = @connection.data_sources.sort - @connection.views
sorted_tables.each do |table_name|
table(table_name, stream) unless ignored?(table_name)
@@ -112,20 +112,27 @@ HEADER
tbl = StringIO.new
# first dump primary key column
- pk = @connection.primary_key(table)
+ if @connection.respond_to?(:primary_keys)
+ pk = @connection.primary_keys(table)
+ pk = pk.first unless pk.size > 1
+ else
+ pk = @connection.primary_key(table)
+ end
tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
- pkcol = columns.detect { |c| c.name == pk }
- if pkcol
- if pk != 'id'
- tbl.print %Q(, primary_key: "#{pk}")
- end
+
+ case pk
+ when String
+ tbl.print ", primary_key: #{pk.inspect}" unless pk == 'id'
+ pkcol = columns.detect { |c| c.name == pk }
pkcolspec = @connection.column_spec_for_primary_key(pkcol)
if pkcolspec
pkcolspec.each do |key, value|
tbl.print ", #{key}: #{value}"
end
end
+ when Array
+ tbl.print ", primary_key: #{pk.inspect}"
else
tbl.print ", id: false"
end
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index ba5dd0a0d9..8929aa85c8 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -56,22 +56,21 @@ module ActiveRecord
end
def structure_dump(filename)
- args = prepare_command_options('mysqldump')
+ args = prepare_command_options
args.concat(["--result-file", "#{filename}"])
args.concat(["--no-data"])
args.concat(["--routines"])
args.concat(["#{configuration['database']}"])
- unless Kernel.system(*args)
- $stderr.puts "Could not dump the database structure. "\
- "Make sure `mysqldump` is in your PATH and check the command output for warnings."
- end
+
+ run_cmd('mysqldump', args, 'dumping')
end
def structure_load(filename)
- args = prepare_command_options('mysql')
+ args = prepare_command_options
args.concat(['--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}])
args.concat(["--database", "#{configuration['database']}"])
- Kernel.system(*args)
+
+ run_cmd('mysql', args, 'loading')
end
private
@@ -130,7 +129,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
$stdin.gets.strip
end
- def prepare_command_options(command)
+ def prepare_command_options
args = {
'host' => '--host',
'port' => '--port',
@@ -145,7 +144,17 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
'sslkey' => '--ssl-key'
}.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact
- [command, *args]
+ args
+ end
+
+ def run_cmd(cmd, args, action)
+ fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args)
+ end
+
+ def run_cmd_error(cmd, args, action)
+ msg = "failed to execute: `#{cmd}`\n"
+ msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n"
+ msg
end
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 4a569fc242..1a2988ea77 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -11,6 +11,7 @@ module ActiveRecord
:before_commit_without_transaction_enrollment,
:commit_without_transaction_enrollment,
:rollback_without_transaction_enrollment,
+ terminator: deprecated_false_terminator,
scope: [:kind, :name]
end
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index 53f3b53bec..74dfe88349 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -1,27 +1,18 @@
-require 'active_record/type/helpers'
-require 'active_record/type/value'
+require 'active_model/type'
+
+require 'active_record/type/internal/abstract_json'
+require 'active_record/type/internal/timezone'
-require 'active_record/type/big_integer'
-require 'active_record/type/binary'
-require 'active_record/type/boolean'
require 'active_record/type/date'
require 'active_record/type/date_time'
-require 'active_record/type/decimal'
-require 'active_record/type/decimal_without_scale'
-require 'active_record/type/float'
-require 'active_record/type/integer'
-require 'active_record/type/serialized'
-require 'active_record/type/string'
-require 'active_record/type/text'
require 'active_record/type/time'
-require 'active_record/type/unsigned_integer'
+require 'active_record/type/serialized'
require 'active_record/type/adapter_specific_registry'
+
require 'active_record/type/type_map'
require 'active_record/type/hash_lookup_type_map'
-require 'active_record/type/internal/abstract_json'
-
module ActiveRecord
module Type
@registry = AdapterSpecificRegistry.new
@@ -53,6 +44,19 @@ module ActiveRecord
end
end
+ Helpers = ActiveModel::Type::Helpers
+ BigInteger = ActiveModel::Type::BigInteger
+ Binary = ActiveModel::Type::Binary
+ Boolean = ActiveModel::Type::Boolean
+ Decimal = ActiveModel::Type::Decimal
+ DecimalWithoutScale = ActiveModel::Type::DecimalWithoutScale
+ Float = ActiveModel::Type::Float
+ Integer = ActiveModel::Type::Integer
+ String = ActiveModel::Type::String
+ Text = ActiveModel::Type::Text
+ UnsignedInteger = ActiveModel::Type::UnsignedInteger
+ Value = ActiveModel::Type::Value
+
register(:big_integer, Type::BigInteger, override: false)
register(:binary, Type::Binary, override: false)
register(:boolean, Type::Boolean, override: false)
diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb
index 5f71b3cb94..d440eac619 100644
--- a/activerecord/lib/active_record/type/adapter_specific_registry.rb
+++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb
@@ -1,35 +1,24 @@
+require 'active_model/type/registry'
+
module ActiveRecord
# :stopdoc:
module Type
- class AdapterSpecificRegistry
- def initialize
- @registrations = []
- end
-
- def register(type_name, klass = nil, **options, &block)
- block ||= proc { |_, *args| klass.new(*args) }
- registrations << Registration.new(type_name, block, **options)
+ class AdapterSpecificRegistry < ActiveModel::Type::Registry
+ def add_modifier(options, klass, **args)
+ registrations << DecorationRegistration.new(options, klass, **args)
end
- def lookup(symbol, *args)
- registration = registrations
- .select { |r| r.matches?(symbol, *args) }
- .max
+ private
- if registration
- registration.call(self, symbol, *args)
- else
- raise ArgumentError, "Unknown type #{symbol.inspect}"
- end
+ def registration_klass
+ Registration
end
- def add_modifier(options, klass, **args)
- registrations << DecorationRegistration.new(options, klass, **args)
+ def find_registration(symbol, *args)
+ registrations
+ .select { |registration| registration.matches?(symbol, *args) }
+ .max
end
-
- protected
-
- attr_reader :registrations
end
class Registration
@@ -137,6 +126,5 @@ module ActiveRecord
class TypeConflictError < StandardError
end
-
# :startdoc:
end
diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb
index 3ceab59ebb..ccafed054e 100644
--- a/activerecord/lib/active_record/type/date.rb
+++ b/activerecord/lib/active_record/type/date.rb
@@ -1,49 +1,7 @@
module ActiveRecord
module Type
- class Date < Value # :nodoc:
- include Helpers::AcceptsMultiparameterTime.new
-
- def type
- :date
- end
-
- def type_cast_for_schema(value)
- "'#{value.to_s(:db)}'"
- end
-
- private
-
- def cast_value(value)
- if value.is_a?(::String)
- return if value.empty?
- fast_string_to_date(value) || fallback_string_to_date(value)
- elsif value.respond_to?(:to_date)
- value.to_date
- else
- value
- end
- end
-
- def fast_string_to_date(string)
- if string =~ ConnectionAdapters::Column::Format::ISO_DATE
- new_date $1.to_i, $2.to_i, $3.to_i
- end
- end
-
- def fallback_string_to_date(string)
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
- end
-
- def new_date(year, mon, mday)
- if year && year != 0
- ::Date.new(year, mon, mday) rescue nil
- end
- end
-
- def value_from_multiparameter_assignment(*)
- time = super
- time && time.to_date
- end
+ class Date < ActiveModel::Type::Date
+ include Internal::Timezone
end
end
end
diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb
index a5199959b9..1fb9380ecd 100644
--- a/activerecord/lib/active_record/type/date_time.rb
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -1,44 +1,7 @@
module ActiveRecord
module Type
- class DateTime < Value # :nodoc:
- include Helpers::TimeValue
- include Helpers::AcceptsMultiparameterTime.new(
- defaults: { 4 => 0, 5 => 0 }
- )
-
- def type
- :datetime
- end
-
- private
-
- def cast_value(string)
- return string unless string.is_a?(::String)
- return if string.empty?
-
- fast_string_to_time(string) || fallback_string_to_time(string)
- end
-
- # '0.123456' -> 123456
- # '1.123456' -> 123456
- def microseconds(time)
- time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
- end
-
- def fallback_string_to_time(string)
- time_hash = ::Date._parse(string)
- time_hash[:sec_fraction] = microseconds(time_hash)
-
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
- end
-
- def value_from_multiparameter_assignment(values_hash)
- missing_parameter = (1..3).detect { |key| !values_hash.key?(key) }
- if missing_parameter
- raise ArgumentError, missing_parameter
- end
- super
- end
+ class DateTime < ActiveModel::Type::DateTime
+ include Internal::Timezone
end
end
end
diff --git a/activerecord/lib/active_record/type/helpers.rb b/activerecord/lib/active_record/type/helpers.rb
deleted file mode 100644
index 634d417d13..0000000000
--- a/activerecord/lib/active_record/type/helpers.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'active_record/type/helpers/accepts_multiparameter_time'
-require 'active_record/type/helpers/numeric'
-require 'active_record/type/helpers/mutable'
-require 'active_record/type/helpers/time_value'
diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb
index 963a8245d0..097d1bd363 100644
--- a/activerecord/lib/active_record/type/internal/abstract_json.rb
+++ b/activerecord/lib/active_record/type/internal/abstract_json.rb
@@ -1,8 +1,8 @@
module ActiveRecord
module Type
module Internal # :nodoc:
- class AbstractJson < Type::Value # :nodoc:
- include Type::Helpers::Mutable
+ class AbstractJson < ActiveModel::Type::Value # :nodoc:
+ include ActiveModel::Type::Helpers::Mutable
def type
:json
diff --git a/activerecord/lib/active_record/type/internal/timezone.rb b/activerecord/lib/active_record/type/internal/timezone.rb
new file mode 100644
index 0000000000..947e06158a
--- /dev/null
+++ b/activerecord/lib/active_record/type/internal/timezone.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module Type
+ module Internal
+ module Timezone
+ def is_utc?
+ ActiveRecord::Base.default_timezone == :utc
+ end
+
+ def default_timezone
+ ActiveRecord::Base.default_timezone
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
index ea3e0d6a45..4ff0740cfb 100644
--- a/activerecord/lib/active_record/type/serialized.rb
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module Type
- class Serialized < DelegateClass(Type::Value) # :nodoc:
- include Helpers::Mutable
+ class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc:
+ include ActiveModel::Type::Helpers::Mutable
attr_reader :subtype, :coder
@@ -41,6 +41,12 @@ module ActiveRecord
ActiveRecord::Store::IndifferentHashAccessor
end
+ def assert_valid_value(value)
+ if coder.respond_to?(:assert_valid_value)
+ coder.assert_valid_value(value)
+ end
+ end
+
private
def default_value?(value)
diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb
index 19a10021bc..70988d84ff 100644
--- a/activerecord/lib/active_record/type/time.rb
+++ b/activerecord/lib/active_record/type/time.rb
@@ -1,42 +1,8 @@
module ActiveRecord
module Type
- class Time < Value # :nodoc:
- include Helpers::TimeValue
- include Helpers::AcceptsMultiparameterTime.new(
- defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
- )
-
- def type
- :time
- end
-
- def user_input_in_time_zone(value)
- return unless value.present?
-
- case value
- when ::String
- value = "2000-01-01 #{value}"
- when ::Time
- value = value.change(year: 2000, day: 1, month: 1)
- end
-
- super(value)
- end
-
- private
-
- def cast_value(value)
- return value unless value.is_a?(::String)
- return if value.empty?
-
- dummy_time_value = "2000-01-01 #{value}"
-
- fast_string_to_time(dummy_time_value) || begin
- time_hash = ::Date._parse(dummy_time_value)
- return if time_hash[:hour].nil?
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
- end
- end
+ class Time < ActiveModel::Type::Time
+ include Internal::Timezone
end
end
end
+
diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb
index 09f5ba6b74..81d7ed39bb 100644
--- a/activerecord/lib/active_record/type/type_map.rb
+++ b/activerecord/lib/active_record/type/type_map.rb
@@ -1,12 +1,12 @@
-require 'thread_safe'
+require 'concurrent'
module ActiveRecord
module Type
class TypeMap # :nodoc:
def initialize
@mapping = {}
- @cache = ThreadSafe::Cache.new do |h, key|
- h.fetch_or_store(key, ThreadSafe::Cache.new)
+ @cache = Concurrent::Map.new do |h, key|
+ h.fetch_or_store(key, Concurrent::Map.new)
end
end
@@ -57,7 +57,7 @@ module ActiveRecord
end
def default_value
- @default_value ||= Value.new
+ @default_value ||= ActiveModel::Type::Value.new
end
end
end
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
index 3878270770..868d08ed44 100644
--- a/activerecord/lib/active_record/type_caster/connection.rb
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -20,7 +20,7 @@ module ActiveRecord
private
def column_for(attribute_name)
- if connection.schema_cache.table_exists?(table_name)
+ if connection.schema_cache.data_source_exists?(table_name)
connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
end
end
diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
index 49a68fb94c..43c817e057 100644
--- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb
+++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
@@ -7,7 +7,7 @@ module ActiveRecord
module ConnectionAdapters
class FakeAdapter < AbstractAdapter
- attr_accessor :tables, :primary_keys
+ attr_accessor :data_sources, :primary_keys
@columns = Hash.new { |h,k| h[k] = [] }
class << self
@@ -16,7 +16,7 @@ module ActiveRecord
def initialize(connection, logger)
super
- @tables = []
+ @data_sources = []
@primary_keys = {}
@columns = self.class.columns
end
@@ -37,7 +37,7 @@ module ActiveRecord
@columns[table_name]
end
- def table_exists?(*)
+ def data_source_exists?(*)
true
end
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 1712ff0ac6..62579a4a7a 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -36,6 +36,21 @@ module ActiveRecord
assert !@connection.table_exists?(nil)
end
+ def test_data_sources
+ data_sources = @connection.data_sources
+ assert data_sources.include?("accounts")
+ assert data_sources.include?("authors")
+ assert data_sources.include?("tasks")
+ assert data_sources.include?("topics")
+ end
+
+ def test_data_source_exists?
+ assert @connection.data_source_exists?("accounts")
+ assert @connection.data_source_exists?(:accounts)
+ assert_not @connection.data_source_exists?("nonexistingtable")
+ assert_not @connection.data_source_exists?(nil)
+ end
+
def test_indexes
idx_name = "accounts_idx"
@@ -63,7 +78,7 @@ module ActiveRecord
end
end
- if current_adapter?(:MysqlAdapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_charset
assert_not_nil @connection.charset
assert_not_equal 'character_set_database', @connection.charset
diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
index f0fd95ac16..0b5c9e1798 100644
--- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
@@ -100,17 +100,15 @@ class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase
assert_equal "DROP TABLE `people`", drop_table(:people)
end
- if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- def test_create_mysql_database_with_encoding
- assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
- assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
- assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
- end
+ def test_create_mysql_database_with_encoding
+ assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
+ assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
+ assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
+ end
- def test_recreate_mysql_database_with_encoding
- create_database(:luca, {:charset => 'latin1'})
- assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'})
- end
+ def test_recreate_mysql_database_with_encoding
+ create_database(:luca, {:charset => 'latin1'})
+ assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'})
end
def test_add_column
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index ddbc007b87..8b62998964 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -183,7 +183,7 @@ class MysqlConnectionTest < ActiveRecord::MysqlTestCase
def with_example_table(&block)
definition ||= <<-SQL
- `id` int(11) auto_increment PRIMARY KEY,
+ `id` int auto_increment PRIMARY KEY,
`data` varchar(255)
SQL
super(@connection, 'ex', definition, &block)
diff --git a/activerecord/test/cases/adapters/mysql/explain_test.rb b/activerecord/test/cases/adapters/mysql/explain_test.rb
new file mode 100644
index 0000000000..c44c1e6648
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql/explain_test.rb
@@ -0,0 +1,21 @@
+require "cases/helper"
+require 'models/developer'
+require 'models/computer'
+
+class MysqlExplainTest < ActiveRecord::MysqlTestCase
+ fixtures :developers
+
+ def test_explain_for_one_query
+ explain = Developer.where(id: 1).explain
+ assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
+ assert_match %r(developers |.* const), explain
+ end
+
+ def test_explain_with_eager_loading
+ explain = Developer.where(id: 1).includes(:audit_logs).explain
+ assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
+ assert_match %r(developers |.* const), explain
+ assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain
+ assert_match %r(audit_logs |.* ALL), explain
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
index b804cb45b9..29573d8e0d 100644
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
@@ -1,4 +1,3 @@
-
require "cases/helper"
require 'support/ddl_helper'
@@ -66,14 +65,6 @@ module ActiveRecord
end
end
- def test_tables_quoting
- @conn.tables(nil, "foo-bar", nil)
- flunk
- rescue => e
- # assertion for *quoted* database properly
- assert_match(/database 'foo-bar'/, e.inspect)
- end
-
def test_pk_and_sequence_for
with_example_table do
pk, seq = @conn.pk_and_sequence_for('ex')
@@ -83,7 +74,7 @@ module ActiveRecord
end
def test_pk_and_sequence_for_with_non_standard_primary_key
- with_example_table '`code` INT(11) auto_increment, PRIMARY KEY (`code`)' do
+ with_example_table '`code` INT auto_increment, PRIMARY KEY (`code`)' do
pk, seq = @conn.pk_and_sequence_for('ex')
assert_equal 'code', pk
assert_equal @conn.default_sequence_name('ex', 'code'), seq
@@ -91,7 +82,7 @@ module ActiveRecord
end
def test_pk_and_sequence_for_with_custom_index_type_pk
- with_example_table '`id` INT(11) auto_increment, PRIMARY KEY USING BTREE (`id`)' do
+ with_example_table '`id` INT auto_increment, PRIMARY KEY USING BTREE (`id`)' do
pk, seq = @conn.pk_and_sequence_for('ex')
assert_equal 'id', pk
assert_equal @conn.default_sequence_name('ex', 'id'), seq
@@ -99,7 +90,7 @@ module ActiveRecord
end
def test_composite_primary_key
- with_example_table '`id` INT(11), `number` INT(11), foo INT(11), PRIMARY KEY (`id`, `number`)' do
+ with_example_table '`id` INT, `number` INT, foo INT, PRIMARY KEY (`id`, `number`)' do
assert_nil @conn.primary_key('ex')
end
end
@@ -141,7 +132,7 @@ module ActiveRecord
def with_example_table(definition = nil, &block)
definition ||= <<-SQL
- `id` int(11) auto_increment PRIMARY KEY,
+ `id` int auto_increment PRIMARY KEY,
`number` integer,
`data` varchar(255)
SQL
diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
index 6be36566de..0d1f968022 100644
--- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
@@ -3,7 +3,7 @@ require 'cases/helper'
class MysqlStatementPoolTest < ActiveRecord::MysqlTestCase
if Process.respond_to?(:fork)
def test_cache_is_per_pid
- cache = ActiveRecord::ConnectionAdapters::MysqlAdapter::StatementPool.new nil, 10
+ cache = ActiveRecord::ConnectionAdapters::MysqlAdapter::StatementPool.new(10)
cache['foo'] = 'bar'
assert_equal 'bar', cache['foo']
diff --git a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb
index ed9398a918..84c5394c2e 100644
--- a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb
+++ b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb
@@ -1,6 +1,8 @@
require "cases/helper"
+require "support/schema_dumping_helper"
class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase
+ include SchemaDumpingHelper
self.use_transactional_tests = false
class UnsignedType < ActiveRecord::Base
@@ -9,12 +11,15 @@ class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table("unsigned_types", force: true) do |t|
- t.column :unsigned_integer, "int unsigned"
+ t.integer :unsigned_integer, unsigned: true
+ t.bigint :unsigned_bigint, unsigned: true
+ t.float :unsigned_float, unsigned: true
+ t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2
end
end
teardown do
- @connection.drop_table "unsigned_types"
+ @connection.drop_table "unsigned_types", if_exists: true
end
test "unsigned int max value is in range" do
@@ -26,5 +31,35 @@ class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase
assert_raise(RangeError) do
UnsignedType.create(unsigned_integer: -10)
end
+ assert_raise(RangeError) do
+ UnsignedType.create(unsigned_bigint: -10)
+ end
+ assert_raise(ActiveRecord::StatementInvalid) do
+ UnsignedType.create(unsigned_float: -10.0)
+ end
+ assert_raise(ActiveRecord::StatementInvalid) do
+ UnsignedType.create(unsigned_decimal: -10.0)
+ end
+ end
+
+ test "schema definition can use unsigned as the type" do
+ @connection.change_table("unsigned_types") do |t|
+ t.unsigned_integer :unsigned_integer_t
+ t.unsigned_bigint :unsigned_bigint_t
+ t.unsigned_float :unsigned_float_t
+ t.unsigned_decimal :unsigned_decimal_t, precision: 10, scale: 2
+ end
+
+ @connection.columns("unsigned_types").select { |c| /^unsigned_/ === c.name }.each do |column|
+ assert column.unsigned?
+ end
+ end
+
+ test "schema dump includes unsigned option" do
+ schema = dump_table_schema "unsigned_types"
+ assert_match %r{t.integer\s+"unsigned_integer",\s+limit: 4,\s+unsigned: true$}, schema
+ assert_match %r{t.integer\s+"unsigned_bigint",\s+limit: 8,\s+unsigned: true$}, schema
+ assert_match %r{t.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema
+ assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index 6558d60aa1..31dc69a45b 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -100,17 +100,15 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
assert_equal "DROP TABLE `people`", drop_table(:people)
end
- if current_adapter?(:Mysql2Adapter)
- def test_create_mysql_database_with_encoding
- assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
- assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
- assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
- end
+ def test_create_mysql_database_with_encoding
+ assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
+ assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
+ assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci})
+ end
- def test_recreate_mysql_database_with_encoding
- create_database(:luca, {:charset => 'latin1'})
- assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'})
- end
+ def test_recreate_mysql_database_with_encoding
+ create_database(:luca, {:charset => 'latin1'})
+ assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'})
end
def test_add_column
diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb
index 880a2123d2..faf2acb9cb 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb
@@ -36,14 +36,6 @@ module ActiveRecord
assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist")
end
- def test_tables_quoting
- @connection.tables(nil, "foo-bar", nil)
- flunk
- rescue => e
- # assertion for *quoted* database properly
- assert_match(/database 'foo-bar'/, e.inspect)
- end
-
def test_dump_indexes
index_a_name = 'index_key_tests_on_snack'
index_b_name = 'index_key_tests_on_pizza'
diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
index 9e06db2519..a6f6dd21bb 100644
--- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
@@ -1,6 +1,8 @@
require "cases/helper"
+require "support/schema_dumping_helper"
class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
self.use_transactional_tests = false
class UnsignedType < ActiveRecord::Base
@@ -9,12 +11,15 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table("unsigned_types", force: true) do |t|
- t.column :unsigned_integer, "int unsigned"
+ t.integer :unsigned_integer, unsigned: true
+ t.bigint :unsigned_bigint, unsigned: true
+ t.float :unsigned_float, unsigned: true
+ t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2
end
end
teardown do
- @connection.drop_table "unsigned_types"
+ @connection.drop_table "unsigned_types", if_exists: true
end
test "unsigned int max value is in range" do
@@ -26,5 +31,35 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
assert_raise(RangeError) do
UnsignedType.create(unsigned_integer: -10)
end
+ assert_raise(RangeError) do
+ UnsignedType.create(unsigned_bigint: -10)
+ end
+ assert_raise(ActiveRecord::StatementInvalid) do
+ UnsignedType.create(unsigned_float: -10.0)
+ end
+ assert_raise(ActiveRecord::StatementInvalid) do
+ UnsignedType.create(unsigned_decimal: -10.0)
+ end
+ end
+
+ test "schema definition can use unsigned as the type" do
+ @connection.change_table("unsigned_types") do |t|
+ t.unsigned_integer :unsigned_integer_t
+ t.unsigned_bigint :unsigned_bigint_t
+ t.unsigned_float :unsigned_float_t
+ t.unsigned_decimal :unsigned_decimal_t, precision: 10, scale: 2
+ end
+
+ @connection.columns("unsigned_types").select { |c| /^unsigned_/ === c.name }.each do |column|
+ assert column.unsigned?
+ end
+ end
+
+ test "schema dump includes unsigned option" do
+ schema = dump_table_schema "unsigned_types"
+ assert_match %r{t.integer\s+"unsigned_integer",\s+limit: 4,\s+unsigned: true$}, schema
+ assert_match %r{t.integer\s+"unsigned_bigint",\s+limit: 8,\s+unsigned: true$}, schema
+ assert_match %r{t.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema
+ assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index 6e6850c4a9..e361521155 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -123,6 +123,20 @@ module ActiveRecord
assert_equal expect.to_i, result.rows.first.first
end
+ def test_exec_insert_default_values_with_returning_disabled_and_no_sequence_name_given
+ connection = connection_without_insert_returning
+ result = connection.exec_insert("insert into postgresql_partitioned_table_parent DEFAULT VALUES", nil, [], 'id')
+ expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first
+ assert_equal expect.to_i, result.rows.first.first
+ end
+
+ def test_exec_insert_default_values_quoted_schema_with_returning_disabled_and_no_sequence_name_given
+ connection = connection_without_insert_returning
+ result = connection.exec_insert('insert into "public"."postgresql_partitioned_table_parent" DEFAULT VALUES', nil, [], 'id')
+ expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first
+ assert_equal expect.to_i, result.rows.first.first
+ end
+
def test_sql_for_insert_with_returning_disabled
connection = connection_without_insert_returning
result = connection.sql_for_insert('sql', nil, nil, nil, 'binds')
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index bee612d8d3..f89a394f96 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -5,9 +5,11 @@ require 'support/schema_dumping_helper'
module PGSchemaHelper
def with_schema_search_path(schema_search_path)
@connection.schema_search_path = schema_search_path
+ @connection.schema_cache.clear!
yield if block_given?
ensure
@connection.schema_search_path = "'$user', public"
+ @connection.schema_cache.clear!
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
index ef324183a7..559b951109 100644
--- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
@@ -6,7 +6,7 @@ module ActiveRecord::ConnectionAdapters
if Process.respond_to?(:fork)
def test_cache_is_per_pid
- cache = StatementPool.new nil, 10
+ cache = StatementPool.new(10)
cache['foo'] = 'bar'
assert_equal 'bar', cache['foo']
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 02b67f901f..938350627f 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -500,7 +500,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
line_item = LineItem.create!
invoice = Invoice.create!(line_items: [line_item])
initial = invoice.updated_at
- line_item.touch
+ travel(1.second) do
+ line_item.touch
+ end
assert_not_equal initial, invoice.reload.updated_at
end
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 d160c30375..20af436e02 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
@@ -95,6 +95,15 @@ class DeveloperWithExtendOption < Developer
has_and_belongs_to_many :projects, extend: NamedExtension
end
+class ProjectUnscopingDavidDefaultScope < ActiveRecord::Base
+ self.table_name = 'projects'
+ has_and_belongs_to_many :developers, -> { unscope(where: 'name') },
+ class_name: "LazyBlockDeveloperCalledDavid",
+ join_table: "developers_projects",
+ foreign_key: "project_id",
+ association_foreign_key: "developer_id"
+end
+
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
:parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers
@@ -936,4 +945,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
assert_equal 1, professor.courses.count
end
+
+ def test_habtm_scope_can_unscope
+ project = ProjectUnscopingDavidDefaultScope.new
+ project.save!
+
+ developer = LazyBlockDeveloperCalledDavid.new(name: "Not David")
+ developer.save!
+ project.developers << developer
+
+ projects = ProjectUnscopingDavidDefaultScope.includes(:developers).where(id: project.id)
+ assert_equal 1, projects.first.developers.size
+ end
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 0beaf0056a..cd19a7a5bc 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -170,7 +170,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
part = ShipPart.create(name: 'cockpit')
updated_at = part.updated_at
- ship.parts << part
+ travel(1.second) do
+ ship.parts << part
+ end
assert_equal part.ship, ship
assert_not_equal part.updated_at, updated_at
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 423b8238b1..ece4dab539 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -13,6 +13,9 @@ require 'models/mixed_case_monkey'
require 'models/admin'
require 'models/admin/account'
require 'models/admin/user'
+require 'models/developer'
+require 'models/company'
+require 'models/project'
class AutomaticInverseFindingTests < ActiveRecord::TestCase
fixtures :ratings, :comments, :cars
@@ -198,6 +201,16 @@ class InverseAssociationTests < ActiveRecord::TestCase
belongs_to_ref = Sponsor.reflect_on_association(:sponsor_club)
assert_nil belongs_to_ref.inverse_of
end
+
+ def test_this_inverse_stuff
+ firm = Firm.create!(name: 'Adequate Holdings')
+ Project.create!(name: 'Project 1', firm: firm)
+ Developer.create!(name: 'Gorbypuff', firm: firm)
+
+ new_project = Project.last
+ assert Project.reflect_on_association(:lead_developer).inverse_of.present?, "Expected inverse of to be present"
+ assert new_project.lead_developer.present?, "Expected lead developer to be present on the project"
+ end
end
class InverseHasOneTests < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index e2608e3670..52d197718e 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -175,9 +175,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal category_attrs , category.attributes_before_type_cast
end
- if current_adapter?(:MysqlAdapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_read_attributes_before_type_cast_on_boolean
- bool = Boolean.create({ "value" => false })
+ bool = Boolean.create!({ "value" => false })
if RUBY_PLATFORM =~ /java/
# JRuby will return the value before typecast as string
assert_equal "0", bool.reload.attributes_before_type_cast["value"]
@@ -542,9 +542,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
developer.save!
- assert_equal "50000", developer.salary_before_type_cast
- assert_equal 1337, developer.name_before_type_cast
-
assert_equal 50000, developer.salary
assert_equal "1337", developer.name
end
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
index 9d927481ec..5a0e463a48 100644
--- a/activerecord/test/cases/attribute_set_test.rb
+++ b/activerecord/test/cases/attribute_set_test.rb
@@ -29,7 +29,7 @@ module ActiveRecord
assert_equal :bar, attributes[:bar].name
end
- test "duping creates a new hash and dups each attribute" do
+ test "duping creates a new hash, but does not dup the attributes" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
attributes = builder.build_from_database(foo: 1, bar: 'foo')
@@ -43,6 +43,24 @@ module ActiveRecord
assert_equal 1, attributes[:foo].value
assert_equal 2, duped[:foo].value
+ assert_equal 'foobar', attributes[:bar].value
+ assert_equal 'foobar', duped[:bar].value
+ end
+
+ test "deep_duping creates a new hash and dups each attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
+ attributes = builder.build_from_database(foo: 1, bar: 'foo')
+
+ # Ensure the type cast value is cached
+ attributes[:foo].value
+ attributes[:bar].value
+
+ duped = attributes.deep_dup
+ duped.write_from_database(:foo, 2)
+ duped[:bar].value << 'bar'
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2, duped[:foo].value
assert_equal 'foo', attributes[:bar].value
assert_equal 'foobar', duped[:bar].value
end
@@ -160,6 +178,9 @@ module ActiveRecord
return if value.nil?
value + " from database"
end
+
+ def assert_valid_value(*)
+ end
end
test "write_from_database sets the attribute with database typecasting" do
@@ -207,5 +228,16 @@ module ActiveRecord
assert_equal [:foo], attributes.accessed
end
+
+ test "#map returns a new attribute set with the changes applied" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: "1", bar: "2")
+ new_attributes = attributes.map do |attr|
+ attr.with_cast_value(attr.value + 1)
+ end
+
+ assert_equal 2, new_attributes.fetch_value(:foo)
+ assert_equal 3, new_attributes.fetch_value(:bar)
+ end
end
end
diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb
index 0ec368f51d..a24a4fc6a4 100644
--- a/activerecord/test/cases/attribute_test.rb
+++ b/activerecord/test/cases/attribute_test.rb
@@ -4,7 +4,6 @@ module ActiveRecord
class AttributeTest < ActiveRecord::TestCase
setup do
@type = Minitest::Mock.new
- @type.expect(:==, false, [false])
end
teardown do
@@ -108,6 +107,9 @@ module ActiveRecord
def deserialize(value)
value + " from database"
end
+
+ def assert_valid_value(*)
+ end
end
test "with_value_from_user returns a new attribute with the value from the user" do
@@ -180,12 +182,65 @@ module ActiveRecord
assert attribute.has_been_read?
end
+ test "an attribute is not changed if it hasn't been assigned or mutated" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+
+ refute attribute.changed?
+ end
+
+ test "an attribute is changed if it's been assigned a new value" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ changed = attribute.with_value_from_user(2)
+
+ assert changed.changed?
+ end
+
+ test "an attribute is not changed if it's assigned the same value" do
+ attribute = Attribute.from_database(:foo, 1, Type::Value.new)
+ unchanged = attribute.with_value_from_user(1)
+
+ refute unchanged.changed?
+ end
+
test "an attribute can not be mutated if it has not been read,
and skips expensive calculations" do
type_which_raises_from_all_methods = Object.new
attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods)
- assert_not attribute.changed_in_place_from?("bar")
+ assert_not attribute.changed_in_place?
+ end
+
+ test "an attribute is changed if it has been mutated" do
+ attribute = Attribute.from_database(:foo, "bar", Type::String.new)
+ attribute.value << "!"
+
+ assert attribute.changed_in_place?
+ assert attribute.changed?
+ end
+
+ test "an attribute can forget its changes" do
+ attribute = Attribute.from_database(:foo, "bar", Type::String.new)
+ changed = attribute.with_value_from_user("foo")
+ forgotten = changed.forgetting_assignment
+
+ assert changed.changed? # sanity check
+ refute forgotten.changed?
+ end
+
+ test "with_value_from_user validates the value" do
+ type = Type::Value.new
+ type.define_singleton_method(:assert_valid_value) do |value|
+ if value == 1
+ raise ArgumentError
+ end
+ end
+
+ attribute = Attribute.from_database(:foo, 1, type)
+ assert_equal 1, attribute.value
+ assert_equal 2, attribute.with_value_from_user(2).value
+ assert_raises ArgumentError do
+ attribute.with_value_from_user(1)
+ end
end
end
end
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 3aa0849cb5..dbbcaa075d 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -204,7 +204,7 @@ class BasicsTest < ActiveRecord::TestCase
)
# For adapters which support microsecond resolution.
- if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) || mysql_56?
+ if subsecond_precision_supported?
assert_equal 11, Topic.find(1).written_on.sec
assert_equal 223300, Topic.find(1).written_on.usec
assert_equal 9900, Topic.find(2).written_on.usec
@@ -1571,4 +1571,22 @@ class BasicsTest < ActiveRecord::TestCase
assert_not topic.id_changed?
end
+
+ test "ignored columns are not present in columns_hash" do
+ cache_columns = Developer.connection.schema_cache.columns_hash(Developer.table_name)
+ assert_includes cache_columns.keys, 'first_name'
+ refute_includes Developer.columns_hash.keys, 'first_name'
+ end
+
+ test "ignored columns have no attribute methods" do
+ refute Developer.new.respond_to?(:first_name)
+ refute Developer.new.respond_to?(:first_name=)
+ refute Developer.new.respond_to?(:first_name?)
+ end
+
+ test "ignored columns don't prevent explicit declaration of attribute methods" do
+ assert Developer.new.respond_to?(:last_name)
+ assert Developer.new.respond_to?(:last_name=)
+ assert Developer.new.respond_to?(:last_name?)
+ end
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index aa10817527..d904b802fa 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -681,4 +681,36 @@ class CalculationsTest < ActiveRecord::TestCase
end
assert block_called
end
+
+ def test_having_with_strong_parameters
+ protected_params = Class.new do
+ attr_reader :permitted
+ alias :permitted? :permitted
+
+ def initialize(parameters)
+ @parameters = parameters
+ @permitted = false
+ end
+
+ def to_h
+ @parameters
+ end
+
+ def permit!
+ @permitted = true
+ self
+ end
+ end
+
+ params = protected_params.new(credit_limit: '50')
+
+ assert_raises(ActiveModel::ForbiddenAttributesError) do
+ Account.group(:id).having(params)
+ end
+
+ result = Account.group(:id).having(params.permit!)
+ assert_equal 50, result[0].credit_limit
+ assert_equal 50, result[1].credit_limit
+ assert_equal 50, result[2].credit_limit
+ end
end
diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
index 80244d1439..2749273884 100644
--- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
+++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
@@ -22,6 +22,10 @@ module ActiveRecord
assert_lookup_type :string, "SET('one', 'two', 'three')"
end
+ def test_set_type_with_value_matching_other_type
+ assert_lookup_type :string, "SET('unicode', '8bit', 'none', 'time')"
+ end
+
def test_enum_type_with_value_matching_other_type
assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')"
end
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index c7531f5418..db832fe55d 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -29,7 +29,7 @@ module ActiveRecord
def test_clearing
@cache.columns('posts')
@cache.columns_hash('posts')
- @cache.tables('posts')
+ @cache.data_sources('posts')
@cache.primary_keys('posts')
@cache.clear!
@@ -40,17 +40,22 @@ module ActiveRecord
def test_dump_and_load
@cache.columns('posts')
@cache.columns_hash('posts')
- @cache.tables('posts')
+ @cache.data_sources('posts')
@cache.primary_keys('posts')
@cache = Marshal.load(Marshal.dump(@cache))
assert_equal 11, @cache.columns('posts').size
assert_equal 11, @cache.columns_hash('posts').size
- assert @cache.tables('posts')
+ assert @cache.data_sources('posts')
assert_equal 'id', @cache.primary_keys('posts')
end
+ def test_table_methods_deprecation
+ assert_deprecated { assert @cache.table_exists?('posts') }
+ assert_deprecated { assert @cache.tables('posts') }
+ assert_deprecated { @cache.clear_table_cache!('posts') }
+ end
end
end
end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index f5aaf22e13..cd1967c373 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -89,7 +89,7 @@ class DirtyTest < ActiveRecord::TestCase
target = Class.new(ActiveRecord::Base)
target.table_name = 'pirates'
- pirate = target.create
+ pirate = target.create!
pirate.created_on = pirate.created_on
assert !pirate.created_on_changed?
end
@@ -467,8 +467,10 @@ class DirtyTest < ActiveRecord::TestCase
topic.save!
updated_at = topic.updated_at
- topic.content[:hello] = 'world'
- topic.save!
+ travel(1.second) do
+ topic.content[:hello] = 'world'
+ topic.save!
+ end
assert_not_equal updated_at, topic.updated_at
assert_equal 'world', topic.content[:hello]
@@ -521,6 +523,9 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal Hash.new, pirate.previous_changes
pirate = Pirate.find_by_catchphrase("arrr")
+
+ travel(1.second)
+
pirate.catchphrase = "Me Maties!"
pirate.save!
@@ -532,6 +537,9 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.previous_changes.key?('created_on')
pirate = Pirate.find_by_catchphrase("Me Maties!")
+
+ travel(1.second)
+
pirate.catchphrase = "Thar She Blows!"
pirate.save
@@ -542,6 +550,8 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.previous_changes.key?('parrot_id')
assert !pirate.previous_changes.key?('created_on')
+ travel(1.second)
+
pirate = Pirate.find_by_catchphrase("Thar She Blows!")
pirate.update(catchphrase: "Ahoy!")
@@ -552,6 +562,8 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.previous_changes.key?('parrot_id')
assert !pirate.previous_changes.key?('created_on')
+ travel(1.second)
+
pirate = Pirate.find_by_catchphrase("Ahoy!")
pirate.update_attribute(:catchphrase, "Ninjas suck!")
@@ -561,6 +573,8 @@ class DirtyTest < ActiveRecord::TestCase
assert_not_nil pirate.previous_changes['updated_on'][1]
assert !pirate.previous_changes.key?('parrot_id')
assert !pirate.previous_changes.key?('created_on')
+ ensure
+ travel_back
end
if ActiveRecord::Base.connection.supports_migrations?
@@ -578,6 +592,7 @@ class DirtyTest < ActiveRecord::TestCase
end
def test_datetime_attribute_can_be_updated_with_fractional_seconds
+ skip "Fractional seconds are not supported" unless subsecond_precision_supported?
in_time_zone 'Paris' do
target = Class.new(ActiveRecord::Base)
target.table_name = 'topics'
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 0dc884497c..307b68764e 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -701,12 +701,12 @@ class FinderTest < ActiveRecord::TestCase
end
def test_bind_arity
- assert_nothing_raised { bind '' }
+ assert_nothing_raised { bind '' }
assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 }
assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' }
- assert_nothing_raised { bind '?', 1 }
- assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
+ assert_nothing_raised { bind '?', 1 }
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 }
end
def test_named_bind_variables
@@ -721,6 +721,12 @@ class FinderTest < ActiveRecord::TestCase
assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on
end
+ def test_named_bind_arity
+ assert_nothing_raised { bind "name = :name", { name: "37signals" } }
+ assert_nothing_raised { bind "name = :name", { name: "37signals", id: 1 } }
+ assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "name = :name", { id: 1 } }
+ end
+
class SimpleEnumerable
include Enumerable
diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb
index 92efa8aca7..242e7a9bec 100644
--- a/activerecord/test/cases/fixture_set/file_test.rb
+++ b/activerecord/test/cases/fixture_set/file_test.rb
@@ -123,6 +123,18 @@ END
end
end
+ def test_removes_fixture_config_row
+ File.open(::File.join(FIXTURES_ROOT, 'other_posts.yml')) do |fh|
+ assert_equal(['second_welcome'], fh.each.map { |name, _| name })
+ end
+ end
+
+ def test_extracts_model_class_from_config_row
+ File.open(::File.join(FIXTURES_ROOT, 'other_posts.yml')) do |fh|
+ assert_equal 'Post', fh.model_class
+ end
+ end
+
private
def tmp_yaml(name, contents)
t = Tempfile.new name
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index e3c4bd9f4a..a0eaa66e94 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -7,15 +7,16 @@ require 'models/binary'
require 'models/book'
require 'models/bulb'
require 'models/category'
+require 'models/comment'
require 'models/company'
require 'models/computer'
require 'models/course'
require 'models/developer'
+require 'models/doubloon'
require 'models/joke'
require 'models/matey'
require 'models/parrot'
require 'models/pirate'
-require 'models/doubloon'
require 'models/post'
require 'models/randomly_named_c1'
require 'models/reply'
@@ -516,6 +517,38 @@ class OverRideFixtureMethodTest < ActiveRecord::TestCase
end
end
+class FixtureWithSetModelClassTest < ActiveRecord::TestCase
+ fixtures :other_posts, :other_comments
+
+ # Set to false to blow away fixtures cache and ensure our fixtures are loaded
+ # and thus takes into account the +set_model_class+.
+ self.use_transactional_tests = false
+
+ def test_uses_fixture_class_defined_in_yaml
+ assert_kind_of Post, other_posts(:second_welcome)
+ end
+
+ def test_loads_the_associations_to_fixtures_with_set_model_class
+ post = other_posts(:second_welcome)
+ comment = other_comments(:second_greetings)
+ assert_equal [comment], post.comments
+ assert_equal post, comment.post
+ end
+end
+
+class SetFixtureClassPrevailsTest < ActiveRecord::TestCase
+ set_fixture_class bad_posts: Post
+ fixtures :bad_posts
+
+ # Set to false to blow away fixtures cache and ensure our fixtures are loaded
+ # and thus takes into account the +set_model_class+.
+ self.use_transactional_tests = false
+
+ def test_uses_set_fixture_class
+ assert_kind_of Post, bad_posts(:bad_welcome)
+ end
+end
+
class CheckSetTableNameFixturesTest < ActiveRecord::TestCase
set_fixture_class :funny_jokes => Joke
fixtures :funny_jokes
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index b61a5126e0..8773986882 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -46,10 +46,10 @@ def in_memory_db?
ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:"
end
-def mysql_56?
- current_adapter?(:MysqlAdapter, :Mysql2Adapter) &&
- ActiveRecord::Base.connection.send(:version) >= '5.6.0' &&
- ActiveRecord::Base.connection.send(:version) < '5.7.0'
+def subsecond_precision_supported?
+ !current_adapter?(:MysqlAdapter, :Mysql2Adapter) ||
+ (ActiveRecord::Base.connection.send(:version) >= '5.6.0' &&
+ ActiveRecord::Base.connection.send(:version) < '5.7.0')
end
def mysql_enforcing_gtid_consistency?
diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb
index 018b7b0d8f..9169207b0a 100644
--- a/activerecord/test/cases/integration_test.rb
+++ b/activerecord/test/cases/integration_test.rb
@@ -96,7 +96,9 @@ class IntegrationTest < ActiveRecord::TestCase
owner.update_column :updated_at, Time.current
key = owner.cache_key
- assert pet.touch
+ travel(1.second) do
+ assert pet.touch
+ end
assert_not_equal key, owner.reload.cache_key
end
@@ -125,6 +127,7 @@ class IntegrationTest < ActiveRecord::TestCase
end
def test_cache_key_format_is_precise_enough
+ skip("Subsecond precision is not supported") unless subsecond_precision_supported?
dev = Developer.first
key = dev.cache_key
dev.touch
diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb
index 5bc0898f33..ad85684c0b 100644
--- a/activerecord/test/cases/migration/helper.rb
+++ b/activerecord/test/cases/migration/helper.rb
@@ -28,7 +28,7 @@ module ActiveRecord
super
TestModel.reset_table_name
TestModel.reset_sequence_name
- connection.drop_table :test_models rescue nil
+ connection.drop_table :test_models, if_exists: true
end
private
diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb
index 1594f99852..4f0da999d8 100644
--- a/activerecord/test/cases/migration/references_foreign_key_test.rb
+++ b/activerecord/test/cases/migration/references_foreign_key_test.rb
@@ -32,6 +32,14 @@ module ActiveRecord
assert_equal [], @connection.foreign_keys("testings")
end
+ test "foreign keys can be created in one query" do
+ assert_queries(1) do
+ @connection.create_table :testings do |t|
+ t.references :testing_parent, foreign_key: true
+ end
+ end
+ end
+
test "options hash can be passed" do
@connection.change_table :testing_parents do |t|
t.integer :other_id
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index 0745a37ee9..5e4ba47988 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -241,6 +241,33 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
end
end
+class CompositePrimaryKeyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ self.use_transactional_tests = false
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t|
+ t.string :region
+ t.integer :code
+ end
+ end
+
+ def teardown
+ @connection.drop_table(:barcodes, if_exists: true)
+ end
+
+ def test_composite_primary_key
+ assert_equal ["region", "code"], @connection.primary_keys("barcodes")
+ end
+
+ def test_collectly_dump_composite_primary_key
+ schema = dump_table_schema "barcodes"
+ assert_match %r{create_table "barcodes", primary_key: \["region", "code"\]}, schema
+ end
+end
+
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase
self.use_transactional_tests = false
@@ -300,11 +327,12 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter)
if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
test "primary key column type with options" do
- @connection.create_table(:widgets, id: :primary_key, limit: 8, force: true)
+ @connection.create_table(:widgets, id: :primary_key, limit: 8, unsigned: true, force: true)
column = @connection.columns(:widgets).find { |c| c.name == 'id' }
assert column.auto_increment?
assert_equal :integer, column.type
assert_equal 8, column.limit
+ assert column.unsigned?
end
end
end
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index 6af31017d6..c3a1471205 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -276,5 +276,31 @@ module ActiveRecord
assert_equal essays(:david_modest_proposal), essay
end
+
+ def test_where_with_strong_parameters
+ protected_params = Class.new do
+ attr_reader :permitted
+ alias :permitted? :permitted
+
+ def initialize(parameters)
+ @parameters = parameters
+ @permitted = false
+ end
+
+ def to_h
+ @parameters
+ end
+
+ def permit!
+ @permitted = true
+ self
+ end
+ end
+
+ author = authors(:david)
+ params = protected_params.new(name: author.name)
+ assert_raises(ActiveModel::ForbiddenAttributesError) { Author.where(params) }
+ assert_equal author, Author.where(params.permit!).first
+ end
end
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 37d3965022..b0fb905760 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -243,18 +243,23 @@ module ActiveRecord
end
def test_select_quotes_when_using_from_clause
- if sqlite3_version_includes_quoting_bug?
- skip <<-ERROR.squish
- You are using an outdated version of SQLite3 which has a bug in
- quoted column names. Please update SQLite3 and rebuild the sqlite3
- ruby gem
- ERROR
- end
+ skip_if_sqlite3_version_includes_quoting_bug
quoted_join = ActiveRecord::Base.connection.quote_table_name("join")
selected = Post.select(:join).from(Post.select("id as #{quoted_join}")).map(&:join)
assert_equal Post.pluck(:id), selected
end
+ def test_selecting_aliased_attribute_quotes_column_name_when_from_is_used
+ skip_if_sqlite3_version_includes_quoting_bug
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = :test_with_keyword_column_name
+ alias_attribute :description, :desc
+ end
+ klass.create!(description: "foo")
+
+ assert_equal ["foo"], klass.select(:description).from(klass.all).map(&:desc)
+ end
+
def test_relation_merging_with_merged_joins_as_strings
join_string = "LEFT OUTER JOIN #{Rating.quoted_table_name} ON #{SpecialComment.quoted_table_name}.id = #{Rating.quoted_table_name}.comment_id"
special_comments_with_ratings = SpecialComment.joins join_string
@@ -292,6 +297,16 @@ module ActiveRecord
private
+ def skip_if_sqlite3_version_includes_quoting_bug
+ if sqlite3_version_includes_quoting_bug?
+ skip <<-ERROR.squish
+ You are using an outdated version of SQLite3 which has a bug in
+ quoted column names. Please update SQLite3 and rebuild the sqlite3
+ ruby gem
+ ERROR
+ end
+ end
+
def sqlite3_version_includes_quoting_bug?
if current_adapter?(:SQLite3Adapter)
selected_quoted_column_names = ActiveRecord::Base.connection.exec_query(
diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb
index 262e0abc22..14e392ac30 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -9,11 +9,11 @@ class SanitizeTest < ActiveRecord::TestCase
def test_sanitize_sql_array_handles_string_interpolation
quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi")
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"])
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi".mb_chars])
+ assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi"])
+ assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi".mb_chars])
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper")
- assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper"])
- assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper".mb_chars])
+ assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper"])
+ assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper".mb_chars])
end
def test_sanitize_sql_array_handles_bind_variables
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index d0deb4c273..a93fa57257 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -270,15 +270,16 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
end
- def test_warn_when_external_structure_dump_fails
+ def test_warn_when_external_structure_dump_command_execution_fails
filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db").returns(false)
+ Kernel.expects(:system)
+ .with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db")
+ .returns(false)
- warnings = capture(:stderr) do
+ e = assert_raise(RuntimeError) {
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- end
-
- assert_match(/Could not dump the database structure/, warnings)
+ }
+ assert_match(/^failed to execute: `mysqldump`$/, e.message)
end
def test_structure_dump_with_port_number
@@ -311,6 +312,7 @@ module ActiveRecord
def test_structure_load
filename = "awesome-file.sql"
Kernel.expects(:system).with('mysql', '--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db")
+ .returns(true)
ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
end
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index 7761ea5612..47e664f4e7 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -103,7 +103,7 @@ module ActiveRecord
# ignored SQL, or better yet, use a different notification for the queries
# instead examining the SQL content.
oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
- mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /]
+ mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im]
postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im]
diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb
index 3f4baf8378..1970fe82d0 100644
--- a/activerecord/test/cases/test_fixtures_test.rb
+++ b/activerecord/test/cases/test_fixtures_test.rb
@@ -28,7 +28,7 @@ class TestFixturesTest < ActiveRecord::TestCase
assert_equal true, @klass.use_transactional_tests
end
- def test_use_transactional_tests_can_be_overriden
+ def test_use_transactional_tests_can_be_overridden
@klass.use_transactional_tests = "foobar"
assert_equal "foobar", @klass.use_transactional_tests
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index 5dab32995c..970f6bcf4a 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -84,7 +84,9 @@ class TimestampTest < ActiveRecord::TestCase
def test_touching_an_attribute_updates_timestamp
previously_created_at = @developer.created_at
- @developer.touch(:created_at)
+ travel(1.second) do
+ @developer.touch(:created_at)
+ end
assert !@developer.created_at_changed? , 'created_at should not be changed'
assert !@developer.changed?, 'record should not be changed'
@@ -199,8 +201,10 @@ class TimestampTest < ActiveRecord::TestCase
owner = pet.owner
previously_owner_updated_at = owner.updated_at
- pet.name = "Fluffy the Third"
- pet.save
+ travel(1.second) do
+ pet.name = "Fluffy the Third"
+ pet.save
+ end
assert_not_equal previously_owner_updated_at, pet.owner.updated_at
end
@@ -210,7 +214,9 @@ class TimestampTest < ActiveRecord::TestCase
owner = pet.owner
previously_owner_updated_at = owner.updated_at
- pet.destroy
+ travel(1.second) do
+ pet.destroy
+ end
assert_not_equal previously_owner_updated_at, pet.owner.updated_at
end
@@ -254,8 +260,10 @@ class TimestampTest < ActiveRecord::TestCase
owner.update_columns(happy_at: 3.days.ago)
previously_owner_updated_at = owner.updated_at
- pet.name = "I'm a parrot"
- pet.save
+ travel(1.second) do
+ pet.name = "I'm a parrot"
+ pet.save
+ end
assert_not_equal previously_owner_updated_at, pet.owner.updated_at
end
diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb
index b3c42c8e42..7058f4fbe2 100644
--- a/activerecord/test/cases/touch_later_test.rb
+++ b/activerecord/test/cases/touch_later_test.rb
@@ -72,7 +72,7 @@ class TouchLaterTest < ActiveRecord::TestCase
end
def test_touch_touches_immediately_with_a_custom_time
- time = Time.now.utc - 25.days
+ time = (Time.now.utc - 25.days).change(nsec: 0)
topic = Topic.create!(updated_at: time, created_at: time)
assert_equal time, topic.updated_at
assert_equal time, topic.created_at
diff --git a/activerecord/test/cases/type/date_time_test.rb b/activerecord/test/cases/type/date_time_test.rb
new file mode 100644
index 0000000000..bc4900e1c2
--- /dev/null
+++ b/activerecord/test/cases/type/date_time_test.rb
@@ -0,0 +1,14 @@
+require "cases/helper"
+require "models/task"
+
+module ActiveRecord
+ module Type
+ class IntegerTest < ActiveRecord::TestCase
+ def test_datetime_seconds_precision_applied_to_timestamp
+ skip "This test is invalid if subsecond precision isn't supported" unless subsecond_precision_supported?
+ p = Task.create!(starting: ::Time.now)
+ assert_equal p.starting.usec, p.reload.starting.usec
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb
index 0dcdbd0667..c0932d5357 100644
--- a/activerecord/test/cases/type/integer_test.rb
+++ b/activerecord/test/cases/type/integer_test.rb
@@ -4,112 +4,12 @@ require "models/company"
module ActiveRecord
module Type
class IntegerTest < ActiveRecord::TestCase
- test "simple values" do
- type = Type::Integer.new
- assert_equal 1, type.cast(1)
- assert_equal 1, type.cast('1')
- assert_equal 1, type.cast('1ignore')
- assert_equal 0, type.cast('bad1')
- assert_equal 0, type.cast('bad')
- assert_equal 1, type.cast(1.7)
- assert_equal 0, type.cast(false)
- assert_equal 1, type.cast(true)
- assert_nil type.cast(nil)
- end
-
- test "random objects cast to nil" do
- type = Type::Integer.new
- assert_nil type.cast([1,2])
- assert_nil type.cast({1 => 2})
- assert_nil type.cast(1..2)
- end
-
test "casting ActiveRecord models" do
type = Type::Integer.new
firm = Firm.create(:name => 'Apple')
assert_nil type.cast(firm)
end
- test "casting objects without to_i" do
- type = Type::Integer.new
- assert_nil type.cast(::Object.new)
- end
-
- test "casting nan and infinity" do
- type = Type::Integer.new
- assert_nil type.cast(::Float::NAN)
- assert_nil type.cast(1.0/0.0)
- end
-
- test "casting booleans for database" do
- type = Type::Integer.new
- assert_equal 1, type.serialize(true)
- assert_equal 0, type.serialize(false)
- end
-
- test "changed?" do
- type = Type::Integer.new
-
- assert type.changed?(5, 5, '5wibble')
- assert_not type.changed?(5, 5, '5')
- assert_not type.changed?(5, 5, '5.0')
- assert_not type.changed?(-5, -5, '-5')
- assert_not type.changed?(-5, -5, '-5.0')
- assert_not type.changed?(nil, nil, nil)
- end
-
- test "values below int min value are out of range" do
- assert_raises(::RangeError) do
- Integer.new.serialize(-2147483649)
- end
- end
-
- test "values above int max value are out of range" do
- assert_raises(::RangeError) do
- Integer.new.serialize(2147483648)
- end
- end
-
- test "very small numbers are out of range" do
- assert_raises(::RangeError) do
- Integer.new.serialize(-9999999999999999999999999999999)
- end
- end
-
- test "very large numbers are out of range" do
- assert_raises(::RangeError) do
- Integer.new.serialize(9999999999999999999999999999999)
- end
- end
-
- test "normal numbers are in range" do
- type = Integer.new
- assert_equal(0, type.serialize(0))
- assert_equal(-1, type.serialize(-1))
- assert_equal(1, type.serialize(1))
- end
-
- test "int max value is in range" do
- assert_equal(2147483647, Integer.new.serialize(2147483647))
- end
-
- test "int min value is in range" do
- assert_equal(-2147483648, Integer.new.serialize(-2147483648))
- end
-
- test "columns with a larger limit have larger ranges" do
- type = Integer.new(limit: 8)
-
- assert_equal(9223372036854775807, type.serialize(9223372036854775807))
- assert_equal(-9223372036854775808, type.serialize(-9223372036854775808))
- assert_raises(::RangeError) do
- type.serialize(-9999999999999999999999999999999)
- end
- assert_raises(::RangeError) do
- type.serialize(9999999999999999999999999999999)
- end
- end
-
test "values which are out of range can be re-assigned" do
klass = Class.new(ActiveRecord::Base) do
self.table_name = 'posts'
diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb
index 56e9bf434d..6fe6d46711 100644
--- a/activerecord/test/cases/type/string_test.rb
+++ b/activerecord/test/cases/type/string_test.rb
@@ -2,20 +2,6 @@ require 'cases/helper'
module ActiveRecord
class StringTypeTest < ActiveRecord::TestCase
- test "type casting" do
- type = Type::String.new
- assert_equal "t", type.cast(true)
- assert_equal "f", type.cast(false)
- assert_equal "123", type.cast(123)
- end
-
- test "values are duped coming out" do
- s = "foo"
- type = Type::String.new
- assert_not_same s, type.cast(s)
- assert_not_same s, type.deserialize(s)
- end
-
test "string mutations are detected" do
klass = Class.new(Base)
klass.table_name = 'authors'
diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb
index 9b1859c2ce..81fcf04a27 100644
--- a/activerecord/test/cases/types_test.rb
+++ b/activerecord/test/cases/types_test.rb
@@ -3,111 +3,6 @@ require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class TypesTest < ActiveRecord::TestCase
- def test_type_cast_boolean
- type = Type::Boolean.new
- assert type.cast('').nil?
- assert type.cast(nil).nil?
-
- assert type.cast(true)
- assert type.cast(1)
- assert type.cast('1')
- assert type.cast('t')
- assert type.cast('T')
- assert type.cast('true')
- assert type.cast('TRUE')
- assert type.cast('on')
- assert type.cast('ON')
- assert type.cast(' ')
- assert type.cast("\u3000\r\n")
- assert type.cast("\u0000")
- assert type.cast('SOMETHING RANDOM')
-
- # explicitly check for false vs nil
- assert_equal false, type.cast(false)
- assert_equal false, type.cast(0)
- assert_equal false, type.cast('0')
- assert_equal false, type.cast('f')
- assert_equal false, type.cast('F')
- assert_equal false, type.cast('false')
- assert_equal false, type.cast('FALSE')
- assert_equal false, type.cast('off')
- assert_equal false, type.cast('OFF')
- end
-
- def test_type_cast_float
- type = Type::Float.new
- assert_equal 1.0, type.cast("1")
- end
-
- def test_changing_float
- type = Type::Float.new
-
- assert type.changed?(5.0, 5.0, '5wibble')
- assert_not type.changed?(5.0, 5.0, '5')
- assert_not type.changed?(5.0, 5.0, '5.0')
- assert_not type.changed?(nil, nil, nil)
- end
-
- def test_type_cast_binary
- type = Type::Binary.new
- assert_equal nil, type.cast(nil)
- assert_equal "1", type.cast("1")
- assert_equal 1, type.cast(1)
- end
-
- def test_type_cast_time
- type = Type::Time.new
- assert_equal nil, type.cast(nil)
- assert_equal nil, type.cast('')
- assert_equal nil, type.cast('ABC')
-
- time_string = Time.now.utc.strftime("%T")
- assert_equal time_string, type.cast(time_string).strftime("%T")
- end
-
- def test_type_cast_datetime_and_timestamp
- type = Type::DateTime.new
- assert_equal nil, type.cast(nil)
- assert_equal nil, type.cast('')
- assert_equal nil, type.cast(' ')
- assert_equal nil, type.cast('ABC')
-
- datetime_string = Time.now.utc.strftime("%FT%T")
- assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T")
- end
-
- def test_type_cast_date
- type = Type::Date.new
- assert_equal nil, type.cast(nil)
- assert_equal nil, type.cast('')
- assert_equal nil, type.cast(' ')
- assert_equal nil, type.cast('ABC')
-
- date_string = Time.now.utc.strftime("%F")
- assert_equal date_string, type.cast(date_string).strftime("%F")
- end
-
- def test_type_cast_duration_to_integer
- type = Type::Integer.new
- assert_equal 1800, type.cast(30.minutes)
- assert_equal 7200, type.cast(2.hours)
- end
-
- def test_string_to_time_with_timezone
- [:utc, :local].each do |zone|
- with_timezone_config default: zone do
- type = Type::DateTime.new
- assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT")
- end
- end
- end
-
- def test_type_equality
- assert_equal Type::Value.new, Type::Value.new
- assert_not_equal Type::Value.new, Type::Integer.new
- assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2)
- end
-
def test_attributes_which_are_invalid_for_database_can_still_be_reassigned
type_which_cannot_go_to_the_database = Type::Value.new
def type_which_cannot_go_to_the_database.serialize(*)
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index a429d06aad..d04f4f7ce7 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -168,4 +168,15 @@ class ValidationsTest < ActiveRecord::TestCase
ensure
Topic.reset_column_information
end
+
+ def test_acceptance_validator_doesnt_require_db_connection
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'posts'
+ end
+ klass.reset_column_information
+
+ assert_no_queries do
+ klass.validates_acceptance_of(:foo)
+ end
+ end
end
diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb
index 1eb1430065..e80d8bd584 100644
--- a/activerecord/test/cases/view_test.rb
+++ b/activerecord/test/cases/view_test.rb
@@ -1,7 +1,9 @@
require "cases/helper"
require "models/book"
+require "support/schema_dumping_helper"
module ViewBehavior
+ include SchemaDumpingHelper
extend ActiveSupport::Concern
included do
@@ -31,11 +33,26 @@ module ViewBehavior
assert_equal ["Ruby for Rails"], books.map(&:name)
end
+ def test_views
+ assert_equal [Ebook.table_name], @connection.views
+ end
+
+ def test_view_exists
+ view_name = Ebook.table_name
+ assert @connection.view_exists?(view_name), "'#{view_name}' view should exist"
+ end
+
def test_table_exists
view_name = Ebook.table_name
+ # TODO: switch this assertion around once we changed #tables to not return views.
assert @connection.table_exists?(view_name), "'#{view_name}' table should exist"
end
+ def test_views_ara_valid_data_sources
+ view_name = Ebook.table_name
+ assert @connection.data_source_exists?(view_name), "'#{view_name}' should be a data source"
+ end
+
def test_column_definitions
assert_equal([["id", :integer],
["name", :string],
@@ -53,6 +70,11 @@ module ViewBehavior
end
assert_nil model.primary_key
end
+
+ def test_does_not_dump_view_as_table
+ schema = dump_table_schema "ebooks"
+ assert_no_match %r{create_table "ebooks"}, schema
+ end
end
if ActiveRecord::Base.connection.supports_views?
@@ -70,6 +92,7 @@ class ViewWithPrimaryKeyTest < ActiveRecord::TestCase
end
class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
fixtures :books
class Paperback < ActiveRecord::Base; end
@@ -91,6 +114,15 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
assert_equal ["Agile Web Development with Rails"], books.map(&:name)
end
+ def test_views
+ assert_equal [Paperback.table_name], @connection.views
+ end
+
+ def test_view_exists
+ view_name = Paperback.table_name
+ assert @connection.view_exists?(view_name), "'#{view_name}' view should exist"
+ end
+
def test_table_exists
view_name = Paperback.table_name
assert @connection.table_exists?(view_name), "'#{view_name}' table should exist"
@@ -109,6 +141,11 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
def test_does_not_have_a_primary_key
assert_nil Paperback.primary_key
end
+
+ def test_does_not_dump_view_as_table
+ schema = dump_table_schema "paperbacks"
+ assert_no_match %r{create_table "paperbacks"}, schema
+ end
end
# sqlite dose not support CREATE, INSERT, and DELETE for VIEW
diff --git a/activerecord/test/fixtures/bad_posts.yml b/activerecord/test/fixtures/bad_posts.yml
new file mode 100644
index 0000000000..addee8e3bf
--- /dev/null
+++ b/activerecord/test/fixtures/bad_posts.yml
@@ -0,0 +1,9 @@
+# Please do not use this fixture without `set_fixture_class` as Post
+
+_fixture:
+ model_class: BadPostModel
+
+bad_welcome:
+ author_id: 1
+ title: Welcome to the another weblog
+ body: It's really nice today
diff --git a/activerecord/test/fixtures/other_comments.yml b/activerecord/test/fixtures/other_comments.yml
new file mode 100644
index 0000000000..55e8216ec7
--- /dev/null
+++ b/activerecord/test/fixtures/other_comments.yml
@@ -0,0 +1,6 @@
+_fixture:
+ model_class: Comment
+
+second_greetings:
+ post: second_welcome
+ body: Thank you for the second welcome
diff --git a/activerecord/test/fixtures/other_posts.yml b/activerecord/test/fixtures/other_posts.yml
new file mode 100644
index 0000000000..39ff763547
--- /dev/null
+++ b/activerecord/test/fixtures/other_posts.yml
@@ -0,0 +1,7 @@
+_fixture:
+ model_class: Post
+
+second_welcome:
+ author_id: 1
+ title: Welcome to the another weblog
+ body: It's really nice today
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 8c1f14bd36..0d90cbb110 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -144,9 +144,6 @@ class Author < ActiveRecord::Base
has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post"
- scope :relation_include_posts, -> { includes(:posts) }
- scope :relation_include_tags, -> { includes(:tags) }
-
attr_accessor :post_log
after_initialize :set_post_log
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 67936e8e5d..a96b8ef0f2 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -86,6 +86,9 @@ class Firm < Company
has_many :association_with_references, -> { references(:foo) }, :class_name => 'Client'
+ has_one :lead_developer, class_name: "Developer"
+ has_many :projects
+
def log
@log ||= []
end
diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb
index 3ea17c3abf..9f2f69e1ee 100644
--- a/activerecord/test/models/contact.rb
+++ b/activerecord/test/models/contact.rb
@@ -3,7 +3,7 @@ module ContactFakeColumns
base.class_eval do
establish_connection(:adapter => 'fake')
- connection.tables = [table_name]
+ connection.data_sources = [table_name]
connection.primary_keys = {
table_name => 'id'
}
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 8ac7a9df6a..7c5941b1af 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -7,6 +7,8 @@ module DeveloperProjectsAssociationExtension2
end
class Developer < ActiveRecord::Base
+ self.ignored_columns = %w(first_name last_name)
+
has_and_belongs_to_many :projects do
def find_most_recent
order("id DESC").first
@@ -52,6 +54,9 @@ class Developer < ActiveRecord::Base
has_many :ratings, through: :comments
has_one :ship, dependent: :nullify
+ belongs_to :firm
+ has_many :contracted_projects, class_name: "Project"
+
scope :jamises, -> { where(:name => 'Jamis') }
validates_inclusion_of :salary, :in => 50000..200000
@@ -61,6 +66,9 @@ class Developer < ActiveRecord::Base
developer.audit_logs.build :message => "Computer created"
end
+ attr_accessor :last_name
+ define_attribute_method 'last_name'
+
def log=(message)
audit_logs.build :message => message
end
diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb
index b26035d944..ddc9dcaf29 100644
--- a/activerecord/test/models/parrot.rb
+++ b/activerecord/test/models/parrot.rb
@@ -21,9 +21,3 @@ end
class DeadParrot < Parrot
belongs_to :killer, :class_name => 'Pirate', foreign_key: :killer_id
end
-
-class FunkyParrot < Parrot
- before_destroy do
- raise "before_destroy was called"
- end
-end
diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb
index ad12f00d42..a4a9c6b0d4 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -37,7 +37,6 @@ class Person < ActiveRecord::Base
has_many :essays, primary_key: "first_name", foreign_key: "writer_id"
scope :males, -> { where(:gender => 'M') }
- scope :females, -> { where(:gender => 'F') }
end
class PersonWithDependentDestroyJobs < ActiveRecord::Base
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index 7f42a4b1f8..5328330653 100644
--- a/activerecord/test/models/project.rb
+++ b/activerecord/test/models/project.rb
@@ -11,6 +11,8 @@ class Project < ActiveRecord::Base
:before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"},
:after_remove => Proc.new {|o, r| o.developers_log << "after_removing#{r.id}"}
has_and_belongs_to_many :well_payed_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, :class_name => "Developer"
+ belongs_to :firm
+ has_one :lead_developer, through: :firm, inverse_of: :contracted_projects
attr_accessor :developers_log
after_initialize :set_developers_log
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 994ece9244..d334a2740e 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -1,4 +1,3 @@
-
ActiveRecord::Schema.define do
def except(adapter_names_to_exclude)
unless [adapter_names_to_exclude].flatten.include?(adapter_name)
@@ -251,11 +250,20 @@ ActiveRecord::Schema.define do
create_table :developers, force: true do |t|
t.string :name
+ t.string :first_name
t.integer :salary, default: 70000
- t.datetime :created_at
- t.datetime :updated_at
- t.datetime :created_on
- t.datetime :updated_on
+ t.integer :firm_id
+ if subsecond_precision_supported?
+ t.datetime :created_at, precision: 6
+ t.datetime :updated_at, precision: 6
+ t.datetime :created_on, precision: 6
+ t.datetime :updated_on, precision: 6
+ else
+ t.datetime :created_at
+ t.datetime :updated_at
+ t.datetime :created_on
+ t.datetime :updated_on
+ end
end
create_table :developers_projects, force: true, id: false do |t|
@@ -354,7 +362,11 @@ ActiveRecord::Schema.define do
create_table :invoices, force: true do |t|
t.integer :balance
- t.datetime :updated_at
+ if subsecond_precision_supported?
+ t.datetime :updated_at, precision: 6
+ else
+ t.datetime :updated_at
+ end
end
create_table :iris, force: true do |t|
@@ -504,7 +516,11 @@ ActiveRecord::Schema.define do
create_table :owners, primary_key: :owner_id, force: true do |t|
t.string :name
- t.column :updated_at, :datetime
+ if subsecond_precision_supported?
+ t.column :updated_at, :datetime, precision: 6
+ else
+ t.column :updated_at, :datetime
+ end
t.column :happy_at, :datetime
t.string :essay_id
end
@@ -522,10 +538,17 @@ ActiveRecord::Schema.define do
t.column :color, :string
t.column :parrot_sti_class, :string
t.column :killer_id, :integer
- t.column :created_at, :datetime
- t.column :created_on, :datetime
- t.column :updated_at, :datetime
- t.column :updated_on, :datetime
+ if subsecond_precision_supported?
+ t.column :created_at, :datetime, precision: 0
+ t.column :created_on, :datetime, precision: 0
+ t.column :updated_at, :datetime, precision: 0
+ t.column :updated_on, :datetime, precision: 0
+ else
+ t.column :created_at, :datetime
+ t.column :created_on, :datetime
+ t.column :updated_at, :datetime
+ t.column :updated_on, :datetime
+ end
end
create_table :parrots_pirates, id: false, force: true do |t|
@@ -568,15 +591,24 @@ ActiveRecord::Schema.define do
create_table :pets, primary_key: :pet_id, force: true do |t|
t.string :name
t.integer :owner_id, :integer
- t.timestamps null: false
+ if subsecond_precision_supported?
+ t.timestamps null: false, precision: 6
+ else
+ t.timestamps null: false
+ end
end
create_table :pirates, force: true do |t|
t.column :catchphrase, :string
t.column :parrot_id, :integer
t.integer :non_validated_parrot_id
- t.column :created_on, :datetime
- t.column :updated_on, :datetime
+ if subsecond_precision_supported?
+ t.column :created_on, :datetime, precision: 6
+ t.column :updated_on, :datetime, precision: 6
+ else
+ t.column :created_on, :datetime
+ t.column :updated_on, :datetime
+ end
end
create_table :posts, force: true do |t|
@@ -627,6 +659,7 @@ ActiveRecord::Schema.define do
create_table :projects, force: true do |t|
t.string :name
t.string :type
+ t.integer :firm_id
end
create_table :randomly_named_table1, force: true do |t|
@@ -686,7 +719,11 @@ ActiveRecord::Schema.define do
create_table :ship_parts, force: true do |t|
t.string :name
t.integer :ship_id
- t.datetime :updated_at
+ if subsecond_precision_supported?
+ t.datetime :updated_at, precision: 6
+ else
+ t.datetime :updated_at
+ end
end
create_table :prisoners, force: true do |t|
@@ -756,7 +793,7 @@ ActiveRecord::Schema.define do
t.string :title, limit: 250
t.string :author_name
t.string :author_email_address
- if mysql_56?
+ if subsecond_precision_supported?
t.datetime :written_on, precision: 6
else
t.datetime :written_on
@@ -779,7 +816,11 @@ ActiveRecord::Schema.define do
t.string :parent_title
t.string :type
t.string :group
- t.timestamps null: true
+ if subsecond_precision_supported?
+ t.timestamps null: true, precision: 6
+ else
+ t.timestamps null: true
+ end
end
create_table :toys, primary_key: :toy_id, force: true do |t|
@@ -944,6 +985,10 @@ ActiveRecord::Schema.define do
t.string :token
t.string :auth_token
end
+
+ create_table :test_with_keyword_column_name, force: true do |t|
+ t.string :desc
+ end
end
Course.connection.create_table :courses, force: true do |t|
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index addf5d521a..19588d622c 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,7 +1,29 @@
+* `assert_difference` and `assert_no_difference` now returns the result of the
+ yielded block.
+
+ Example:
+
+ post = assert_difference -> { Post.count }, 1 do
+ Post.create
+ end
+
+ *Lucas Mazza*
+
+* Short-circuit `blank?` on date and time values since they are never blank.
+
+ Fixes #21657
+
+ *Andrew White*
+
+* Replaced deprecated `ThreadSafe::Cache` with its successor `Concurrent::Map` now that
+ the thread_safe gem has been merged into concurrent-ruby.
+
+ *Jerry D'Antonio*
+
* Updated Unicode version to 8.0.0
*Anshul Sharma*
-
+
* `number_to_currency` and `number_with_delimiter` now accept custom `delimiter_pattern` option
to handle placement of delimiter, to support currency formats like INR
@@ -266,30 +288,24 @@
The preferred method to halt a callback chain from now on is to explicitly
`throw(:abort)`.
- In the past, returning `false` in an ActiveSupport callback had the side
- effect of halting the callback chain. This is not recommended anymore and,
- depending on the value of
- `Callbacks::CallbackChain.halt_and_display_warning_on_return_false`, will
- either not work at all or display a deprecation warning.
+ In the past, callbacks could only be halted by explicitly providing a
+ terminator and by having a callback match the conditions of the terminator.
-* Add `Callbacks::CallbackChain.halt_and_display_warning_on_return_false`
+* Add `ActiveSupport.halt_callback_chains_on_return_false`
- Setting `Callbacks::CallbackChain.halt_and_display_warning_on_return_false`
- to `true` will let an app support the deprecated way of halting callback
- chains by returning `false`.
+ Setting `ActiveSupport.halt_callback_chains_on_return_false`
+ to `true` will let an app support the deprecated way of halting Active Record,
+ and Active Model callback chains by returning `false`.
Setting the value to `false` will tell the app to ignore any `false` value
- returned by callbacks, and only halt the chain upon `throw(:abort)`.
-
- The value can also be set with the Rails configuration option
- `config.active_support.halt_callback_chains_on_return_false`.
+ returned by those callbacks, and only halt the chain upon `throw(:abort)`.
When the configuration option is missing, its value is `true`, so older apps
ported to Rails 5.0 will not break (but display a deprecation warning).
For new Rails 5.0 apps, its value is set to `false` in an initializer, so
these apps will support the new behavior by default.
- *claudiob*
+ *claudiob*, *Roque Pinel*
* Changes arguments and default value of CallbackChain's `:terminator` option
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index 67b6663915..93878518d7 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -24,7 +24,6 @@ Gem::Specification.new do |s|
s.add_dependency 'json', '~> 1.7', '>= 1.7.7'
s.add_dependency 'tzinfo', '~> 1.1'
s.add_dependency 'minitest', '~> 5.1'
- s.add_dependency 'thread_safe','~> 0.3', '>= 0.3.4'
- s.add_dependency 'concurrent-ruby', '~> 0.9.1'
+ s.add_dependency 'concurrent-ruby', '~> 1.0.0.pre3', '< 2.0.0'
s.add_dependency 'method_source'
end
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index 588d6c49f9..63277a65b4 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -75,11 +75,11 @@ module ActiveSupport
cattr_accessor :test_order # :nodoc:
def self.halt_callback_chains_on_return_false
- Callbacks::CallbackChain.halt_and_display_warning_on_return_false
+ Callbacks.halt_and_display_warning_on_return_false
end
def self.halt_callback_chains_on_return_false=(value)
- Callbacks::CallbackChain.halt_and_display_warning_on_return_false = value
+ Callbacks.halt_and_display_warning_on_return_false = value
end
end
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index 80c5fdba17..252374e817 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -4,6 +4,7 @@ require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/kernel/singleton_class'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/string/filters'
require 'active_support/deprecation'
require 'thread'
@@ -66,6 +67,12 @@ module ActiveSupport
CALLBACK_FILTER_TYPES = [:before, :after, :around]
+ # If true, Active Record and Active Model callbacks returning +false+ will
+ # halt the entire callback chain and display a deprecation message.
+ # If false, callback chains will only be halted by calling +throw :abort+.
+ # Defaults to +true+.
+ mattr_accessor(:halt_and_display_warning_on_return_false) { true }
+
# Runs the callbacks for the given event.
#
# Calls the before and around callbacks in the order they were set, yields
@@ -450,12 +457,6 @@ module ActiveSupport
attr_reader :name, :config
- # If true, any callback returning +false+ will halt the entire callback
- # chain and display a deprecation message. If false, callback chains will
- # only be halted by calling +throw :abort+. Defaults to +true+.
- class_attribute :halt_and_display_warning_on_return_false
- self.halt_and_display_warning_on_return_false = true
-
def initialize(name, config)
@name = name
@config = {
@@ -536,23 +537,12 @@ module ActiveSupport
Proc.new do |target, result_lambda|
terminate = true
catch(:abort) do
- result = result_lambda.call if result_lambda.is_a?(Proc)
- if halt_and_display_warning_on_return_false && result == false
- display_deprecation_warning_for_false_terminator
- else
- terminate = false
- end
+ result_lambda.call if result_lambda.is_a?(Proc)
+ terminate = false
end
terminate
end
end
-
- def display_deprecation_warning_for_false_terminator
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Returning `false` in a callback will not implicitly halt a callback chain in the next release of Rails.
- To explicitly halt a callback chain, please use `throw :abort` instead.
- MSG
- end
end
module ClassMethods
@@ -686,7 +676,8 @@ module ActiveSupport
#
# In this example, if any before validate callbacks returns +false+,
# any successive before and around callback is not executed.
- # Defaults to +false+, meaning no value halts the chain.
+ #
+ # The default terminator halts the chain when a callback throws +:abort+.
#
# * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after
# callbacks should be terminated by the <tt>:terminator</tt> option. By
@@ -764,6 +755,30 @@ module ActiveSupport
def set_callbacks(name, callbacks)
send "_#{name}_callbacks=", callbacks
end
+
+ def deprecated_false_terminator
+ Proc.new do |target, result_lambda|
+ terminate = true
+ catch(:abort) do
+ result = result_lambda.call if result_lambda.is_a?(Proc)
+ if Callbacks.halt_and_display_warning_on_return_false && result == false
+ display_deprecation_warning_for_false_terminator
+ else
+ terminate = false
+ end
+ end
+ terminate
+ end
+ end
+
+ private
+
+ def display_deprecation_warning_for_false_terminator
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in the next release of Rails.
+ To explicitly halt the callback chain, please use `throw :abort` instead.
+ MSG
+ end
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb
index c5cc31abc5..8718b7e1e5 100644
--- a/activesupport/lib/active_support/core_ext/array/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/array/conversions.rb
@@ -85,7 +85,9 @@ class Array
# Extends <tt>Array#to_s</tt> to convert a collection of elements into a
# comma separated id list if <tt>:db</tt> argument is given as the format.
#
- # Blog.all.to_formatted_s(:db) # => "1,2,3"
+ # Blog.all.to_formatted_s(:db) # => "1,2,3"
+ # Blog.none.to_formatted_s(:db) # => "null"
+ # [1,2].to_formatted_s # => "[1, 2]"
def to_formatted_s(format = :default)
case format
when :db
diff --git a/activesupport/lib/active_support/core_ext/date.rb b/activesupport/lib/active_support/core_ext/date.rb
index 465fedda80..7f0f4639a2 100644
--- a/activesupport/lib/active_support/core_ext/date.rb
+++ b/activesupport/lib/active_support/core_ext/date.rb
@@ -1,5 +1,5 @@
require 'active_support/core_ext/date/acts_like'
+require 'active_support/core_ext/date/blank'
require 'active_support/core_ext/date/calculations'
require 'active_support/core_ext/date/conversions'
require 'active_support/core_ext/date/zones'
-
diff --git a/activesupport/lib/active_support/core_ext/date/blank.rb b/activesupport/lib/active_support/core_ext/date/blank.rb
new file mode 100644
index 0000000000..71627b6a6f
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/date/blank.rb
@@ -0,0 +1,12 @@
+require 'date'
+
+class Date #:nodoc:
+ # No Date is blank:
+ #
+ # Date.today.blank? # => false
+ #
+ # @return [false]
+ def blank?
+ false
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb
index c60e833441..d589b67bf7 100644
--- a/activesupport/lib/active_support/core_ext/date/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date/calculations.rb
@@ -26,7 +26,7 @@ class Date
Thread.current[:beginning_of_week] = find_beginning_of_week!(week_start)
end
- # Returns week start day symbol (e.g. :monday), or raises an ArgumentError for invalid day symbol.
+ # Returns week start day symbol (e.g. :monday), or raises an +ArgumentError+ for invalid day symbol.
def find_beginning_of_week!(week_start)
raise ArgumentError, "Invalid beginning of week: #{week_start}" unless ::Date::DAYS_INTO_WEEK.key?(week_start)
week_start
diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb
index 31479a1269..ed8bca77ac 100644
--- a/activesupport/lib/active_support/core_ext/date/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date/conversions.rb
@@ -75,10 +75,10 @@ class Date
#
# date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007
#
- # date.to_time # => Sat Nov 10 00:00:00 0800 2007
- # date.to_time(:local) # => Sat Nov 10 00:00:00 0800 2007
+ # date.to_time # => 2007-11-10 00:00:00 0800
+ # date.to_time(:local) # => 2007-11-10 00:00:00 0800
#
- # date.to_time(:utc) # => Sat Nov 10 00:00:00 UTC 2007
+ # date.to_time(:utc) # => 2007-11-10 00:00:00 UTC
def to_time(form = :local)
::Time.send(form, year, month, day)
end
diff --git a/activesupport/lib/active_support/core_ext/date_time.rb b/activesupport/lib/active_support/core_ext/date_time.rb
index e8a27b9f38..bcb228b09a 100644
--- a/activesupport/lib/active_support/core_ext/date_time.rb
+++ b/activesupport/lib/active_support/core_ext/date_time.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/date_time/acts_like'
+require 'active_support/core_ext/date_time/blank'
require 'active_support/core_ext/date_time/calculations'
require 'active_support/core_ext/date_time/conversions'
require 'active_support/core_ext/date_time/zones'
diff --git a/activesupport/lib/active_support/core_ext/date_time/blank.rb b/activesupport/lib/active_support/core_ext/date_time/blank.rb
new file mode 100644
index 0000000000..56981b75fb
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/date_time/blank.rb
@@ -0,0 +1,12 @@
+require 'date'
+
+class DateTime #:nodoc:
+ # No DateTime is ever blank:
+ #
+ # DateTime.now.blank? # => false
+ #
+ # @return [false]
+ def blank?
+ false
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb
index 2a9c09fc29..f59d05b214 100644
--- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb
@@ -40,6 +40,8 @@ class DateTime
alias_method :to_default_s, :to_s if instance_methods(false).include?(:to_s)
alias_method :to_s, :to_formatted_s
+ # Returns a formatted string of the offset from UTC, or an alternative
+ # string if the time zone is already UTC.
#
# datetime = DateTime.civil(2000, 1, 1, 0, 0, 0, Rational(-6, 24))
# datetime.formatted_offset # => "-06:00"
diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb
index c30044b9ff..07a282e8b6 100644
--- a/activesupport/lib/active_support/core_ext/hash/keys.rb
+++ b/activesupport/lib/active_support/core_ext/hash/keys.rb
@@ -1,10 +1,14 @@
class Hash
- # Returns a new hash with all keys converted using the block operation.
+ # Returns a new hash with all keys converted using the +block+ operation.
#
# hash = { name: 'Rob', age: '28' }
#
- # hash.transform_keys{ |key| key.to_s.upcase }
- # # => {"NAME"=>"Rob", "AGE"=>"28"}
+ # hash.transform_keys { |key| key.to_s.upcase } # => {"NAME"=>"Rob", "AGE"=>"28"}
+ #
+ # If you do not provide a +block+, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # hash.transform_keys.with_index { |k, i| [k, i].join } # => {"name0"=>"Rob", "age1"=>"28"}
def transform_keys
return enum_for(:transform_keys) unless block_given?
result = self.class.new
@@ -14,8 +18,8 @@ class Hash
result
end
- # Destructively converts all keys using the block operations.
- # Same as transform_keys but modifies +self+.
+ # Destructively converts all keys using the +block+ operations.
+ # Same as +transform_keys+ but modifies +self+.
def transform_keys!
return enum_for(:transform_keys!) unless block_given?
keys.each do |key|
@@ -60,7 +64,7 @@ class Hash
alias_method :to_options!, :symbolize_keys!
# Validates all keys in a hash match <tt>*valid_keys</tt>, raising
- # ArgumentError on a mismatch.
+ # +ArgumentError+ on a mismatch.
#
# Note that keys are treated differently than HashWithIndifferentAccess,
# meaning that string and symbol keys will not match.
diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
index e9bcce761f..9ddb838774 100644
--- a/activesupport/lib/active_support/core_ext/hash/transform_values.rb
+++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
@@ -2,10 +2,15 @@ class Hash
# Returns a new hash with the results of running +block+ once for every value.
# The keys are unchanged.
#
- # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 }
- # # => { a: 2, b: 4, c: 6 }
+ # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } # => { a: 2, b: 4, c: 6 }
+ #
+ # If you do not provide a +block+, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # { a: 1, b: 2 }.transform_values.with_index { |v, i| [v, i].join.to_i } # => { a: 10, b: 21 }
def transform_values
return enum_for(:transform_values) unless block_given?
+ return {} if empty?
result = self.class.new
each do |key, value|
result[key] = yield(value)
@@ -13,7 +18,8 @@ class Hash
result
end
- # Destructive +transform_values+
+ # Destructively converts all values using the +block+ operations.
+ # Same as +transform_values+ but modifies +self+.
def transform_values!
return enum_for(:transform_values!) unless block_given?
each do |key, value|
diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb
index 20a0856e71..e333b26133 100644
--- a/activesupport/lib/active_support/core_ext/marshal.rb
+++ b/activesupport/lib/active_support/core_ext/marshal.rb
@@ -6,7 +6,7 @@ module ActiveSupport
if exc.message.match(%r|undefined class/module (.+)|)
# try loading the class/module
$1.constantize
- # if it is a IO we need to go back to read the object
+ # if it is an IO we need to go back to read the object
source.rewind if source.respond_to?(:rewind)
retry
else
diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
index a77da573fe..bf175a8a70 100644
--- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
@@ -19,9 +19,9 @@ class Module
# The attribute name must be a valid method name in Ruby.
#
# module Foo
- # mattr_reader :"1_Badname "
+ # mattr_reader :"1_Badname"
# end
- # # => NameError: invalid attribute name
+ # # => NameError: invalid attribute name: 1_Badname
#
# If you want to opt out the creation on the instance reader method, pass
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
@@ -53,7 +53,7 @@ class Module
def mattr_reader(*syms)
options = syms.extract_options!
syms.each do |sym|
- raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
+ raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /\A[_A-Za-z]\w*\z/
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@#{sym} = nil unless defined? @@#{sym}
@@ -119,7 +119,7 @@ class Module
def mattr_writer(*syms)
options = syms.extract_options!
syms.each do |sym|
- raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
+ raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /\A[_A-Za-z]\w*\z/
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@#{sym} = nil unless defined? @@#{sym}
diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
index 0c8ff79237..d5bb11deed 100644
--- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
@@ -41,7 +41,7 @@ class Numeric
# 1000.to_s(:percentage, delimiter: '.', separator: ',') # => 1.000,000%
# 302.24398923423.to_s(:percentage, precision: 5) # => 302.24399%
# 1000.to_s(:percentage, locale: :fr) # => 1 000,000%
- # 100.to_s(:percentage, format: '%n %') # => 100 %
+ # 100.to_s(:percentage, format: '%n %') # => 100.000 %
#
# Delimited:
# 12345678.to_s(:delimited) # => 12,345,678
@@ -78,7 +78,7 @@ class Numeric
# 1234567.to_s(:human_size, precision: 2) # => 1.2 MB
# 483989.to_s(:human_size, precision: 2) # => 470 KB
# 1234567.to_s(:human_size, precision: 2, separator: ',') # => 1,2 MB
- # 1234567890123.to_s(:human_size, precision: 5) # => "1.1229 TB"
+ # 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB"
# 524288000.to_s(:human_size, precision: 5) # => "500 MB"
#
# Human-friendly format:
diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb
index a294228979..039c50a4a2 100644
--- a/activesupport/lib/active_support/core_ext/object/blank.rb
+++ b/activesupport/lib/active_support/core_ext/object/blank.rb
@@ -127,3 +127,14 @@ class Numeric #:nodoc:
false
end
end
+
+class Time #:nodoc:
+ # No Time is blank:
+ #
+ # Time.now.blank? # => false
+ #
+ # @return [false]
+ def blank?
+ false
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb
index 55f281b213..d4c17dfb07 100644
--- a/activesupport/lib/active_support/core_ext/object/inclusion.rb
+++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb
@@ -5,7 +5,7 @@ class Object
# characters = ["Konata", "Kagami", "Tsukasa"]
# "Konata".in?(characters) # => true
#
- # This will throw an ArgumentError if the argument doesn't respond
+ # This will throw an +ArgumentError+ if the argument doesn't respond
# to +#include?+.
def in?(another_object)
another_object.include?(self)
@@ -18,7 +18,7 @@ class Object
#
# params[:bucket_type].presence_in %w( project calendar )
#
- # This will throw an ArgumentError if the argument doesn't respond to +#include?+.
+ # This will throw an +ArgumentError+ if the argument doesn't respond to +#include?+.
#
# @return [Object]
def presence_in(another_object)
diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb
index c67eb25b68..8c16d95b62 100644
--- a/activesupport/lib/active_support/core_ext/object/try.rb
+++ b/activesupport/lib/active_support/core_ext/object/try.rb
@@ -94,7 +94,7 @@ class Object
# :call-seq:
# try!(*a, &b)
#
- # Same as #try, but raises a NoMethodError exception if the receiver is
+ # Same as #try, but raises a +NoMethodError+ exception if the receiver is
# not +nil+ and does not implement the tried method.
#
# "a".try!(:upcase) # => "A"
diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb
index 6cdbea1f37..98cf7430f7 100644
--- a/activesupport/lib/active_support/core_ext/securerandom.rb
+++ b/activesupport/lib/active_support/core_ext/securerandom.rb
@@ -10,8 +10,8 @@ module SecureRandom
#
# The result may contain alphanumeric characters except 0, O, I and l
#
- # p SecureRandom.base58 #=> "4kUgL2pdQMSCQtjE"
- # p SecureRandom.base58(24) #=> "77TMHrHJFvFDwodq8w7Ev2m7"
+ # p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE"
+ # p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7"
#
def self.base58(n = 16)
SecureRandom.random_bytes(n).unpack("C*").map do |byte|
diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb
index 7055f7f699..cc6f2158e7 100644
--- a/activesupport/lib/active_support/core_ext/string/multibyte.rb
+++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb
@@ -9,12 +9,10 @@ class String
# encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy
# class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string.
#
- # name = 'Claus Müller'
- # name.reverse # => "rell??M sualC"
- # name.length # => 13
- #
- # name.mb_chars.reverse.to_s # => "rellüM sualC"
- # name.mb_chars.length # => 12
+ # >> "lj".upcase
+ # => "lj"
+ # >> "lj".mb_chars.upcase.to_s
+ # => "LJ"
#
# == Method chaining
#
diff --git a/activesupport/lib/active_support/core_ext/string/strip.rb b/activesupport/lib/active_support/core_ext/string/strip.rb
index a523e76b69..55b9b87352 100644
--- a/activesupport/lib/active_support/core_ext/string/strip.rb
+++ b/activesupport/lib/active_support/core_ext/string/strip.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/object/try'
-
class String
# Strips indentation in heredocs.
#
diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb
index dbf1f2f373..24d13937f6 100644
--- a/activesupport/lib/active_support/core_ext/time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/time/conversions.rb
@@ -55,7 +55,8 @@ class Time
alias_method :to_default_s, :to_s
alias_method :to_s, :to_formatted_s
- # Returns the UTC offset as an +HH:MM formatted string.
+ # Returns a formatted string of the offset from UTC, or an alternative
+ # string if the time zone is already UTC.
#
# Time.local(2000).formatted_offset # => "-06:00"
# Time.local(2000).formatted_offset(false) # => "-0600"
diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb
index 133d3938eb..877dc84ec8 100644
--- a/activesupport/lib/active_support/core_ext/time/zones.rb
+++ b/activesupport/lib/active_support/core_ext/time/zones.rb
@@ -53,7 +53,7 @@ class Time
# Returns a TimeZone instance matching the time zone provided.
# Accepts the time zone in any format supported by <tt>Time.zone=</tt>.
- # Raises an ArgumentError for invalid time zones.
+ # Raises an +ArgumentError+ for invalid time zones.
#
# Time.find_zone! "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...>
# Time.find_zone! "EST" # => #<ActiveSupport::TimeZone @name="EST" ...>
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index 8215a3085e..16b726bcba 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -1,6 +1,6 @@
require 'set'
require 'thread'
-require 'thread_safe'
+require 'concurrent'
require 'pathname'
require 'active_support/core_ext/module/aliasing'
require 'active_support/core_ext/module/attribute_accessors'
@@ -585,7 +585,7 @@ module ActiveSupport #:nodoc:
class ClassCache
def initialize
- @store = ThreadSafe::Cache.new
+ @store = Concurrent::Map.new
end
def empty?
diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb
index 42560f3515..c3907e9c22 100644
--- a/activesupport/lib/active_support/inflector/inflections.rb
+++ b/activesupport/lib/active_support/inflector/inflections.rb
@@ -1,4 +1,4 @@
-require 'thread_safe'
+require 'concurrent'
require 'active_support/core_ext/array/prepend_and_append'
require 'active_support/i18n'
@@ -25,7 +25,7 @@ module ActiveSupport
# singularization rules that is runs. This guarantees that your rules run
# before any of the rules that may already have been loaded.
class Inflections
- @__instance__ = ThreadSafe::Cache.new
+ @__instance__ = Concurrent::Map.new
class Uncountables < Array
def initialize
diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb
index aefcf73622..7472d4386a 100644
--- a/activesupport/lib/active_support/inflector/transliterate.rb
+++ b/activesupport/lib/active_support/inflector/transliterate.rb
@@ -75,15 +75,15 @@ module ActiveSupport
parameterized_string.gsub!(/[^a-z0-9\-_]+/i, sep)
unless sep.nil? || sep.empty?
if sep == "-".freeze
- re_duplicate_seperator = /-{2,}/
+ re_duplicate_separator = /-{2,}/
re_leading_trailing_separator = /^-|-$/i
else
re_sep = Regexp.escape(sep)
- re_duplicate_seperator = /#{re_sep}{2,}/
+ re_duplicate_separator = /#{re_sep}{2,}/
re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
end
# No more than one of the separator in a row.
- parameterized_string.gsub!(re_duplicate_seperator, sep)
+ parameterized_string.gsub!(re_duplicate_separator, sep)
# Remove leading/trailing separator.
parameterized_string.gsub!(re_leading_trailing_separator, ''.freeze)
end
diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb
index 51d2da3a79..6bc3db6ec6 100644
--- a/activesupport/lib/active_support/key_generator.rb
+++ b/activesupport/lib/active_support/key_generator.rb
@@ -1,4 +1,4 @@
-require 'thread_safe'
+require 'concurrent'
require 'openssl'
module ActiveSupport
@@ -28,7 +28,7 @@ module ActiveSupport
class CachingKeyGenerator
def initialize(key_generator)
@key_generator = key_generator
- @cache_keys = ThreadSafe::Cache.new
+ @cache_keys = Concurrent::Map.new
end
# Returns a derived key suitable for use. The default key_size is chosen
diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb
index 0131fe2572..71354dd15f 100644
--- a/activesupport/lib/active_support/notifications/fanout.rb
+++ b/activesupport/lib/active_support/notifications/fanout.rb
@@ -1,5 +1,5 @@
require 'mutex_m'
-require 'thread_safe'
+require 'concurrent'
module ActiveSupport
module Notifications
@@ -12,7 +12,7 @@ module ActiveSupport
def initialize
@subscribers = []
- @listeners_for = ThreadSafe::Cache.new
+ @listeners_for = Concurrent::Map.new
super
end
@@ -51,7 +51,7 @@ module ActiveSupport
end
def listeners_for(name)
- # this is correctly done double-checked locking (ThreadSafe::Cache's lookups have volatile semantics)
+ # this is correctly done double-checked locking (Concurrent::Map's lookups have volatile semantics)
@listeners_for[name] || synchronize do
# use synchronisation when accessing @subscribers
@listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) }
diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb
index 9762d95145..504f96961a 100644
--- a/activesupport/lib/active_support/number_helper.rb
+++ b/activesupport/lib/active_support/number_helper.rb
@@ -118,7 +118,7 @@ module ActiveSupport
# number_to_percentage(1000, locale: :fr) # => 1 000,000%
# number_to_percentage:(1000, precision: nil) # => 1000%
# number_to_percentage('98a') # => 98a%
- # number_to_percentage(100, format: '%n %') # => 100 %
+ # number_to_percentage(100, format: '%n %') # => 100.000 %
def number_to_percentage(number, options = {})
NumberToPercentageConverter.convert(number, options)
end
diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb
index d87ce3474d..ae8c15d8bf 100644
--- a/activesupport/lib/active_support/testing/assertions.rb
+++ b/activesupport/lib/active_support/testing/assertions.rb
@@ -68,13 +68,15 @@ module ActiveSupport
}
before = exps.map(&:call)
- yield
+ retval = yield
expressions.zip(exps).each_with_index do |(code, e), i|
error = "#{code.inspect} didn't change by #{difference}"
error = "#{message}.\n#{error}" if message
assert_equal(before[i] + difference, e.call, error)
end
+
+ retval
end
# Assertion that the numeric result of evaluating an expression is not
diff --git a/activesupport/lib/active_support/testing/file_fixtures.rb b/activesupport/lib/active_support/testing/file_fixtures.rb
index 4c6a0801b8..affb84cda5 100644
--- a/activesupport/lib/active_support/testing/file_fixtures.rb
+++ b/activesupport/lib/active_support/testing/file_fixtures.rb
@@ -18,7 +18,7 @@ module ActiveSupport
# Returns a +Pathname+ to the fixture file named +fixture_name+.
#
- # Raises ArgumentError if +fixture_name+ can't be found.
+ # Raises +ArgumentError+ if +fixture_name+ can't be found.
def file_fixture(fixture_name)
path = Pathname.new(File.join(file_fixture_path, fixture_name))
diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb
index 1de0a19998..edf8b30a0a 100644
--- a/activesupport/lib/active_support/testing/isolation.rb
+++ b/activesupport/lib/active_support/testing/isolation.rb
@@ -41,7 +41,23 @@ module ActiveSupport
pid = fork do
read.close
yield
- write.puts [Marshal.dump(self.dup)].pack("m")
+ begin
+ if error?
+ failures.map! { |e|
+ begin
+ Marshal.dump e
+ e
+ rescue TypeError
+ ex = Exception.new e.message
+ ex.set_backtrace e.backtrace
+ Minitest::UnexpectedError.new ex
+ end
+ }
+ end
+ result = Marshal.dump(self.dup)
+ end
+
+ write.puts [result].pack("m")
exit!
end
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index 07fc787550..3592dcba39 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -14,7 +14,7 @@ module ActiveSupport
# Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
# Time.zone.local(2007, 2, 10, 15, 30, 45) # => Sat, 10 Feb 2007 15:30:45 EST -05:00
# Time.zone.parse('2007-02-10 15:30:45') # => Sat, 10 Feb 2007 15:30:45 EST -05:00
- # Time.zone.at(1170361845) # => Sat, 10 Feb 2007 15:30:45 EST -05:00
+ # Time.zone.at(1171139445) # => Sat, 10 Feb 2007 15:30:45 EST -05:00
# Time.zone.now # => Sun, 18 May 2008 13:07:55 EDT -04:00
# Time.utc(2007, 2, 10, 20, 30, 45).in_time_zone # => Sat, 10 Feb 2007 15:30:45 EST -05:00
#
@@ -41,6 +41,9 @@ module ActiveSupport
'Time'
end
+ PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N".freeze }
+ PRECISIONS[0] = '%FT%T'.freeze
+
include Comparable
attr_reader :time_zone
@@ -142,11 +145,7 @@ module ActiveSupport
#
# Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00"
def xmlschema(fraction_digits = 0)
- fraction = if fraction_digits.to_i > 0
- (".%06i" % time.usec)[0, fraction_digits.to_i + 1]
- end
-
- "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}"
+ "#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z'.freeze)}"
end
alias_method :iso8601, :xmlschema
@@ -388,6 +387,11 @@ module ActiveSupport
end
alias_method :kind_of?, :is_a?
+ # An instance of ActiveSupport::TimeWithZone is never blank
+ def blank?
+ false
+ end
+
def freeze
period; utc; time # preload instance variables before freezing
super
diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb
index 5c87923a32..681f659100 100644
--- a/activesupport/lib/active_support/values/time_zone.rb
+++ b/activesupport/lib/active_support/values/time_zone.rb
@@ -1,5 +1,5 @@
require 'tzinfo'
-require 'thread_safe'
+require 'concurrent'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/object/try'
@@ -189,7 +189,7 @@ module ActiveSupport
UTC_OFFSET_WITH_COLON = '%s%02d:%02d'
UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(':', '')
- @lazy_zones_map = ThreadSafe::Cache.new
+ @lazy_zones_map = Concurrent::Map.new
class << self
# Assumes self represents an offset from UTC in seconds (as returned from
@@ -285,8 +285,12 @@ module ActiveSupport
end
end
- # Returns the offset of this time zone as a formatted string, of the
- # format "+HH:MM".
+ # Returns a formatted string of the offset from UTC, or an alternative
+ # string if the time zone is already UTC.
+ #
+ # zone = ActiveSupport::TimeZone['Central Time (US & Canada)']
+ # zone.formatted_offset # => "-06:00"
+ # zone.formatted_offset(false) # => "-0600"
def formatted_offset(colon=true, alternate_utc_string = nil)
utc_offset == 0 && alternate_utc_string || self.class.seconds_to_utc_offset(utc_offset, colon)
end
diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb
index cda9732cae..3b00ff87a0 100644
--- a/activesupport/test/callbacks_test.rb
+++ b/activesupport/test/callbacks_test.rb
@@ -766,34 +766,30 @@ module CallbacksTest
end
class CallbackFalseTerminatorWithoutConfigTest < ActiveSupport::TestCase
- def test_returning_false_halts_callback_if_config_variable_is_not_set
+ def test_returning_false_does_not_halt_callback_if_config_variable_is_not_set
obj = CallbackFalseTerminator.new
- assert_deprecated do
- obj.save
- assert_equal :second, obj.halted
- assert !obj.saved
- end
+ obj.save
+ assert_equal nil, obj.halted
+ assert obj.saved
end
end
class CallbackFalseTerminatorWithConfigTrueTest < ActiveSupport::TestCase
def setup
- ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = true
+ ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = true
end
- def test_returning_false_halts_callback_if_config_variable_is_true
+ def test_returning_false_does_not_halt_callback_if_config_variable_is_true
obj = CallbackFalseTerminator.new
- assert_deprecated do
- obj.save
- assert_equal :second, obj.halted
- assert !obj.saved
- end
+ obj.save
+ assert_equal nil, obj.halted
+ assert obj.saved
end
end
class CallbackFalseTerminatorWithConfigFalseTest < ActiveSupport::TestCase
def setup
- ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = false
+ ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = false
end
def test_returning_false_does_not_halt_callback_if_config_variable_is_false
diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb
index a7e12117f3..5a0b99e22c 100644
--- a/activesupport/test/core_ext/hash/transform_keys_test.rb
+++ b/activesupport/test/core_ext/hash/transform_keys_test.rb
@@ -24,9 +24,21 @@ class TransformKeysTest < ActiveSupport::TestCase
assert_equal Enumerator, enumerator.class
end
+ test "transform_keys! returns an Enumerator if no block is given" do
+ original = { a: 'a', b: 'b' }
+ enumerator = original.transform_keys!
+ assert_equal Enumerator, enumerator.class
+ end
+
test "transform_keys is chainable with Enumerable methods" do
original = { a: 'a', b: 'b' }
mapped = original.transform_keys.with_index { |k, i| [k, i].join.to_sym }
assert_equal({ a0: 'a', b1: 'b' }, mapped)
end
+
+ test "transform_keys! is chainable with Enumerable methods" do
+ original = { a: 'a', b: 'b' }
+ original.transform_keys!.with_index { |k, i| [k, i].join.to_sym }
+ assert_equal({ a0: 'a', b1: 'b' }, original)
+ end
end
diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb
index 45ed11fef7..7c33227dc0 100644
--- a/activesupport/test/core_ext/hash/transform_values_test.rb
+++ b/activesupport/test/core_ext/hash/transform_values_test.rb
@@ -53,9 +53,21 @@ class TransformValuesTest < ActiveSupport::TestCase
assert_equal Enumerator, enumerator.class
end
+ test "transform_values! returns an Enumerator if no block is given" do
+ original = { a: 'a', b: 'b' }
+ enumerator = original.transform_values!
+ assert_equal Enumerator, enumerator.class
+ end
+
test "transform_values is chainable with Enumerable methods" do
original = { a: 'a', b: 'b' }
mapped = original.transform_values.with_index { |v, i| [v, i].join }
assert_equal({ a: 'a0', b: 'b1' }, mapped)
end
+
+ test "transform_values! is chainable with Enumerable methods" do
+ original = { a: 'a', b: 'b' }
+ original.transform_values!.with_index { |v, i| [v, i].join }
+ assert_equal({ a: 'a0', b: 'b1' }, original)
+ end
end
diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb
index 128c5e3d1a..0b0f3a2808 100644
--- a/activesupport/test/core_ext/module/attribute_accessor_test.rb
+++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb
@@ -69,6 +69,20 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
end
end
assert_equal "invalid attribute name: 1nvalid", exception.message
+
+ exception = assert_raises NameError do
+ Class.new do
+ mattr_reader "valid_part\ninvalid_part"
+ end
+ end
+ assert_equal "invalid attribute name: valid_part\ninvalid_part", exception.message
+
+ exception = assert_raises NameError do
+ Class.new do
+ mattr_writer "valid_part\ninvalid_part"
+ end
+ end
+ assert_equal "invalid attribute name: valid_part\ninvalid_part", exception.message
end
def test_should_use_default_value_if_block_passed
diff --git a/activesupport/test/core_ext/object/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb
index 8a5e385dd7..a142096993 100644
--- a/activesupport/test/core_ext/object/blank_test.rb
+++ b/activesupport/test/core_ext/object/blank_test.rb
@@ -1,4 +1,3 @@
-
require 'abstract_unit'
require 'active_support/core_ext/object/blank'
diff --git a/activesupport/test/core_ext/object/try_test.rb b/activesupport/test/core_ext/object/try_test.rb
index 5ea0f0eca6..25bf0207b8 100644
--- a/activesupport/test/core_ext/object/try_test.rb
+++ b/activesupport/test/core_ext/object/try_test.rb
@@ -121,7 +121,7 @@ class ObjectTryTest < ActiveSupport::TestCase
assert_equal 5, Decorator.new(@string).size
end
- def test_try_with_overriden_method_on_delegator
+ def test_try_with_overridden_method_on_delegator
assert_equal 'overridden reverse', Decorator.new(@string).reverse
end
diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb
index ccb7f02331..c40f0bacbf 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -110,14 +110,14 @@ class TimeWithZoneTest < ActiveSupport::TestCase
@twz += 0.1234560001 # advance the time by a fraction of a second
assert_equal "1999-12-31T19:00:00.123-05:00", @twz.xmlschema(3)
assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(6)
- assert_equal "1999-12-31T19:00:00.123456-05:00", @twz.xmlschema(12)
+ assert_equal "1999-12-31T19:00:00.123456000100-05:00", @twz.xmlschema(12)
end
def test_xmlschema_with_fractional_seconds_lower_than_hundred_thousand
@twz += 0.001234 # advance the time by a fraction
assert_equal "1999-12-31T19:00:00.001-05:00", @twz.xmlschema(3)
assert_equal "1999-12-31T19:00:00.001234-05:00", @twz.xmlschema(6)
- assert_equal "1999-12-31T19:00:00.001234-05:00", @twz.xmlschema(12)
+ assert_equal "1999-12-31T19:00:00.001234000000-05:00", @twz.xmlschema(12)
end
def test_xmlschema_with_nil_fractional_seconds
diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb
index 18a8b92eb9..e6898658b5 100644
--- a/activesupport/test/inflector_test_cases.rb
+++ b/activesupport/test/inflector_test_cases.rb
@@ -1,4 +1,3 @@
-
module InflectorTestCases
SingularToPlural = {
"search" => "searches",
diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb
index d8704716e7..2a885e32bf 100644
--- a/activesupport/test/multibyte_conformance_test.rb
+++ b/activesupport/test/multibyte_conformance_test.rb
@@ -1,4 +1,3 @@
-
require 'abstract_unit'
require 'multibyte_test_helpers'
diff --git a/activesupport/test/multibyte_proxy_test.rb b/activesupport/test/multibyte_proxy_test.rb
index 11f5374017..360cf57302 100644
--- a/activesupport/test/multibyte_proxy_test.rb
+++ b/activesupport/test/multibyte_proxy_test.rb
@@ -1,4 +1,3 @@
-
require 'abstract_unit'
class MultibyteProxyText < ActiveSupport::TestCase
diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb
index 2e4b5cc873..58cf5488cd 100644
--- a/activesupport/test/multibyte_test_helpers.rb
+++ b/activesupport/test/multibyte_test_helpers.rb
@@ -1,4 +1,3 @@
-
module MultibyteTestHelpers
UNICODE_STRING = 'こにちわ'.freeze
ASCII_STRING = 'ohayo'.freeze
diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb
index 9e6d1a91d0..18228a2ac5 100644
--- a/activesupport/test/test_case_test.rb
+++ b/activesupport/test/test_case_test.rb
@@ -56,6 +56,14 @@ class AssertDifferenceTest < ActiveSupport::TestCase
end
end
+ def test_assert_difference_retval
+ incremented = assert_difference '@object.num', +1 do
+ @object.increment
+ end
+
+ assert_equal incremented, 1
+ end
+
def test_assert_difference_with_implicit_difference
assert_difference '@object.num' do
@object.increment
diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb
index 67f5f1cdd5..f50bcddbe7 100644
--- a/guides/source/_welcome.html.erb
+++ b/guides/source/_welcome.html.erb
@@ -16,9 +16,9 @@
<% end %>
<p>
The guides for earlier releases:
-<a href="http://guides.rubyonrails.org/v4.2.0/">Rails 4.2.0</a>,
-<a href="http://guides.rubyonrails.org/v4.1.8/">Rails 4.1.8</a>,
-<a href="http://guides.rubyonrails.org/v4.0.12/">Rails 4.0.12</a>,
-<a href="http://guides.rubyonrails.org/v3.2.21/">Rails 3.2.21</a> and
-<a href="http://guides.rubyonrails.org/v2.3.11/">Rails 2.3.11</a>.
+<a href="http://guides.rubyonrails.org/v4.2/">Rails 4.2</a>,
+<a href="http://guides.rubyonrails.org/v4.1/">Rails 4.1</a>,
+<a href="http://guides.rubyonrails.org/v4.0/">Rails 4.0</a>,
+<a href="http://guides.rubyonrails.org/v3.2/">Rails 3.2</a>, and
+<a href="http://guides.rubyonrails.org/v2.3/">Rails 2.3</a>.
</p>
diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md
index c39cd34e9a..4800cece82 100644
--- a/guides/source/action_mailer_basics.md
+++ b/guides/source/action_mailer_basics.md
@@ -760,8 +760,8 @@ config.action_mailer.smtp_settings = {
enable_starttls_auto: true }
```
Note: As of July 15, 2014, Google increased [its security measures](https://support.google.com/accounts/answer/6010255) and now blocks attempts from apps it deems less secure.
-You can change your gmail settings [here](https://www.google.com/settings/security/lesssecureapps) to allow the attempts or
-use another ESP to send email by replacing 'smpt.gmail.com' above with the address of your provider.
+You can change your gmail settings [here](https://www.google.com/settings/security/lesssecureapps) to allow the attempts or
+use another ESP to send email by replacing 'smtp.gmail.com' above with the address of your provider.
Mailer Testing
--------------
diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md
index 00c41a480e..76454e77c7 100644
--- a/guides/source/action_view_overview.md
+++ b/guides/source/action_view_overview.md
@@ -147,6 +147,39 @@ xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
end
```
+#### Jbuilder
+[Jbuilder](https://github.com/rails/jbuilder) is a gem that's
+maintained by the Rails team and included in the default Rails Gemfile.
+It's similar to Builder, but is used to generate JSON, instead of XML.
+
+If you don't have it, you can add the following to your Gemfile:
+
+```ruby
+gem 'jbuilder'
+```
+
+A Jbuilder object named `json` is automatically made available to templates with
+a `.jbuilder` extension.
+
+Here is a basic example:
+
+```ruby
+json.name("Alex")
+json.email("alex@example.com")
+```
+
+would produce:
+
+```json
+{
+ "name": "Alex",
+ "email: "alex@example.com"
+}
+```
+
+See the [Jbuilder documention](https://github.com/rails/jbuilder#jbuilder) for
+more examples and information.
+
#### Template Caching
By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will check the file's modification time and recompile it in development mode.
diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md
index dadac7fb54..7f88c13dc0 100644
--- a/guides/source/active_record_validations.md
+++ b/guides/source/active_record_validations.md
@@ -242,7 +242,7 @@ end
>> person = Person.new
>> person.valid?
->> person.errors.details[:name] #=> [{error: :blank}]
+>> person.errors.details[:name] # => [{error: :blank}]
```
Using `details` with custom validators is covered in the [Working with
diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md
index 01bf928407..367a1bf7c0 100644
--- a/guides/source/active_support_core_extensions.md
+++ b/guides/source/active_support_core_extensions.md
@@ -1865,15 +1865,15 @@ The methods `to_date`, `to_time`, and `to_datetime` are basically convenience wr
```ruby
"2010-07-27".to_date # => Tue, 27 Jul 2010
-"2010-07-27 23:37:00".to_time # => Tue Jul 27 23:37:00 UTC 2010
+"2010-07-27 23:37:00".to_time # => 2010-07-27 23:37:00 +0200
"2010-07-27 23:37:00".to_datetime # => Tue, 27 Jul 2010 23:37:00 +0000
```
`to_time` receives an optional argument `:utc` or `:local`, to indicate which time zone you want the time in:
```ruby
-"2010-07-27 23:42:00".to_time(:utc) # => Tue Jul 27 23:42:00 UTC 2010
-"2010-07-27 23:42:00".to_time(:local) # => Tue Jul 27 23:42:00 +0200 2010
+"2010-07-27 23:42:00".to_time(:utc) # => 2010-07-27 23:42:00 UTC
+"2010-07-27 23:42:00".to_time(:local) # => 2010-07-27 23:42:00 +0200
```
Default is `:utc`.
diff --git a/guides/source/api_app.md b/guides/source/api_app.md
index 28727a51bd..feaaff166a 100644
--- a/guides/source/api_app.md
+++ b/guides/source/api_app.md
@@ -194,7 +194,6 @@ An API application comes with the following middlewares by default:
- `ActionDispatch::RemoteIp`
- `ActionDispatch::Reloader`
- `ActionDispatch::Callbacks`
-- `ActionDispatch::ParamsParser`
- `Rack::Head`
- `Rack::ConditionalGet`
- `Rack::ETag`
@@ -292,9 +291,9 @@ instructions in the `Rack::Sendfile` documentation.
NOTE: The `Rack::Sendfile` middleware is always outside of the `Rack::Lock`
mutex, even in single-threaded applications.
-### Using ActionDispatch::ParamsParser
+### Using ActionDispatch::Request
-`ActionDispatch::ParamsParser` will take parameters from the client in the JSON
+`ActionDispatch::Request#params` will take parameters from the client in the JSON
format and make them available in your controller inside `params`.
To use this, your client will need to make a request with JSON-encoded parameters
@@ -313,7 +312,7 @@ jQuery.ajax({
});
```
-`ActionDispatch::ParamsParser` will see the `Content-Type` and your parameters
+`ActionDispatch::Request` will see the `Content-Type` and your parameters
will be:
```ruby
diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md
index a4feff798d..526bf768cc 100644
--- a/guides/source/api_documentation_guidelines.md
+++ b/guides/source/api_documentation_guidelines.md
@@ -239,7 +239,7 @@ You can quickly test the RDoc output with the following command:
```
$ echo "+:to_param+" | rdoc --pipe
-#=> <p><code>:to_param</code></p>
+# => <p><code>:to_param</code></p>
```
### Regular Font
diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md
index 1191f5edfe..999c533fb3 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -768,7 +768,7 @@ The `belongs_to` association creates a one-to-one match with another model. In d
When you declare a `belongs_to` association, the declaring class automatically gains five methods related to the association:
-* `association(force_reload = false)`
+* `association`
* `association=(associate)`
* `build_association(attributes = {})`
* `create_association(attributes = {})`
@@ -794,7 +794,7 @@ create_customer!
NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix.
-##### `association(force_reload = false)`
+##### `association`
The `association` method returns the associated object, if any. If no associated object is found, it returns `nil`.
@@ -802,7 +802,11 @@ The `association` method returns the associated object, if any. If no associated
@customer = @order.customer
```
-If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), pass `true` as the `force_reload` argument.
+If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload` on the parent object.
+
+```ruby
+@customer = @order.reload.customer
+```
##### `association=(associate)`
@@ -928,8 +932,11 @@ If you set the `:dependent` option to:
* `:destroy`, when the object is destroyed, `destroy` will be called on its
associated objects.
-* `:delete`, when the object is destroyed, all its associated objects will be
+* `:delete_all`, when the object is destroyed, all its associated objects will be
deleted directly from the database without calling their `destroy` method.
+* `:nullify`, causes the foreign key to be set to `NULL`. Callbacks are not executed.
+* `:restrict_with_exception`, causes an exception to be raised if there is an associated record
+* `:restrict_with_error`, causes an error to be added to the owner if there is an associated object
WARNING: You should not specify this option on a `belongs_to` association that is connected with a `has_many` association on the other class. Doing so can lead to orphaned records in your database.
@@ -1113,7 +1120,7 @@ The `has_one` association creates a one-to-one match with another model. In data
When you declare a `has_one` association, the declaring class automatically gains five methods related to the association:
-* `association(force_reload = false)`
+* `association`
* `association=(associate)`
* `build_association(attributes = {})`
* `create_association(attributes = {})`
@@ -1139,7 +1146,7 @@ create_account!
NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix.
-##### `association(force_reload = false)`
+##### `association`
The `association` method returns the associated object, if any. If no associated object is found, it returns `nil`.
@@ -1147,7 +1154,11 @@ The `association` method returns the associated object, if any. If no associated
@account = @supplier.account
```
-If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), pass `true` as the `force_reload` argument.
+If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload` on the parent object.
+
+```ruby
+@account = @supplier.reload.account
+```
##### `association=(associate)`
@@ -1380,7 +1391,7 @@ The `has_many` association creates a one-to-many relationship with another model
When you declare a `has_many` association, the declaring class automatically gains 16 methods related to the association:
-* `collection(force_reload = false)`
+* `collection`
* `collection<<(object, ...)`
* `collection.delete(object, ...)`
* `collection.destroy(object, ...)`
@@ -1408,7 +1419,7 @@ end
Each instance of the `Customer` model will have these methods:
```ruby
-orders(force_reload = false)
+orders
orders<<(object, ...)
orders.delete(object, ...)
orders.destroy(object, ...)
@@ -1426,7 +1437,7 @@ orders.create(attributes = {})
orders.create!(attributes = {})
```
-##### `collection(force_reload = false)`
+##### `collection`
The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array.
@@ -1892,7 +1903,7 @@ The `has_and_belongs_to_many` association creates a many-to-many relationship wi
When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 16 methods related to the association:
-* `collection(force_reload = false)`
+* `collection`
* `collection<<(object, ...)`
* `collection.delete(object, ...)`
* `collection.destroy(object, ...)`
@@ -1920,7 +1931,7 @@ end
Each instance of the `Part` model will have these methods:
```ruby
-assemblies(force_reload = false)
+assemblies
assemblies<<(object, ...)
assemblies.delete(object, ...)
assemblies.destroy(object, ...)
@@ -1945,7 +1956,7 @@ If the join table for a `has_and_belongs_to_many` association has additional col
WARNING: The use of extra attributes on the join table in a `has_and_belongs_to_many` association is deprecated. If you require this sort of complex behavior on the table that joins two models in a many-to-many relationship, you should use a `has_many :through` association instead of `has_and_belongs_to_many`.
-##### `collection(force_reload = false)`
+##### `collection`
The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array.
diff --git a/guides/source/command_line.md b/guides/source/command_line.md
index cd265331d6..e85f9fc9c6 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -412,7 +412,7 @@ Ruby version 2.2.2 (x86_64-linux)
RubyGems version 2.4.6
Rack version 1.6
JavaScript Runtime Node.js (V8)
-Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag
+Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, Rack::Head, Rack::ConditionalGet, Rack::ETag
Application root /home/foobar/commandsapp
Environment development
Database adapter sqlite3
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 5e72b96787..87114c4ef0 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -214,7 +214,6 @@ Every Rails application comes with a standard set of middleware which it uses in
* `ActionDispatch::Cookies` sets cookies for the request.
* `ActionDispatch::Session::CookieStore` is responsible for storing the session in cookies. An alternate middleware can be used for this by changing the `config.action_controller.session_store` to an alternate value. Additionally, options passed to this can be configured by using `config.action_controller.session_options`.
* `ActionDispatch::Flash` sets up the `flash` keys. Only available if `config.action_controller.session_store` is set to a value.
-* `ActionDispatch::ParamsParser` parses out parameters from the request into `params`.
* `Rack::MethodOverride` allows the method to be overridden if `params[:_method]` is set. This is the middleware which supports the PATCH, PUT, and DELETE HTTP method types.
* `Rack::Head` converts HEAD requests to GET requests and serves them as so.
@@ -245,7 +244,7 @@ config.middleware.swap ActionController::Failsafe, Lifo::Failsafe
They can also be removed from the stack completely:
```ruby
-config.middleware.delete "Rack::MethodOverride"
+config.middleware.delete Rack::MethodOverride
```
### Configuring i18n
@@ -536,7 +535,7 @@ There are a few configuration options available in Active Support:
* `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`.
-* `config.active_support.halt_callback_chains_on_return_false` specifies whether ActiveRecord, ActiveModel and ActiveModel::Validations callback chains can be halted by returning `false` in a 'before' callback. Defaults to `true`.
+* `ActiveSupport.halt_callback_chains_on_return_false` specifies whether Active Record and Active Model callback chains can be halted by returning `false` in a 'before' callback. Defaults to `true`.
* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`.
@@ -642,7 +641,7 @@ TIP: You don't have to update the database configurations manually. If you look
### Connection Preference
-Since there are two ways to set your connection, via environment variable it is important to understand how the two can interact.
+Since there are two ways to configure your connection (using `config/database.yml` or using an environment variable) it is important to understand how they can interact.
If you have an empty `config/database.yml` file but your `ENV['DATABASE_URL']` is present, then Rails will connect to the database via your environment variable:
diff --git a/guides/source/engines.md b/guides/source/engines.md
index 3b1588b75a..71844b7990 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -150,7 +150,7 @@ When you include the engine into an application later on, you will do so with
this line in the Rails application's `Gemfile`:
```ruby
-gem 'blorgh', path: "vendor/engines/blorgh"
+gem 'blorgh', path: 'engines/blorgh'
```
Don't forget to run `bundle install` as usual. By specifying it as a gem within
@@ -639,7 +639,7 @@ However, because you are developing the `blorgh` engine on your local machine,
you will need to specify the `:path` option in your `Gemfile`:
```ruby
-gem 'blorgh', path: "/path/to/blorgh"
+gem 'blorgh', path: 'engines/blorgh'
```
Then run `bundle` to install the gem.
@@ -843,28 +843,10 @@ above the "Title" output inside `app/views/blorgh/articles/show.html.erb`:
```html+erb
<p>
<b>Author:</b>
- <%= @article.author %>
+ <%= @article.author.name %>
</p>
```
-By outputting `@article.author` using the `<%=` tag, the `to_s` method will be
-called on the object. By default, this will look quite ugly:
-
-```
-#<User:0x00000100ccb3b0>
-```
-
-This is undesirable. It would be much better to have the user's name there. To
-do this, add a `to_s` method to the `User` class within the application:
-
-```ruby
-def to_s
- name
-end
-```
-
-Now instead of the ugly Ruby object output, the author's name will be displayed.
-
#### Using a Controller Provided by the Application
Because Rails controllers generally share code for things like authentication
diff --git a/guides/source/i18n.md b/guides/source/i18n.md
index 987320a0f8..ea79855919 100644
--- a/guides/source/i18n.md
+++ b/guides/source/i18n.md
@@ -109,7 +109,7 @@ The **translations load path** (`I18n.load_path`) is an array of paths to files
NOTE: The backend lazy-loads these translations when a translation is looked up for the first time. This backend can be swapped with something else even after translations have already been announced.
-The default `application.rb` file has instructions on how to add locales from another directory and how to set a different default locale.
+The default `config/application.rb` file has instructions on how to add locales from another directory and how to set a different default locale.
```ruby
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
@@ -117,7 +117,7 @@ The default `application.rb` file has instructions on how to add locales from an
# config.i18n.default_locale = :de
```
-The load path must be specified before any translations are looked up. To change the default locale from an initializer instead of `application.rb`:
+The load path must be specified before any translations are looked up. To change the default locale from an initializer instead of `config/application.rb`:
```ruby
# config/initializers/locale.rb
diff --git a/guides/source/kindle/layout.html.erb b/guides/source/kindle/layout.html.erb
index f0a286210b..fd8746776b 100644
--- a/guides/source/kindle/layout.html.erb
+++ b/guides/source/kindle/layout.html.erb
@@ -14,12 +14,12 @@
<% if content_for? :header_section %>
<%= yield :header_section %>
- <div class="pagebreak">
+ <div class="pagebreak"></div>
<% end %>
<% if content_for? :index_section %>
<%= yield :index_section %>
- <div class="pagebreak">
+ <div class="pagebreak"></div>
<% end %>
<%= yield.html_safe %>
diff --git a/guides/source/kindle/toc.ncx.erb b/guides/source/kindle/toc.ncx.erb
index 2c6d8e3bdf..5094fea4ca 100644
--- a/guides/source/kindle/toc.ncx.erb
+++ b/guides/source/kindle/toc.ncx.erb
@@ -32,12 +32,12 @@
</navPoint>
<navPoint class="article" id="credits" playOrder="3">
<navLabel><text>Credits</text></navLabel>
- <content src="credits.html">
+ <content src="credits.html"/>
</navPoint>
<navPoint class="article" id="copyright" playOrder="4">
<navLabel><text>Copyright &amp; License</text></navLabel>
- <content src="copyright.html">
- </navPoint>
+ <content src="copyright.html"/>
+ </navPoint>
</navPoint>
<% play_order = 4 %>
@@ -47,7 +47,7 @@
<text><%= section['name'] %></text>
</navLabel>
<content src="<%=section['documents'].first['url'] %>"/>
-
+
<% section['documents'].each_with_index do |document, document_no| %>
<navPoint class="article" id="_<%=section_no+1%>.<%=document_no+1%>" playOrder="<%=play_order +=1 %>">
<navLabel>
diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md
index 1e2fe94010..87f869aff3 100644
--- a/guides/source/rails_on_rack.md
+++ b/guides/source/rails_on_rack.md
@@ -121,7 +121,6 @@ use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
-use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
@@ -172,7 +171,7 @@ Add the following lines to your application configuration:
```ruby
# config/application.rb
-config.middleware.delete "Rack::Lock"
+config.middleware.delete Rack::Lock
```
And now if you inspect the middleware stack, you'll find that `Rack::Lock` is
@@ -192,16 +191,16 @@ If you want to remove session related middleware, do the following:
```ruby
# config/application.rb
-config.middleware.delete "ActionDispatch::Cookies"
-config.middleware.delete "ActionDispatch::Session::CookieStore"
-config.middleware.delete "ActionDispatch::Flash"
+config.middleware.delete ActionDispatch::Cookies
+config.middleware.delete ActionDispatch::Session::CookieStore
+config.middleware.delete ActionDispatch::Flash
```
And to remove browser related middleware,
```ruby
# config/application.rb
-config.middleware.delete "Rack::MethodOverride"
+config.middleware.delete Rack::MethodOverride
```
### Internal Middleware Stack
@@ -284,10 +283,6 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
* Sets up the flash keys. Only available if `config.action_controller.session_store` is set to a value.
-**`ActionDispatch::ParamsParser`**
-
-* Parses out parameters from the request into `params`.
-
**`Rack::Head`**
* Converts HEAD requests to `GET` requests and serves them as so.
diff --git a/guides/source/routing.md b/guides/source/routing.md
index e4799d93fa..1fd38c0940 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -7,7 +7,7 @@ This guide covers the user-facing features of Rails routing.
After reading this guide, you will know:
-* How to interpret the code in `routes.rb`.
+* How to interpret the code in `config/routes.rb`.
* How to construct your own routes, using either the preferred resourceful style or the `match` method.
* What parameters to expect an action to receive.
* How to automatically create paths and URLs using route helpers.
@@ -1118,7 +1118,7 @@ Rails offers facilities for inspecting and testing your routes.
To get a complete list of the available routes in your application, visit `http://localhost:3000/rails/info/routes` in your browser while your server is running in the **development** environment. You can also execute the `rake routes` command in your terminal to produce the same output.
-Both methods will list all of your routes, in the same order that they appear in `routes.rb`. For each route, you'll see:
+Both methods will list all of your routes, in the same order that they appear in `config/routes.rb`. For each route, you'll see:
* The route name (if any)
* The HTTP verb used (if the route doesn't respond to all verbs)
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index 52464a1c51..490bda3571 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -55,23 +55,26 @@ Upgrading from Rails 4.2 to Rails 5.0
### Halting callback chains by returning `false`
-In Rails 4.2, when a 'before' callback returns `false` in ActiveRecord,
-ActiveModel and ActiveModel::Validations, then the entire callback chain
-is halted. In other words, successive 'before' callbacks are not executed,
-and neither is the action wrapped in callbacks.
+In Rails 4.2, when a 'before' callback returns `false` in Active Record
+and Active Model, then the entire callback chain is halted. In other words,
+successive 'before' callbacks are not executed, and neither is the action wrapped
+in callbacks.
-In Rails 5.0, returning `false` in a callback will not have this side effect
-of halting the callback chain. Instead, callback chains must be explicitly
-halted by calling `throw(:abort)`.
+In Rails 5.0, returning `false` in an Active Record or Active Model callback
+will not have this side effect of halting the callback chain. Instead, callback
+chains must be explicitly halted by calling `throw(:abort)`.
-When you upgrade from Rails 4.2 to Rails 5.0, returning `false` in a callback
-will still halt the callback chain, but you will receive a deprecation warning
-about this upcoming change.
+When you upgrade from Rails 4.2 to Rails 5.0, returning `false` in those kind of
+callbacks will still halt the callback chain, but you will receive a deprecation
+warning about this upcoming change.
When you are ready, you can opt into the new behavior and remove the deprecation
warning by adding the following configuration to your `config/application.rb`:
- config.active_support.halt_callback_chains_on_return_false = false
+ ActiveSupport.halt_callback_chains_on_return_false = false
+
+Note that this option will not affect Active Support callbacks since they never
+halted the chain when any value was returned.
See [#17227](https://github.com/rails/rails/pull/17227) for more details.
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index f007d11c72..3e45a09dec 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,3 +1,65 @@
+* Add fail fast to `bin/rails test`
+
+ Adding `--fail-fast` or `-f` when running tests will interrupt the run on
+ the first failure:
+
+ ```
+ # Running:
+
+ ................................................S......E
+
+ ArgumentError: Wups! Bet you didn't expect this!
+ test/models/bunny_test.rb:19:in `block in <class:BunnyTest>'
+
+ bin/rails test test/models/bunny_test.rb:18
+
+ ....................................F
+
+ This failed
+
+ bin/rails test test/models/bunny_test.rb:14
+
+ Interrupted. Exiting...
+
+
+ Finished in 0.051427s, 1808.3872 runs/s, 1769.4972 assertions/s.
+
+ ```
+
+ Note that any unexpected errors don't abort the run.
+
+ *Kasper Timm Hansen*
+
+* Add inline output to `bin/rails test`
+
+ Any failures or errors (and skips if running in verbose mode) are output
+ during a test run:
+
+ ```
+ # Running:
+
+ .....S..........................................F
+
+ This failed
+
+ bin/rails test test/models/bunny_test.rb:14
+
+ .................................E
+
+ ArgumentError: Wups! Bet you didn't expect this!
+ test/models/bunny_test.rb:19:in `block in <class:BunnyTest>'
+
+ bin/rails test test/models/bunny_test.rb:18
+
+ ....................
+
+ Finished in 0.069708s, 1477.6019 runs/s, 1448.9106 assertions/s.
+ ```
+
+ Output can be deferred to after a run with the `--defer-output` option.
+
+ *Kasper Timm Hansen*
+
* Fix displaying mailer previews on non local requests when config
`action_mailer.show_previews` is set
@@ -234,10 +296,11 @@
Newly generated Rails apps have a new initializer called
`callback_terminator.rb` which sets the value of the configuration option
- `config.active_support.halt_callback_chains_on_return_false` to `false`.
+ `ActiveSupport.halt_callback_chains_on_return_false` to `false`.
- As a result, new Rails apps do not halt callback chains when a callback
- returns `false`; only when they are explicitly halted with `throw(:abort)`.
+ As a result, new Rails apps do not halt Active Record and Active Model
+ callback chains when a callback returns `false`; only when they are
+ explicitly halted with `throw(:abort)`.
The terminator is *not* added when running `rake rails:update`, so returning
`false` will still work on old apps ported to Rails 5, displaying a
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index 8b109c90b1..7916e24af1 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -156,15 +156,6 @@ module Rails
self
end
- # Implements call according to the Rack API. It simply
- # dispatches the request to the underlying middleware stack.
- def call(env)
- req = ActionDispatch::Request.new env
- env["ORIGINAL_FULLPATH"] = req.fullpath
- env["ORIGINAL_SCRIPT_NAME"] = req.script_name
- super(env)
- end
-
# Reload application routes regardless if they changed or not.
def reload_routes!
routes_reloader.reload!
@@ -514,5 +505,18 @@ module Rails
end
end
end
+
+ private
+
+ def build_request(env)
+ req = super
+ env["ORIGINAL_FULLPATH"] = req.fullpath
+ env["ORIGINAL_SCRIPT_NAME"] = req.script_name
+ req
+ end
+
+ def build_middleware
+ config.app_middleware + super
+ end
end
end
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index 4fc7a1db62..75112f29b6 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -36,7 +36,6 @@ module Rails
@time_zone = "UTC"
@beginning_of_week = :monday
@log_level = nil
- @middleware = app_middleware
@generators = app_generators
@cache_store = [ :file_store, "#{root}/tmp/cache/" ]
@railties_order = [:all]
diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb
index 88eade5c5a..21062f3a53 100644
--- a/railties/lib/rails/application/default_middleware_stack.rb
+++ b/railties/lib/rails/application/default_middleware_stack.rb
@@ -72,7 +72,6 @@ module Rails
middleware.use ::ActionDispatch::Flash
end
- middleware.use ::ActionDispatch::ParamsParser
middleware.use ::Rack::Head
middleware.use ::Rack::ConditionalGet
middleware.use ::Rack::ETag, "no-cache"
diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb
index d99d27a756..30eafd59f2 100644
--- a/railties/lib/rails/configuration.rb
+++ b/railties/lib/rails/configuration.rb
@@ -33,9 +33,9 @@ module Rails
# config.middleware.delete ActionDispatch::Flash
#
class MiddlewareStackProxy
- def initialize
- @operations = []
- @delete_operations = []
+ def initialize(operations = [], delete_operations = [])
+ @operations = operations
+ @delete_operations = delete_operations
end
def insert_before(*args, &block)
@@ -71,6 +71,19 @@ module Rails
other
end
+
+ def +(other) # :nodoc:
+ MiddlewareStackProxy.new(@operations + other.operations, @delete_operations + other.delete_operations)
+ end
+
+ protected
+ def operations
+ @operations
+ end
+
+ def delete_operations
+ @delete_operations
+ end
end
class Generators #:nodoc:
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index e90d41cbec..c4bb7d2109 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -2,6 +2,7 @@ require 'rails/railtie'
require 'rails/engine/railties'
require 'active_support/core_ext/module/delegation'
require 'pathname'
+require 'thread'
module Rails
# <tt>Rails::Engine</tt> allows you to wrap a specific Rails application or subset of
@@ -434,6 +435,7 @@ module Rails
@env_config = nil
@helpers = nil
@routes = nil
+ @app_build_lock = Mutex.new
super
end
@@ -504,10 +506,13 @@ module Rails
# Returns the underlying rack application for this engine.
def app
- @app ||= begin
- config.middleware = config.middleware.merge_into(default_middleware_stack)
- config.middleware.build(endpoint)
- end
+ @app || @app_build_lock.synchronize {
+ @app ||= begin
+ stack = default_middleware_stack
+ config.middleware = build_middleware.merge_into(stack)
+ config.middleware.build(endpoint)
+ end
+ }
end
# Returns the endpoint for this engine. If none is registered,
@@ -518,11 +523,8 @@ module Rails
# Define the Rack API for this engine.
def call(env)
- env.merge!(env_config)
- req = ActionDispatch::Request.new env
- req.routes = routes
- req.engine_script_name = req.script_name
- app.call(env)
+ req = build_request env
+ app.call req.env
end
# Defines additional Rack env configuration that is added on each call.
@@ -689,5 +691,19 @@ module Rails
def _all_load_paths #:nodoc:
@_all_load_paths ||= (config.paths.load_paths + _all_autoload_paths).uniq
end
+
+ private
+
+ def build_request(env)
+ env.merge!(env_config)
+ req = ActionDispatch::Request.new env
+ req.routes = routes
+ req.engine_script_name = req.script_name
+ req
+ end
+
+ def build_middleware
+ config.middleware
+ end
end
end
diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb
index 62a4139d07..b4ddee3b1b 100644
--- a/railties/lib/rails/engine/configuration.rb
+++ b/railties/lib/rails/engine/configuration.rb
@@ -4,17 +4,14 @@ module Rails
class Engine
class Configuration < ::Rails::Railtie::Configuration
attr_reader :root
- attr_writer :middleware, :eager_load_paths, :autoload_once_paths, :autoload_paths
+ attr_accessor :middleware
+ attr_writer :eager_load_paths, :autoload_once_paths, :autoload_paths
def initialize(root=nil)
super()
@root = root
@generators = app_generators.dup
- end
-
- # Returns the middleware stack for the engine.
- def middleware
- @middleware ||= Rails::Configuration::MiddlewareStackProxy.new
+ @middleware = Rails::Configuration::MiddlewareStackProxy.new
end
# Holds generators configuration:
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt
new file mode 100644
index 0000000000..f80631bac6
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt
@@ -0,0 +1,8 @@
+
+<% unless options.api? -%>
+//= link_tree ../images
+<% end -%>
+<% unless options.skip_javascript -%>
+//= link_directory ../javascripts .js
+<% end -%>
+//= link_directory ../stylesheets .css
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/manifest.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/manifest.js.tt
deleted file mode 100644
index 3325553f57..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/manifest.js.tt
+++ /dev/null
@@ -1,8 +0,0 @@
-
-<% unless options.api? -%>
-//= link_tree ./images
-<% end -%>
-<% unless options.skip_javascript -%>
-//= link ./javascripts/application.js
-<% end -%>
-//= link ./stylesheets/application.css
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb
index 45c44d24f8..9fca213a04 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cors.rb
@@ -3,7 +3,7 @@
# Read more: https://github.com/cyu/rack-cors
-# Rails.application.config.middleware.insert_before 0, "Rack::Cors" do
+# Rails.application.config.middleware.insert_before 0, Rack::Cors do
# allow do
# origins 'example.com'
#
diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
index 8fea30189e..910c4e743e 100644
--- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
+++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
@@ -106,7 +106,7 @@ task default: :test
def test_dummy_assets
template "rails/javascripts.js", "#{dummy_path}/app/assets/javascripts/application.js", force: true
template "rails/stylesheets.css", "#{dummy_path}/app/assets/stylesheets/application.css", force: true
- template "rails/dummy_manifest.js", "#{dummy_path}/app/assets/manifest.js", force: true
+ template "rails/dummy_manifest.js", "#{dummy_path}/app/assets/config/manifest.js", force: true
end
def test_dummy_clean
@@ -124,7 +124,7 @@ task default: :test
end
def assets_manifest
- template "rails/engine_manifest.js", "app/assets/#{underscored_name}_manifest.js"
+ template "rails/engine_manifest.js", "app/assets/config/#{underscored_name}_manifest.js"
end
def stylesheets
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js
index ace695a811..8d21b2b6fb 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/dummy_manifest.js
@@ -1,11 +1,11 @@
<% unless api? -%>
-//= link_tree ./images
+//= link_tree ../images
<% end -%>
<% unless options.skip_javascript -%>
-//= link ./javascripts/application.js
+//= link_directory ../javascripts .js
<% end -%>
-//= link ./stylesheets/application.css
+//= link_directory ../stylesheets .css
<% if mountable? && !api? -%>
//= link <%= underscored_name %>_manifest.js
<% end -%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js
index f8ef26982a..2f23844f5e 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/engine_manifest.js
@@ -1,6 +1,6 @@
<% if mountable? -%>
-<% unless options.skip_javascript -%>
-//= link ./javascripts/<%= namespaced_name %>/application.js
+<% if !options.skip_javascript -%>
+//= link_directory ../javascripts/<%= namespaced_name %> .js
<% end -%>
-//= link ./stylesheets/<%= namespaced_name %>/application.css
+//= link_directory ../stylesheets/<%= namespaced_name %> .css
<% end -%>
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
index 2c3b04043f..f73e9a96ba 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
+++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
@@ -1,5 +1,5 @@
<% if namespaced? -%>
-require_dependency "<%= namespaced_file_path %>/application_controller"
+require_dependency "<%= namespaced_path %>/application_controller"
<% end -%>
<% module_namespacing do -%>
diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb
index ed0ea9dd4d..38004fe99a 100644
--- a/railties/lib/rails/mailers_controller.rb
+++ b/railties/lib/rails/mailers_controller.rb
@@ -31,7 +31,7 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc:
raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{@email_action}"
end
else
- @part = find_preferred_part(request.format, Mime::HTML, Mime::TEXT)
+ @part = find_preferred_part(request.format, Mime::Type[:HTML], Mime::Type[:TEXT])
render action: 'email', layout: false, formats: %w[html]
end
else
diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb
index bb6e49966d..cf95cd8ad0 100644
--- a/railties/lib/rails/templates/rails/mailers/email.html.erb
+++ b/railties/lib/rails/templates/rails/mailers/email.html.erb
@@ -95,8 +95,8 @@
<% if @email.multipart? %>
<dd>
<select onchange="document.getElementsByName('messageBody')[0].src=this.options[this.selectedIndex].value;">
- <option <%= request.format == Mime::HTML ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option>
- <option <%= request.format == Mime::TEXT ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option>
+ <option <%= request.format == Mime::Type[:HTML] ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option>
+ <option <%= request.format == Mime::Type[:TEXT] ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option>
</select>
</dd>
<% end %>
diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb
index dacab08ec3..3a0a58df88 100644
--- a/railties/lib/rails/test_unit/minitest_plugin.rb
+++ b/railties/lib/rails/test_unit/minitest_plugin.rb
@@ -14,6 +14,8 @@ module Minitest
opts.separator ""
opts.separator " bin/rails test test/controllers test/integration/login_test.rb"
opts.separator ""
+ opts.separator "By default test failures and errors are reported inline during a run."
+ opts.separator ""
opts.separator "Rails options:"
opts.on("-e", "--environment ENV",
@@ -26,6 +28,16 @@ module Minitest
options[:full_backtrace] = true
end
+ opts.on("-d", "--defer-output",
+ "Output test failures and errors after the test run") do
+ options[:output_inline] = false
+ end
+
+ opts.on("-f", "--fail-fast",
+ "Abort test run on first failure") do
+ options[:fail_fast] = true
+ end
+
options[:patterns] = opts.order!
end
@@ -42,7 +54,8 @@ module Minitest
ENV["RAILS_ENV"] = options[:environment] || "test"
unless run_with_autorun
- ::Rails::TestRequirer.require_files @rake_patterns || options[:patterns]
+ patterns = defined?(@rake_patterns) ? @rake_patterns : options[:patterns]
+ ::Rails::TestRequirer.require_files(patterns)
end
unless options[:full_backtrace] || ENV["BACKTRACE"]
diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb
index 09b8675cf8..8f1116b6af 100644
--- a/railties/lib/rails/test_unit/reporter.rb
+++ b/railties/lib/rails/test_unit/reporter.rb
@@ -6,8 +6,25 @@ module Rails
class_attribute :executable
self.executable = "bin/rails test"
+ def record(result)
+ super
+
+ if output_inline? && result.failure && (!result.skipped? || options[:verbose])
+ io.puts
+ io.puts
+ io.puts result.failures.map(&:message)
+ io.puts
+ io.puts format_rerun_snippet(result)
+ io.puts
+ end
+
+ if fail_fast? && result.failure && !result.error? && !result.skipped?
+ raise Interrupt
+ end
+ end
+
def report
- return if filtered_results.empty?
+ return if output_inline? || filtered_results.empty?
io.puts
io.puts "Failed tests:"
io.puts
@@ -15,10 +32,7 @@ module Rails
end
def aggregated_results # :nodoc:
- filtered_results.map do |result|
- location, line = result.method(result.name).source_location
- "#{self.executable} #{relative_path_for(location)}:#{line}"
- end.join "\n"
+ filtered_results.map { |result| format_rerun_snippet(result) }.join "\n"
end
def filtered_results
@@ -32,5 +46,19 @@ module Rails
def relative_path_for(file)
file.sub(/^#{Rails.root}\/?/, '')
end
+
+ private
+ def output_inline?
+ options.fetch(:output_inline, true)
+ end
+
+ def fail_fast?
+ options[:fail_fast]
+ end
+
+ def format_rerun_snippet(result)
+ location, line = result.method(result.name).source_location
+ "#{self.executable} #{relative_path_for(location)}:#{line}"
+ end
end
end
diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb
index fef65adda2..8b83784ed6 100644
--- a/railties/test/application/asset_debugging_test.rb
+++ b/railties/test/application/asset_debugging_test.rb
@@ -7,7 +7,10 @@ module ApplicationTests
include Rack::Test::Methods
def setup
- build_app(initializers: true)
+ # FIXME: shush Sass warning spam, not relevant to testing Railties
+ Kernel.silence_warnings do
+ build_app(initializers: true)
+ end
app_file "app/assets/javascripts/application.js", "//= require_tree ."
app_file "app/assets/javascripts/xmlhr.js", "function f1() { alert(); }"
@@ -33,12 +36,19 @@ module ApplicationTests
teardown_app
end
+ # FIXME: shush Sass warning spam, not relevant to testing Railties
+ def get(*)
+ Kernel.silence_warnings { super }
+ end
+
test "assets are concatenated when debug is off and compile is off either if debug_assets param is provided" do
# config.assets.debug and config.assets.compile are false for production environment
ENV["RAILS_ENV"] = "production"
output = Dir.chdir(app_path){ `bin/rake assets:precompile --trace 2>&1` }
assert $?.success?, output
- require "#{app_path}/config/environment"
+
+ # Load app env
+ app "production"
class ::PostsController < ActionController::Base ; end
@@ -51,8 +61,8 @@ module ApplicationTests
test "assets are served with sourcemaps when compile is true and debug_assets params is true" do
add_to_env_config "production", "config.assets.compile = true"
- ENV["RAILS_ENV"] = "production"
- require "#{app_path}/config/environment"
+ # Load app env
+ app "production"
class ::PostsController < ActionController::Base ; end
diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb
index ddcd51f3fc..dca5cf2e5b 100644
--- a/railties/test/application/assets_test.rb
+++ b/railties/test/application/assets_test.rb
@@ -17,14 +17,23 @@ module ApplicationTests
end
def precompile!(env = nil)
- quietly do
- precompile_task = "bin/rake assets:precompile #{env} --trace 2>&1"
- output = Dir.chdir(app_path) { %x[ #{precompile_task} ] }
- assert $?.success?, output
- output
+ with_env env.to_h do
+ quietly do
+ precompile_task = "bin/rake assets:precompile --trace 2>&1"
+ output = Dir.chdir(app_path) { %x[ #{precompile_task} ] }
+ assert $?.success?, output
+ output
+ end
end
end
+ def with_env(env)
+ env.each { |k, v| ENV[k.to_s] = v }
+ yield
+ ensure
+ env.each_key { |k| ENV.delete k.to_s }
+ end
+
def clean_assets!
quietly do
assert Dir.chdir(app_path) { system('bin/rake assets:clobber') }
@@ -32,7 +41,8 @@ module ApplicationTests
end
def assert_file_exists(filename)
- assert Dir[filename].first, "missing #{filename}"
+ globbed = Dir[filename]
+ assert globbed.one?, "Found #{globbed.size} files matching #{filename}. All files in the directory: #{Dir.entries(File.dirname(filename)).inspect}"
end
def assert_no_file_exists(filename)
@@ -51,7 +61,10 @@ module ApplicationTests
add_to_env_config "development", "config.assets.digest = false"
- require "#{app_path}/config/environment"
+ # FIXME: shush Sass warning spam, not relevant to testing Railties
+ Kernel.silence_warnings do
+ require "#{app_path}/config/environment"
+ end
get "/assets/demo.js"
assert_equal 'a = "/assets/rails.png";', last_response.body.strip
@@ -62,8 +75,8 @@ module ApplicationTests
add_to_env_config "production", "config.assets.compile = true"
add_to_env_config "production", "config.assets.precompile = []"
- ENV["RAILS_ENV"] = "production"
- require "#{app_path}/config/environment"
+ # Load app env
+ app "production"
assert !defined?(Uglifier)
get "/assets/demo.js"
@@ -72,11 +85,10 @@ module ApplicationTests
end
test "precompile creates the file, gives it the original asset's content and run in production as default" do
- app_file "app/assets/manifest.js", "//= link_tree ./javascripts"
+ app_file "app/assets/config/manifest.js", "//= link_tree ../javascripts"
app_file "app/assets/javascripts/application.js", "alert();"
app_file "app/assets/javascripts/foo/application.js", "alert();"
- ENV["RAILS_ENV"] = nil
precompile!
files = Dir["#{app_path}/public/assets/application-*.js"]
@@ -88,7 +100,7 @@ module ApplicationTests
end
def test_precompile_does_not_hit_the_database
- app_file "app/assets/manifest.js", "//= link_tree ./javascripts"
+ app_file "app/assets/config/manifest.js", "//= link_tree ../javascripts"
app_file "app/assets/javascripts/application.js", "alert();"
app_file "app/assets/javascripts/foo/application.js", "alert();"
app_file "app/controllers/users_controller.rb", <<-eoruby
@@ -98,10 +110,9 @@ module ApplicationTests
class User < ActiveRecord::Base; raise 'should not be reached'; end
eoruby
- ENV['RAILS_ENV'] = 'production'
- ENV['DATABASE_URL'] = 'postgresql://baduser:badpass@127.0.0.1/dbname'
-
- precompile!
+ precompile! \
+ RAILS_ENV: 'production',
+ DATABASE_URL: 'postgresql://baduser:badpass@127.0.0.1/dbname'
files = Dir["#{app_path}/public/assets/application-*.js"]
files << Dir["#{app_path}/public/assets/foo/application-*.js"].first
@@ -109,9 +120,6 @@ module ApplicationTests
assert_not_nil file, "Expected application.js asset to be generated, but none found"
assert_equal "alert();".strip, File.read(file).strip
end
- ensure
- ENV.delete 'RAILS_ENV'
- ENV.delete 'DATABASE_URL'
end
test "precompile application.js and application.css and all other non JS/CSS files" do
@@ -171,10 +179,9 @@ module ApplicationTests
test 'precompile use assets defined in app env config' do
add_to_env_config 'production', 'config.assets.precompile = [ "something.js" ]'
-
app_file 'app/assets/javascripts/something.js.erb', 'alert();'
- precompile! 'RAILS_ENV=production'
+ precompile! RAILS_ENV: 'production'
assert_file_exists("#{app_path}/public/assets/something-*.js")
end
@@ -183,15 +190,17 @@ module ApplicationTests
add_to_config 'config.assets.precompile = [ "something_manifest.js" ]'
add_to_env_config 'production', 'config.assets.precompile += [ "another_manifest.js" ]'
- app_file 'app/assets/something_manifest.js', '//= link ./javascripts/something.js'
- app_file 'app/assets/another_manifest.js', '//= link ./javascripts/another.js'
+ app_file 'app/assets/config/something_manifest.js', '//= link something.js'
+ app_file 'app/assets/config/another_manifest.js', '//= link another.js'
app_file 'app/assets/javascripts/something.js.erb', 'alert();'
app_file 'app/assets/javascripts/another.js.erb', 'alert();'
- precompile! 'RAILS_ENV=production'
+ precompile! RAILS_ENV: 'production'
+ assert_file_exists("#{app_path}/public/assets/something_manifest-*.js")
assert_file_exists("#{app_path}/public/assets/something-*.js")
+ assert_file_exists("#{app_path}/public/assets/another_manifest-*.js")
assert_file_exists("#{app_path}/public/assets/another-*.js")
end
@@ -199,8 +208,8 @@ module ApplicationTests
add_to_config "config.action_controller.perform_caching = false"
add_to_env_config "production", "config.assets.compile = true"
- ENV["RAILS_ENV"] = "production"
- require "#{app_path}/config/environment"
+ # Load app env
+ app "production"
assert_equal Sprockets::CachedEnvironment, Rails.application.assets.class
end
@@ -211,8 +220,8 @@ module ApplicationTests
app_file "app/assets/javascripts/application.js", "alert();"
precompile!
- manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first
+ manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first
assets = ActiveSupport::JSON.decode(File.read(manifest))
assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"])
assert_match(/application-([0-z]+)\.css/, assets["assets"]["application.css"])
@@ -233,14 +242,14 @@ module ApplicationTests
app_file "app/assets/javascripts/application.js", "alert();"
add_to_env_config "production", "config.serve_static_files = true"
- ENV["RAILS_ENV"] = "production"
- precompile!
+ precompile! RAILS_ENV: 'production'
manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first
assets = ActiveSupport::JSON.decode(File.read(manifest))
asset_path = assets["assets"]["application.js"]
- require "#{app_path}/config/environment"
+ # Load app env
+ app "production"
# Checking if Uglifier is defined we can know if Sprockets was reached or not
assert !defined?(Uglifier)
@@ -249,12 +258,11 @@ module ApplicationTests
assert !defined?(Uglifier)
end
- test "precompile properly refers files referenced with asset_path and runs in the provided RAILS_ENV" do
+ test "precompile properly refers files referenced with asset_path" do
app_file "app/assets/images/rails.png", "notactuallyapng"
app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }"
- add_to_env_config "test", "config.assets.digest = true"
- precompile!('RAILS_ENV=test')
+ precompile!
file = Dir["#{app_path}/public/assets/application-*.css"].first
assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file))
@@ -265,8 +273,7 @@ module ApplicationTests
app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }"
- ENV["RAILS_ENV"] = "production"
- precompile!
+ precompile! RAILS_ENV: 'production'
manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first
assets = ActiveSupport::JSON.decode(File.read(manifest))
@@ -275,8 +282,8 @@ module ApplicationTests
app_file "app/assets/images/rails.png", "p { url: change }"
precompile!
- assets = ActiveSupport::JSON.decode(File.read(manifest))
+ assets = ActiveSupport::JSON.decode(File.read(manifest))
assert_not_equal asset_path, assets["assets"]["application.css"]
end
@@ -284,8 +291,7 @@ module ApplicationTests
app_file "app/assets/images/rails.png", "notactuallyapng"
app_file "app/assets/stylesheets/application.css.erb", "p { background-image: url(<%= asset_path('rails.png') %>) }"
- ENV["RAILS_ENV"] = "production"
- precompile!
+ precompile! RAILS_ENV: 'production'
file = Dir["#{app_path}/public/assets/application-*.css"].first
assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file))
@@ -294,7 +300,7 @@ module ApplicationTests
test "precompile should handle utf8 filenames" do
filename = "レイルズ.png"
app_file "app/assets/images/#{filename}", "not an image really"
- app_file "app/assets/manifest.js", "//= link_tree ./images"
+ app_file "app/assets/config/manifest.js", "//= link_tree ../images"
add_to_config "config.assets.precompile = %w(manifest.js)"
precompile!
@@ -303,7 +309,8 @@ module ApplicationTests
assets = ActiveSupport::JSON.decode(File.read(manifest))
assert asset_path = assets["assets"].find { |(k, _)| k && k =~ /.png/ }[1]
- require "#{app_path}/config/environment"
+ # Load app env
+ app "development"
get "/assets/#{URI.parser.escape(asset_path)}"
assert_match "not an image really", last_response.body
@@ -326,8 +333,8 @@ module ApplicationTests
app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();"
add_to_config "config.assets.compile = false"
- ENV["RAILS_ENV"] = "production"
- require "#{app_path}/config/environment"
+ # Load app env
+ app "production"
get "/assets/demo.js"
assert_equal 404, last_response.status
@@ -344,7 +351,8 @@ module ApplicationTests
add_to_env_config "development", "config.assets.digest = false"
- require "#{app_path}/config/environment"
+ # Load app env
+ app "development"
class ::OmgController < ActionController::Base
def index
@@ -370,7 +378,8 @@ module ApplicationTests
add_to_env_config "development", "config.assets.digest = false"
- require "#{app_path}/config/environment"
+ # Load app env
+ app "development"
get "/assets/demo.js"
assert_match "alert();", last_response.body
@@ -381,10 +390,10 @@ module ApplicationTests
app_with_assets_in_view
# config.assets.debug and config.assets.compile are false for production environment
- ENV["RAILS_ENV"] = "production"
- precompile!
+ precompile! RAILS_ENV: 'production'
- require "#{app_path}/config/environment"
+ # Load app env
+ app "production"
class ::PostsController < ActionController::Base ; end
@@ -400,6 +409,7 @@ module ApplicationTests
app_file "app/assets/javascripts/xmlhr.js.erb", "<%= Post.name %>"
precompile!
+
assert_equal "Post\n;\n", File.read(Dir["#{app_path}/public/assets/application-*.js"].first)
end
@@ -441,7 +451,10 @@ module ApplicationTests
app_with_assets_in_view
add_to_config "config.asset_host = 'example.com'"
add_to_env_config "development", "config.assets.digest = false"
- require "#{app_path}/config/environment"
+
+ # Load app env
+ app "development"
+
class ::PostsController < ActionController::Base; end
get '/posts', {}, {'HTTPS'=>'off'}
@@ -456,6 +469,7 @@ module ApplicationTests
add_to_config "config.assets.precompile = %w{rails.png image_loader.js}"
add_to_config "config.asset_host = 'example.com'"
add_to_env_config "development", "config.assets.digest = false"
+
precompile!
assert_match "src='//example.com/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/image_loader-*.js"].first)
@@ -467,6 +481,7 @@ module ApplicationTests
app_file "app/assets/javascripts/app.js.erb", "var src='<%= image_path('rails.png') %>';"
add_to_config "config.assets.precompile = %w{rails.png app.js}"
add_to_env_config "development", "config.assets.digest = false"
+
precompile!
assert_match "src='/sub/uri/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/app-*.js"].first)
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index f677a7c42a..2f407cd851 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -34,8 +34,19 @@ module ApplicationTests
FileUtils.cp_r(app_path, new_app)
end
- def app
- @app ||= Rails.application
+ def app(env = 'development')
+ @app ||= begin
+ ENV['RAILS_ENV'] = env
+
+ # FIXME: shush Sass warning spam, not relevant to testing Railties
+ Kernel.silence_warnings do
+ require "#{app_path}/config/environment"
+ end
+
+ Rails.application
+ ensure
+ ENV.delete 'RAILS_ENV'
+ end
end
def setup
@@ -78,7 +89,9 @@ module ApplicationTests
require 'my_logger'
config.logger = MyLogger.new STDOUT
RUBY
- require "#{app_path}/config/environment"
+
+ app 'development'
+
assert_equal 'MyLogger', Rails.application.config.logger.class.name
end
@@ -97,7 +110,7 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
ActiveRecord::Migrator.migrations_paths = ["#{app_path}/db/migrate"]
@@ -128,29 +141,29 @@ module ApplicationTests
test "Rails.application is nil until app is initialized" do
require 'rails'
assert_nil Rails.application
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal AppTemplate::Application.instance, Rails.application
end
test "Rails.application responds to all instance methods" do
- require "#{app_path}/config/environment"
+ app 'development'
assert_respond_to Rails.application, :routes_reloader
assert_equal Rails.application.routes_reloader, AppTemplate::Application.routes_reloader
end
test "Rails::Application responds to paths" do
- require "#{app_path}/config/environment"
+ app 'development'
assert_respond_to AppTemplate::Application, :paths
assert_equal ["#{app_path}/app/views"], AppTemplate::Application.paths["app/views"].expanded
end
test "the application root is set correctly" do
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal Pathname.new(app_path), Rails.application.root
end
test "the application root can be seen from the application singleton" do
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal Pathname.new(app_path), AppTemplate::Application.root
end
@@ -162,7 +175,8 @@ module ApplicationTests
use_frameworks []
- require "#{app_path}/config/environment"
+ app 'development'
+
assert_equal Pathname.new(new_app), Rails.application.root
end
@@ -172,7 +186,7 @@ module ApplicationTests
use_frameworks []
Dir.chdir("#{app_path}") do
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal Pathname.new("#{app_path}"), Rails.application.root
end
end
@@ -181,7 +195,9 @@ module ApplicationTests
add_to_config <<-RUBY
config.root = "#{app_path}"
RUBY
- require "#{app_path}/config/environment"
+
+ app 'development'
+
assert_instance_of Pathname, Rails.root
end
@@ -189,7 +205,9 @@ module ApplicationTests
add_to_config <<-RUBY
config.paths["public"] = "somewhere"
RUBY
- require "#{app_path}/config/environment"
+
+ app 'development'
+
assert_instance_of Pathname, Rails.public_path
end
@@ -199,12 +217,13 @@ module ApplicationTests
config.cache_classes = true
RUBY
- require "#{app_path}/config/application"
- assert Rails.application.initialize!
+ app 'development'
+
+ assert_equal :require, ActiveSupport::Dependencies.mechanism
end
test "application is always added to eager_load namespaces" do
- require "#{app_path}/config/application"
+ app 'development'
assert_includes Rails.application.config.eager_load_namespaces, AppTemplate::Application
end
@@ -218,7 +237,7 @@ module ApplicationTests
use_frameworks []
assert_nothing_raised do
- require "#{app_path}/config/application"
+ app 'development'
end
end
@@ -230,7 +249,7 @@ module ApplicationTests
RUBY
assert_nothing_raised do
- require "#{app_path}/config/application"
+ app 'development'
end
end
@@ -239,7 +258,7 @@ module ApplicationTests
Rails.application.config.filter_parameters += [ :password, :foo, 'bar' ]
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal [:password, :foo, 'bar'], Rails.application.env_config['action_dispatch.parameter_filter']
end
@@ -255,7 +274,7 @@ module ApplicationTests
assert !$prepared
- require "#{app_path}/config/environment"
+ app 'development'
get "/"
assert $prepared
@@ -267,7 +286,7 @@ module ApplicationTests
end
test "skipping config.encoding still results in 'utf-8' as the default" do
- require "#{app_path}/config/application"
+ app 'development'
assert_utf8
end
@@ -276,7 +295,7 @@ module ApplicationTests
config.encoding = "utf-8"
RUBY
- require "#{app_path}/config/application"
+ app 'development'
assert_utf8
end
@@ -285,7 +304,7 @@ module ApplicationTests
config.paths["public"] = "somewhere"
RUBY
- require "#{app_path}/config/application"
+ app 'development'
assert_equal Pathname.new(app_path).join("somewhere"), Rails.public_path
end
@@ -293,7 +312,7 @@ module ApplicationTests
restore_default_config
with_rails_env "production" do
- require "#{app_path}/config/environment"
+ app 'production'
assert_not app.config.serve_static_files
end
end
@@ -303,7 +322,7 @@ module ApplicationTests
with_rails_env "production" do
switch_env "RAILS_SERVE_STATIC_FILES", "1" do
- require "#{app_path}/config/environment"
+ app 'production'
assert app.config.serve_static_files
end
end
@@ -314,7 +333,7 @@ module ApplicationTests
with_rails_env "production" do
switch_env "RAILS_SERVE_STATIC_FILES", " " do
- require "#{app_path}/config/environment"
+ app 'production'
assert_not app.config.serve_static_files
end
end
@@ -363,8 +382,8 @@ module ApplicationTests
development:
secret_key_base:
YAML
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal app.env_config['action_dispatch.key_generator'], Rails.application.key_generator
assert_equal app.env_config['action_dispatch.key_generator'].class, ActiveSupport::LegacyKeyGenerator
@@ -380,7 +399,8 @@ module ApplicationTests
development:
secret_key_base:
YAML
- require "#{app_path}/config/environment"
+
+ app 'development'
assert_deprecated(/You didn't set `secret_key_base`./) do
app.env_config
@@ -395,7 +415,8 @@ module ApplicationTests
development:
secret_token: 3b7cd727ee24e8444053437c36cc66c3
YAML
- require "#{app_path}/config/environment"
+
+ app 'development'
assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_token
end
@@ -426,7 +447,7 @@ module ApplicationTests
secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
YAML
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base
end
@@ -436,7 +457,7 @@ module ApplicationTests
Rails.application.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c3"
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base
end
@@ -449,7 +470,8 @@ module ApplicationTests
secret_key_base:
secret_token:
YAML
- require "#{app_path}/config/environment"
+
+ app 'development'
assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.secrets.secret_token
assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token
@@ -463,7 +485,8 @@ module ApplicationTests
aws_secret_access_key: myamazonsecretaccesskey
YAML
- require "#{app_path}/config/environment"
+ app 'development'
+
assert_equal 'myamazonaccesskeyid', app.secrets.aws_access_key_id
assert_equal 'myamazonsecretaccesskey', app.secrets.aws_secret_access_key
end
@@ -471,7 +494,8 @@ module ApplicationTests
test "blank config/secrets.yml does not crash the loading process" do
app_file 'config/secrets.yml', <<-YAML
YAML
- require "#{app_path}/config/environment"
+
+ app 'development'
assert_nil app.secrets.not_defined
end
@@ -484,7 +508,8 @@ module ApplicationTests
development:
secret_key_base:
YAML
- require "#{app_path}/config/environment"
+
+ app 'development'
assert_equal "iaminallyoursecretkeybase", app.secrets.secret_key_base
end
@@ -497,7 +522,8 @@ module ApplicationTests
development:
secret_key_base:
YAML
- require "#{app_path}/config/environment"
+
+ app 'development'
assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token
assert_equal nil, app.secrets.secret_key_base
@@ -512,7 +538,8 @@ module ApplicationTests
development:
secret_key_base:
YAML
- require "#{app_path}/config/environment"
+
+ app 'development'
assert_equal '', app.config.secret_token
assert_equal nil, app.secrets.secret_key_base
@@ -535,7 +562,6 @@ module ApplicationTests
end
test "default form builder specified as a string" do
-
app_file 'config/initializers/form_builder.rb', <<-RUBY
class CustomFormBuilder < ActionView::Helpers::FormBuilder
def text_field(attribute, *args)
@@ -567,7 +593,7 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
get "/posts"
assert_match(/label/, last_response.body)
@@ -606,9 +632,9 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
- params = {authenticity_token: token}
+ params = { authenticity_token: token }
get "/posts/1"
assert_match(/patch/, last_response.body)
@@ -659,9 +685,9 @@ module ApplicationTests
config.action_mailer.interceptors = MyMailInterceptor
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app 'development'
+ require "mail"
_ = ActionMailer::Base
assert_equal [::MyMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors")
@@ -672,9 +698,9 @@ module ApplicationTests
config.action_mailer.interceptors = [MyMailInterceptor, "MyOtherMailInterceptor"]
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app 'development'
+ require "mail"
_ = ActionMailer::Base
assert_equal [::MyMailInterceptor, ::MyOtherMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors")
@@ -685,9 +711,9 @@ module ApplicationTests
config.action_mailer.preview_interceptors = MyPreviewMailInterceptor
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app 'development'
+ require "mail"
_ = ActionMailer::Base
assert_equal [ActionMailer::InlinePreviewInterceptor, ::MyPreviewMailInterceptor], ActionMailer::Base.preview_interceptors
@@ -698,9 +724,9 @@ module ApplicationTests
config.action_mailer.preview_interceptors = [MyPreviewMailInterceptor, "MyOtherPreviewMailInterceptor"]
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app 'development'
+ require "mail"
_ = ActionMailer::Base
assert_equal [ActionMailer::InlinePreviewInterceptor, MyPreviewMailInterceptor, MyOtherPreviewMailInterceptor], ActionMailer::Base.preview_interceptors
@@ -711,9 +737,9 @@ module ApplicationTests
ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor)
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app 'development'
+ require "mail"
_ = ActionMailer::Base
assert_equal [], ActionMailer::Base.preview_interceptors
@@ -724,9 +750,9 @@ module ApplicationTests
config.action_mailer.observers = MyMailObserver
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app 'development'
+ require "mail"
_ = ActionMailer::Base
assert_equal [::MyMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers")
@@ -737,9 +763,9 @@ module ApplicationTests
config.action_mailer.observers = [MyMailObserver, "MyOtherMailObserver"]
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app 'development'
+ require "mail"
_ = ActionMailer::Base
assert_equal [::MyMailObserver, ::MyOtherMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers")
@@ -750,9 +776,9 @@ module ApplicationTests
config.action_mailer.deliver_later_queue_name = 'test_default'
RUBY
- require "#{app_path}/config/environment"
- require "mail"
+ app 'development'
+ require "mail"
_ = ActionMailer::Base
assert_equal 'test_default', ActionMailer::Base.send(:class_variable_get, "@@deliver_later_queue_name")
@@ -764,7 +790,7 @@ module ApplicationTests
config.time_zone = "Wellington"
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal "Wellington", Rails.application.config.time_zone
end
@@ -776,7 +802,7 @@ module ApplicationTests
RUBY
assert_raise(ArgumentError) do
- require "#{app_path}/config/environment"
+ app 'development'
end
end
@@ -786,7 +812,7 @@ module ApplicationTests
config.beginning_of_week = :wednesday
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal :wednesday, Rails.application.config.beginning_of_week
end
@@ -798,13 +824,14 @@ module ApplicationTests
RUBY
assert_raise(ArgumentError) do
- require "#{app_path}/config/environment"
+ app 'development'
end
end
test "config.action_view.cache_template_loading with cache_classes default" do
add_to_config "config.cache_classes = true"
- require "#{app_path}/config/environment"
+
+ app 'development'
require 'action_view/base'
assert_equal true, ActionView::Resolver.caching?
@@ -812,7 +839,8 @@ module ApplicationTests
test "config.action_view.cache_template_loading without cache_classes default" do
add_to_config "config.cache_classes = false"
- require "#{app_path}/config/environment"
+
+ app 'development'
require 'action_view/base'
assert_equal false, ActionView::Resolver.caching?
@@ -823,7 +851,8 @@ module ApplicationTests
config.cache_classes = true
config.action_view.cache_template_loading = false
RUBY
- require "#{app_path}/config/environment"
+
+ app 'development'
require 'action_view/base'
assert_equal false, ActionView::Resolver.caching?
@@ -834,7 +863,8 @@ module ApplicationTests
config.cache_classes = false
config.action_view.cache_template_loading = true
RUBY
- require "#{app_path}/config/environment"
+
+ app 'development'
require 'action_view/base'
assert_equal true, ActionView::Resolver.caching?
@@ -849,7 +879,7 @@ module ApplicationTests
require 'action_view/railtie'
require 'action_view/base'
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal false, ActionView::Resolver.caching?
end
@@ -902,7 +932,7 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
post "/posts.json", '{ "title": "foo", "name": "bar" }', "CONTENT_TYPE" => "application/json"
assert_equal '{"title"=>"foo"}', last_response.body
@@ -924,7 +954,7 @@ module ApplicationTests
config.action_controller.permit_all_parameters = true
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
post "/posts", {post: {"title" =>"zomg"}}
assert_equal 'permitted', last_response.body
@@ -946,7 +976,7 @@ module ApplicationTests
config.action_controller.action_on_unpermitted_parameters = :raise
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters
@@ -955,7 +985,7 @@ module ApplicationTests
end
test "config.action_controller.always_permitted_parameters are: controller, action by default" do
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal %w(controller action), ActionController::Parameters.always_permitted_parameters
end
@@ -963,7 +993,9 @@ module ApplicationTests
add_to_config <<-RUBY
config.action_controller.always_permitted_parameters = %w( controller action format )
RUBY
- require "#{app_path}/config/environment"
+
+ app 'development'
+
assert_equal %w( controller action format ), ActionController::Parameters.always_permitted_parameters
end
@@ -984,7 +1016,7 @@ module ApplicationTests
config.action_controller.action_on_unpermitted_parameters = :raise
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters
@@ -993,25 +1025,19 @@ module ApplicationTests
end
test "config.action_controller.action_on_unpermitted_parameters is :log by default on development" do
- ENV["RAILS_ENV"] = "development"
-
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters
end
test "config.action_controller.action_on_unpermitted_parameters is :log by default on test" do
- ENV["RAILS_ENV"] = "test"
-
- require "#{app_path}/config/environment"
+ app 'test'
assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters
end
test "config.action_controller.action_on_unpermitted_parameters is false by default on production" do
- ENV["RAILS_ENV"] = "production"
-
- require "#{app_path}/config/environment"
+ app 'production'
assert_equal false, ActionController::Parameters.action_on_unpermitted_parameters
end
@@ -1089,17 +1115,14 @@ module ApplicationTests
test "config.active_record.dump_schema_after_migration is false on production" do
build_app
- ENV["RAILS_ENV"] = "production"
- require "#{app_path}/config/environment"
+ app 'production'
assert_not ActiveRecord::Base.dump_schema_after_migration
end
test "config.active_record.dump_schema_after_migration is true by default on development" do
- ENV["RAILS_ENV"] = "development"
-
- require "#{app_path}/config/environment"
+ app 'development'
assert ActiveRecord::Base.dump_schema_after_migration
end
@@ -1125,7 +1148,7 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
assert_not Rails.configuration.ran_block
require 'rake'
@@ -1147,7 +1170,7 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
assert_not Rails.configuration.ran_block
Rails.application.load_generators
@@ -1165,7 +1188,7 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
assert_not Rails.configuration.ran_block
Rails.application.load_console
@@ -1183,7 +1206,7 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
assert_not Rails.configuration.ran_block
Rails.application.load_runner
@@ -1199,14 +1222,14 @@ module ApplicationTests
end
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
assert_kind_of Hash, Rails.application.config.database_configuration
end
test 'raises with proper error message if no database configuration found' do
FileUtils.rm("#{app_path}/config/database.yml")
- require "#{app_path}/config/environment"
+ app 'development'
err = assert_raises RuntimeError do
Rails.application.config.database_configuration
end
@@ -1214,25 +1237,23 @@ module ApplicationTests
end
test 'config.action_mailer.show_previews defaults to true in development' do
- Rails.env = "development"
- require "#{app_path}/config/environment"
+ app 'development'
assert Rails.application.config.action_mailer.show_previews
end
test 'config.action_mailer.show_previews defaults to false in production' do
- Rails.env = "production"
- require "#{app_path}/config/environment"
+ app 'production'
assert_equal false, Rails.application.config.action_mailer.show_previews
end
test 'config.action_mailer.show_previews can be set in the configuration file' do
- Rails.env = "production"
add_to_config <<-RUBY
config.action_mailer.show_previews = true
RUBY
- require "#{app_path}/config/environment"
+
+ app 'production'
assert_equal true, Rails.application.config.action_mailer.show_previews
end
@@ -1247,7 +1268,7 @@ module ApplicationTests
config.my_custom_config = config_for('custom')
RUBY
- require "#{app_path}/config/environment"
+ app 'development'
assert_equal 'custom key', Rails.application.config.my_custom_config['key']
end
@@ -1258,7 +1279,7 @@ module ApplicationTests
RUBY
exception = assert_raises(RuntimeError) do
- require "#{app_path}/config/environment"
+ app 'development'
end
assert_equal "Could not load configuration. No such file - #{app_path}/config/custom.yml", exception.message
@@ -1273,7 +1294,8 @@ module ApplicationTests
add_to_config <<-RUBY
config.my_custom_config = config_for('custom')
RUBY
- require "#{app_path}/config/environment"
+
+ app 'development'
assert_equal({}, Rails.application.config.my_custom_config)
end
@@ -1285,7 +1307,8 @@ module ApplicationTests
add_to_config <<-RUBY
config.my_custom_config = config_for('custom')
RUBY
- require "#{app_path}/config/environment"
+
+ app 'development'
assert_equal({}, Rails.application.config.my_custom_config)
end
@@ -1299,12 +1322,13 @@ module ApplicationTests
add_to_config <<-RUBY
config.my_custom_config = config_for('custom')
RUBY
- require "#{app_path}/config/environment"
+
+ app 'development'
assert_equal 'custom key', Rails.application.config.my_custom_config['key']
end
- test "config_for with syntax error show a more descritive exception" do
+ test "config_for with syntax error show a more descriptive exception" do
app_file 'config/custom.yml', <<-RUBY
development:
key: foo:
@@ -1315,7 +1339,7 @@ module ApplicationTests
RUBY
exception = assert_raises(RuntimeError) do
- require "#{app_path}/config/environment"
+ app 'development'
end
assert_match 'YAML syntax error occurred while parsing', exception.message
diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb
index d298e8d632..138c63266e 100644
--- a/railties/test/application/middleware_test.rb
+++ b/railties/test/application/middleware_test.rb
@@ -43,7 +43,6 @@ module ApplicationTests
"ActionDispatch::Cookies",
"ActionDispatch::Session::CookieStore",
"ActionDispatch::Flash",
- "ActionDispatch::ParamsParser",
"Rack::Head",
"Rack::ConditionalGet",
"Rack::ETag"
@@ -70,7 +69,6 @@ module ApplicationTests
"ActionDispatch::Callbacks",
"ActiveRecord::ConnectionAdapters::ConnectionManagement",
"ActiveRecord::QueryCache",
- "ActionDispatch::ParamsParser",
"Rack::Head",
"Rack::ConditionalGet",
"Rack::ETag"
diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb
index cbada6be97..0777714d35 100644
--- a/railties/test/application/routing_test.rb
+++ b/railties/test/application/routing_test.rb
@@ -21,6 +21,12 @@ module ApplicationTests
assert_equal 200, last_response.status
end
+ test "rails/info in development" do
+ app("development")
+ get "/rails/info"
+ assert_equal 302, last_response.status
+ end
+
test "rails/info/routes in development" do
app("development")
get "/rails/info/routes"
@@ -63,6 +69,12 @@ module ApplicationTests
assert_equal 404, last_response.status
end
+ test "rails/info in production" do
+ app("production")
+ get "/rails/info"
+ assert_equal 404, last_response.status
+ end
+
test "rails/info/routes in production" do
app("production")
get "/rails/info/routes"
diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb
index 2d47a31826..acfba21f1c 100644
--- a/railties/test/application/test_runner_test.rb
+++ b/railties/test/application/test_runner_test.rb
@@ -340,6 +340,36 @@ module ApplicationTests
assert_match '0 runs, 0 assertions', run_test_command('')
end
+ def test_output_inline_by_default
+ app_file 'test/models/post_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class PostTest < ActiveSupport::TestCase
+ def test_post
+ assert false, 'wups!'
+ end
+ end
+ RUBY
+
+ output = run_test_command('test/models/post_test.rb')
+ assert_match %r{Running:\n\nF\n\nwups!\n\nbin/rails test test/models/post_test.rb:4}, output
+ end
+
+ def test_fail_fast
+ app_file 'test/models/post_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class PostTest < ActiveSupport::TestCase
+ def test_post
+ assert false, 'wups!'
+ end
+ end
+ RUBY
+
+ assert_match(/Interrupt/,
+ capture(:stderr) { run_test_command('test/models/post_test.rb --fail-fast') })
+ end
+
def test_raise_error_when_specified_file_does_not_exist
error = capture(:stderr) { run_test_command('test/not_exists.rb') }
assert_match(%r{cannot load such file.+test/not_exists\.rb}, error)
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index daf362357c..e5f10a89d3 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -1,7 +1,6 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/app/app_generator'
require 'generators/shared_generator_tests'
-require 'mocha/setup' # FIXME: stop using mocha
DEFAULT_APP_FILES = %w(
.gitignore
@@ -117,35 +116,33 @@ class AppGeneratorTest < Rails::Generators::TestCase
run_generator [app_root]
- Rails.application.config.root = app_moved_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
+ stub_rails_application(app_moved_root) do
+ Rails.application.stub(:is_a?, -> *args { Rails::Application }) do
+ FileUtils.mv(app_root, app_moved_root)
- FileUtils.mv(app_root, app_moved_root)
+ # make sure we are in correct dir
+ FileUtils.cd(app_moved_root)
- # make sure we are in correct dir
- FileUtils.cd(app_moved_root)
-
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true },
- destination_root: app_moved_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/
- assert_file "myapp_moved/config/initializers/session_store.rb", /_myapp_session/
+ generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true },
+ destination_root: app_moved_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/
+ assert_file "myapp_moved/config/initializers/session_store.rb", /_myapp_session/
+ end
+ end
end
def test_rails_update_generates_correct_session_key
app_root = File.join(destination_root, 'myapp')
run_generator [app_root]
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
-
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_file "myapp/config/initializers/session_store.rb", /_myapp_session/
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file "myapp/config/initializers/session_store.rb", /_myapp_session/
+ end
end
def test_new_application_use_json_serialzier
@@ -158,14 +155,12 @@ class AppGeneratorTest < Rails::Generators::TestCase
app_root = File.join(destination_root, 'myapp')
run_generator [app_root]
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
-
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/)
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/)
+ end
end
def test_rails_update_does_not_create_callback_terminator_initializer
@@ -174,14 +169,12 @@ class AppGeneratorTest < Rails::Generators::TestCase
FileUtils.rm("#{app_root}/config/initializers/callback_terminator.rb")
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
-
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_no_file "#{app_root}/config/initializers/callback_terminator.rb"
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_no_file "#{app_root}/config/initializers/callback_terminator.rb"
+ end
end
def test_rails_update_does_not_remove_callback_terminator_initializer_if_already_present
@@ -190,14 +183,12 @@ class AppGeneratorTest < Rails::Generators::TestCase
FileUtils.touch("#{app_root}/config/initializers/callback_terminator.rb")
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
-
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_file "#{app_root}/config/initializers/callback_terminator.rb"
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file "#{app_root}/config/initializers/callback_terminator.rb"
+ end
end
def test_rails_update_set_the_cookie_serializer_to_marchal_if_it_is_not_already_configured
@@ -206,14 +197,12 @@ class AppGeneratorTest < Rails::Generators::TestCase
FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb")
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
-
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/)
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/)
+ end
end
def test_rails_update_does_not_create_active_record_belongs_to_required_by_default
@@ -222,14 +211,12 @@ class AppGeneratorTest < Rails::Generators::TestCase
FileUtils.rm("#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb")
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
-
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_no_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb"
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_no_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb"
+ end
end
def test_rails_update_does_not_remove_active_record_belongs_to_required_by_default_if_already_present
@@ -238,14 +225,12 @@ class AppGeneratorTest < Rails::Generators::TestCase
FileUtils.touch("#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb")
- Rails.application.config.root = app_root
- Rails.application.class.stubs(:name).returns("Myapp")
- Rails.application.stubs(:is_a?).returns(Rails::Application)
-
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
- generator.send(:app_const)
- quietly { generator.send(:update_config_files) }
- assert_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb"
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb"
+ end
end
def test_application_names_are_not_singularized
@@ -456,13 +441,15 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_usage_read_from_file
- File.expects(:read).returns("USAGE FROM FILE")
- assert_equal "USAGE FROM FILE", Rails::Generators::AppGenerator.desc
+ assert_called(File, :read, returns: "USAGE FROM FILE") do
+ assert_equal "USAGE FROM FILE", Rails::Generators::AppGenerator.desc
+ end
end
def test_default_usage
- Rails::Generators::AppGenerator.expects(:usage_path).returns(nil)
- assert_match(/Create rails files for app generator/, Rails::Generators::AppGenerator.desc)
+ assert_called(Rails::Generators::AppGenerator, :usage_path, returns: nil) do
+ assert_match(/Create rails files for app generator/, Rails::Generators::AppGenerator.desc)
+ end
end
def test_default_namespace
@@ -538,18 +525,31 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_spring_binstubs
jruby_skip "spring doesn't run on JRuby"
- generator.stubs(:bundle_command).with('install')
- generator.expects(:bundle_command).with('exec spring binstub --all').once
- quietly { generator.invoke_all }
+ command_check = -> command do
+ @binstub_called ||= 0
+
+ case command
+ when 'install'
+ # Called when running bundle, we just want to stub it so nothing to do here.
+ when 'exec spring binstub --all'
+ @binstub_called += 1
+ assert_equal 1, @binstub_called, "exec spring binstub --all expected to be called once, but was called #{@install_called} times."
+ end
+ end
+
+ generator.stub :bundle_command, command_check do
+ quietly { generator.invoke_all }
+ end
end
def test_spring_no_fork
jruby_skip "spring doesn't run on JRuby"
- Process.stubs(:respond_to?).with(:fork).returns(false)
- run_generator
+ assert_called_with(Process, :respond_to?, [:fork], returns: false) do
+ run_generator
- assert_file "Gemfile" do |content|
- assert_no_match(/spring/, content)
+ assert_file "Gemfile" do |content|
+ assert_no_match(/spring/, content)
+ end
end
end
@@ -651,18 +651,37 @@ class AppGeneratorTest < Rails::Generators::TestCase
template = %{ after_bundle { run 'echo ran after_bundle' } }
template.instance_eval "def read; self; end" # Make the string respond to read
- generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template)
+ check_open = -> *args do
+ assert_equal [ path, 'Accept' => 'application/x-thor-template' ], args
+ template
+ end
- bundler_first = sequence('bundle, binstubs, after_bundle')
- generator.expects(:bundle_command).with('install').once.in_sequence(bundler_first)
- generator.expects(:bundle_command).with('exec spring binstub --all').in_sequence(bundler_first)
- generator.expects(:run).with('echo ran after_bundle').in_sequence(bundler_first)
+ sequence = ['install', 'exec spring binstub --all', 'echo ran after_bundle']
+ ensure_bundler_first = -> command do
+ @sequence_step ||= 0
- quietly { generator.invoke_all }
+ assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}"
+ @sequence_step += 1
+ end
+
+ generator([destination_root], template: path).stub(:open, check_open, template) do
+ generator.stub(:bundle_command, ensure_bundler_first) do
+ generator.stub(:run, ensure_bundler_first) do
+ quietly { generator.invoke_all }
+ end
+ end
+ end
end
protected
+ def stub_rails_application(root)
+ Rails.application.config.root = root
+ Rails.application.class.stub(:name, "Myapp") do
+ yield
+ end
+ end
+
def action(*args, &block)
capture(:stdout) { generator.send(*args, &block) }
end
diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb
index e839b67960..c4ee6602c5 100644
--- a/railties/test/generators/namespaced_generators_test.rb
+++ b/railties/test/generators/namespaced_generators_test.rb
@@ -281,6 +281,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Controller
assert_file "app/controllers/test_app/admin/roles_controller.rb" do |content|
assert_match(/module TestApp\n class Admin::RolesController < ApplicationController/, content)
+ assert_match(%r(require_dependency "test_app/application_controller"), content)
end
assert_file "test/controllers/test_app/admin/roles_controller_test.rb",
diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb
index afccf9d885..0625e5fbd7 100644
--- a/railties/test/generators/plugin_generator_test.rb
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -1,7 +1,6 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/plugin/plugin_generator'
require 'generators/shared_generator_tests'
-require 'mocha/setup' # FIXME: stop using mocha
DEFAULT_PLUGIN_FILES = %w(
.gitignore
diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb
index 77372fb514..acb78ec888 100644
--- a/railties/test/generators/shared_generator_tests.rb
+++ b/railties/test/generators/shared_generator_tests.rb
@@ -28,9 +28,22 @@ module SharedGeneratorTests
def assert_generates_with_bundler(options = {})
generator([destination_root], options)
- generator.expects(:bundle_command).with('install').once
- generator.stubs(:bundle_command).with('exec spring binstub --all')
- quietly { generator.invoke_all }
+
+ command_check = -> command do
+ @install_called ||= 0
+
+ case command
+ when 'install'
+ @install_called += 1
+ assert_equal 1, @install_called, "install expected to be called once, but was called #{@install_called} times"
+ when 'exec spring binstub --all'
+ # Called when running tests with spring, let through unscathed.
+ end
+ end
+
+ generator.stub :bundle_command, command_check do
+ quietly { generator.invoke_all }
+ end
end
def test_generation_runs_bundle_install
@@ -91,8 +104,14 @@ module SharedGeneratorTests
template = %{ say "It works!" }
template.instance_eval "def read; self; end" # Make the string respond to read
- generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template)
- quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) }
+ check_open = -> *args do
+ assert_equal [ path, 'Accept' => 'application/x-thor-template' ], args
+ template
+ end
+
+ generator([destination_root], template: path).stub(:open, check_open, template) do
+ quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) }
+ end
end
def test_dev_option
@@ -107,18 +126,19 @@ module SharedGeneratorTests
end
def test_skip_gemfile
- generator([destination_root], skip_gemfile: true).expects(:bundle_command).never
- quietly { generator.invoke_all }
- assert_no_file 'Gemfile'
+ assert_not_called(generator([destination_root], skip_gemfile: true), :bundle_command) do
+ quietly { generator.invoke_all }
+ assert_no_file 'Gemfile'
+ end
end
def test_skip_bundle
- generator([destination_root], skip_bundle: true).expects(:bundle_command).never
- quietly { generator.invoke_all }
-
- # skip_bundle is only about running bundle install, ensure the Gemfile is still
- # generated.
- assert_file 'Gemfile'
+ assert_not_called(generator([destination_root], skip_bundle: true), :bundle_command) do
+ quietly { generator.invoke_all }
+ # skip_bundle is only about running bundle install, ensure the Gemfile is still
+ # generated.
+ assert_file 'Gemfile'
+ end
end
def test_skip_git
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index 65d8a55421..df3c2ca66d 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -51,7 +51,12 @@ module TestHelpers
old_env = ENV["RAILS_ENV"]
@app ||= begin
ENV["RAILS_ENV"] = env
- require "#{app_path}/config/environment"
+
+ # FIXME: shush Sass warning spam, not relevant to testing Railties
+ Kernel.silence_warnings do
+ require "#{app_path}/config/environment"
+ end
+
Rails.application
end
ensure
@@ -159,19 +164,20 @@ module TestHelpers
require "rails"
require "action_controller/railtie"
require "action_view/railtie"
+ require 'action_dispatch/middleware/flash'
- app = Class.new(Rails::Application)
- app.config.eager_load = false
- app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
- app.config.session_store :cookie_store, key: "_myapp_session"
- app.config.active_support.deprecation = :log
- app.config.active_support.test_order = :random
- app.config.log_level = :info
+ @app = Class.new(Rails::Application)
+ @app.config.eager_load = false
+ @app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
+ @app.config.session_store :cookie_store, key: "_myapp_session"
+ @app.config.active_support.deprecation = :log
+ @app.config.active_support.test_order = :random
+ @app.config.log_level = :info
- yield app if block_given?
- app.initialize!
+ yield @app if block_given?
+ @app.initialize!
- app.routes.draw do
+ @app.routes.draw do
get "/" => "omg#index"
end
@@ -296,7 +302,10 @@ module TestHelpers
end
def boot_rails
- require File.expand_path('../../../../load_paths', __FILE__)
+ # FIXME: shush Sass warning spam, not relevant to testing Railties
+ Kernel.silence_warnings do
+ require File.expand_path('../../../../load_paths', __FILE__)
+ end
end
end
end
diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb
index 3066ba82d6..59fdf4bc36 100644
--- a/railties/test/test_unit/reporter_test.rb
+++ b/railties/test/test_unit/reporter_test.rb
@@ -57,6 +57,59 @@ class TestUnitReporterTest < ActiveSupport::TestCase
end
end
+ test "outputs failures inline" do
+ @reporter.record(failed_test)
+ @reporter.report
+
+ assert_match %r{\A\n\nboo\n\nbin/rails test .*test/test_unit/reporter_test.rb:6\n\n\z}, @output.string
+ end
+
+ test "outputs errors inline" do
+ @reporter.record(errored_test)
+ @reporter.report
+
+ assert_match %r{\A\n\nArgumentError: wups\n No backtrace\n\nbin/rails test .*test/test_unit/reporter_test.rb:6\n\n\z}, @output.string
+ end
+
+ test "outputs skipped tests inline if verbose" do
+ verbose = Rails::TestUnitReporter.new @output, verbose: true
+ verbose.record(skipped_test)
+ verbose.report
+
+ assert_match %r{\A\n\nskipchurches, misstemples\n\nbin/rails test .*test/test_unit/reporter_test.rb:6\n\n\z}, @output.string
+ end
+
+ test "does not output rerun snippets after run" do
+ @reporter.record(failed_test)
+ @reporter.report
+
+ assert_no_match 'Failed tests:', @output.string
+ end
+
+ test "fail fast interrupts run on failure" do
+ fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true
+ interrupt_raised = false
+
+ # Minitest passes through Interrupt, catch it manually.
+ begin
+ fail_fast.record(failed_test)
+ rescue Interrupt
+ interrupt_raised = true
+ ensure
+ assert interrupt_raised, 'Expected Interrupt to be raised.'
+ end
+ end
+
+ test "fail fast does not interrupt run errors or skips" do
+ fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true
+
+ fail_fast.record(errored_test)
+ assert_no_match 'Failed tests:', @output.string
+
+ fail_fast.record(skipped_test)
+ assert_no_match 'Failed tests:', @output.string
+ end
+
private
def assert_rerun_snippet_count(snippet_count)
assert_equal snippet_count, @output.string.scan(%r{^bin/rails test }).size
@@ -72,6 +125,12 @@ class TestUnitReporterTest < ActiveSupport::TestCase
ft
end
+ def errored_test
+ et = ExampleTest.new(:woot)
+ et.failures << Minitest::UnexpectedError.new(ArgumentError.new("wups"))
+ et
+ end
+
def passing_test
ExampleTest.new(:woot)
end
@@ -79,7 +138,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
def skipped_test
st = ExampleTest.new(:woot)
st.failures << begin
- raise Minitest::Skip
+ raise Minitest::Skip, "skipchurches, misstemples"
rescue Minitest::Assertion => e
e
end