diff options
597 files changed, 7226 insertions, 4024 deletions
diff --git a/.gitignore b/.gitignore index 9268977c2f..4961ad588f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .Gemfile .ruby-version +.byebug_history debug.log pkg /.bundle diff --git a/.rubocop.yml b/.rubocop.yml index 629f5d86ea..0d1d0c36ce 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -33,6 +33,10 @@ Style/EmptyLines: Style/EmptyLinesAroundClassBody: Enabled: true +# In a regular method definition, no empty lines around the body. +Style/EmptyLinesAroundMethodBody: + Enabled: true + # In a regular module definition, no empty lines around the body. Style/EmptyLinesAroundModuleBody: Enabled: true @@ -113,7 +117,7 @@ Style/UnneededPercentQ: # assignments, where it should be aligned with the LHS. Lint/EndAlignment: Enabled: true - AlignWith: variable + EnforcedStyleAlignWith: variable # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. Lint/RequireParentheses: diff --git a/.travis.yml b/.travis.yml index ad65de48aa..eafa06e44f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ addons: bundler_args: --without test --jobs 3 --retry 3 before_install: - "rm ${BUNDLE_GEMFILE}.lock" + - "gem update --system" - "gem update bundler" - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" @@ -63,6 +64,12 @@ matrix: - memcached - redis - rabbitmq + - rvm: 2.4.0 + env: "GEM=aj:integration" + services: + - memcached + - redis + - rabbitmq - rvm: ruby-head env: "GEM=aj:integration" services: @@ -74,17 +81,20 @@ matrix: - "GEM=ar:mysql2 MYSQL=mariadb" addons: mariadb: 10.0 - - rvm: jruby-9.1.5.0 + - rvm: 2.4.0 + env: + - "GEM=ar:sqlite3_mem" + - rvm: jruby-9.1.7.0 jdk: oraclejdk8 env: - "GEM=ap" - - rvm: jruby-9.1.5.0 + - rvm: jruby-9.1.7.0 jdk: oraclejdk8 env: - "GEM=am,aj" allow_failures: - rvm: ruby-head - - rvm: jruby-9.1.5.0 + - rvm: jruby-9.1.7.0 - env: "GEM=ac:integration" fast_finish: true @@ -7,8 +7,6 @@ end gemspec -gem "arel", github: "rails/arel" - # We need a newish Rake since Active Job sets its test tasks' descriptions. gem "rake", ">= 11.1" @@ -16,6 +14,8 @@ gem "rake", ">= 11.1" # be loaded after loading the test library. gem "mocha", "~> 0.14", require: false +gem "capybara", "~> 2.7.0" + gem "rack-cache", "~> 1.2" gem "jquery-rails" gem "coffee-rails" @@ -37,7 +37,7 @@ gem "rb-inotify", github: "matthewd/rb-inotify", branch: "close-handling", requi # Explicitly avoid 1.x that doesn't support Ruby 2.4+ gem "json", ">= 2.0.0" -gem "rubocop", require: false +gem "rubocop", ">= 0.47", require: false group :doc do gem "sdoc", "1.0.0.rc1" @@ -51,13 +51,16 @@ gem "dalli", ">= 2.2.1" gem "listen", ">= 3.0.5", "< 3.2", require: false gem "libxml-ruby", platforms: :ruby +# Action View. For testing Erubis handler deprecation. +gem "erubis", "~> 2.7.0", require: false + # Active Job. group :job do - gem "resque", github: "resque/resque", require: false + gem "resque", require: false gem "resque-scheduler", require: false gem "sidekiq", require: false gem "sucker_punch", require: false - gem "delayed_job", require: false, github: "collectiveidea/delayed_job" + gem "delayed_job", require: false gem "queue_classic", github: "QueueClassic/queue_classic", branch: "master", require: false, platforms: :ruby gem "sneakers", require: false gem "que", require: false @@ -65,7 +68,7 @@ group :job do #TODO: add qu after it support Rails 5.1 # gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false gem "qu-redis", require: false - gem "delayed_job_active_record", require: false, github: "collectiveidea/delayed_job_active_record" + gem "delayed_job_active_record", require: false gem "sequel", require: false end diff --git a/Gemfile.lock b/Gemfile.lock index f256186e29..ffecf4c519 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,21 +7,6 @@ GIT pg (>= 0.17, < 0.20) GIT - remote: https://github.com/collectiveidea/delayed_job.git - revision: e3772d4f0c8470d0fcba00c86ca3bc4f5e876830 - specs: - delayed_job (4.1.2) - activesupport (>= 3.0, < 5.1) - -GIT - remote: https://github.com/collectiveidea/delayed_job_active_record.git - revision: 36f434c4fd660e8f11ce932be117e9c71dde7212 - specs: - delayed_job_active_record (4.1.1) - activerecord (>= 3.0, < 5.1) - delayed_job (>= 3.0, < 5) - -GIT remote: https://github.com/matthewd/rb-inotify.git revision: 90553518d1fb79aedc98a3036c59bd2b6731ac40 branch: close-handling @@ -38,23 +23,6 @@ GIT event_emitter websocket -GIT - remote: https://github.com/rails/arel.git - revision: b26638ef041953992010590b31615c519fa0ea7d - specs: - arel (8.0.0) - -GIT - remote: https://github.com/resque/resque.git - revision: 835a4a7e61a2e33832dbf11975ecfab54af293c6 - specs: - resque (1.27.0) - mono_logger (~> 1.0) - multi_json (~> 1.0) - redis-namespace (~> 1.3) - sinatra (>= 0.9.2) - vegas (~> 0.1.2) - PATH remote: . specs: @@ -78,9 +46,9 @@ PATH actionview (5.1.0.alpha) activesupport (= 5.1.0.alpha) builder (~> 3.1) - erubis (~> 2.7.0) + erubi (~> 1.4) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) + rails-html-sanitizer (~> 1.0, >= 1.0.3) activejob (5.1.0.alpha) activesupport (= 5.1.0.alpha) globalid (>= 0.3.6) @@ -119,7 +87,8 @@ GEM specs: addressable (2.5.0) public_suffix (~> 2.0, >= 2.0.2) - amq-protocol (2.0.1) + amq-protocol (2.1.0) + arel (8.0.0) ast (2.3.0) backburner (1.3.1) beaneater (~> 1.0) @@ -147,10 +116,17 @@ GEM childprocess faraday selenium-webdriver - builder (3.2.2) + builder (3.2.3) bunny (2.6.2) amq-protocol (>= 2.0.1) byebug (9.0.6) + capybara (2.7.1) + addressable + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) childprocess (0.5.9) ffi (~> 1.0, >= 1.0.11) coffee-rails (4.2.1) @@ -167,6 +143,11 @@ GEM daemons (1.2.4) dalli (2.7.6) dante (0.2.0) + delayed_job (4.1.2) + activesupport (>= 3.0, < 5.1) + delayed_job_active_record (4.1.1) + activerecord (>= 3.0, < 5.1) + delayed_job (>= 3.0, < 5) em-hiredis (0.3.1) eventmachine (~> 1.0) hiredis (~> 0.6.0) @@ -178,13 +159,14 @@ GEM http_parser.rb (>= 0.6.0) em-socksify (0.3.1) eventmachine (>= 1.0.0.beta.4) + erubi (1.4.0) erubis (2.7.0) event_emitter (0.2.5) eventmachine (1.2.1) eventmachine (1.2.1-x64-mingw32) eventmachine (1.2.1-x86-mingw32) execjs (2.7.0) - faraday (0.10.0) + faraday (0.11.0) multipart-post (>= 1.2, < 3) faye (1.2.3) cookiejar (>= 0.3.0) @@ -197,19 +179,19 @@ GEM faye-websocket (0.10.5) eventmachine (>= 0.12.0) websocket-driver (>= 0.5.1) - ffi (1.9.14) - ffi (1.9.14-x64-mingw32) - ffi (1.9.14-x86-mingw32) + ffi (1.9.17) + ffi (1.9.17-x64-mingw32) + ffi (1.9.17-x86-mingw32) globalid (0.3.7) activesupport (>= 4.1.0) hiredis (0.6.1) http_parser.rb (0.6.0) - i18n (0.7.0) - jquery-rails (4.2.1) + i18n (0.8.0) + jquery-rails (4.2.2) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) - json (2.0.2) + json (2.0.3) kindlerb (1.2.0) mustache nokogiri @@ -239,21 +221,21 @@ GEM mysql2 (0.4.5-x64-mingw32) mysql2 (0.4.5-x86-mingw32) nio4r (2.0.0) - nokogiri (1.7.0) + nokogiri (1.7.0.1) mini_portile2 (~> 2.1.0) - nokogiri (1.7.0-x64-mingw32) + nokogiri (1.7.0.1-x64-mingw32) mini_portile2 (~> 2.1.0) - nokogiri (1.7.0-x86-mingw32) + nokogiri (1.7.0.1-x86-mingw32) mini_portile2 (~> 2.1.0) - parser (2.3.3.1) + parser (2.4.0.0) ast (~> 2.2) pg (0.19.0) pg (0.19.0-x64-mingw32) pg (0.19.0-x86-mingw32) powerpack (0.1.1) psych (2.2.2) - public_suffix (2.0.4) - puma (3.6.2) + public_suffix (2.0.5) + puma (3.7.0) qu (0.2.0) multi_json qu-redis (0.2.0) @@ -274,7 +256,7 @@ GEM nokogiri (~> 1.6) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - rainbow (2.1.0) + rainbow (2.2.1) rake (12.0.0) rb-fsevent (0.9.8) rdoc (5.0.0) @@ -282,13 +264,19 @@ GEM redis (3.3.2) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) + resque (1.27.0) + mono_logger (~> 1.0) + multi_json (~> 1.0) + redis-namespace (~> 1.3) + sinatra (>= 0.9.2) + vegas (~> 0.1.2) resque-scheduler (4.3.0) mono_logger (~> 1.0) redis (~> 3.3) resque (~> 1.26) rufus-scheduler (~> 3.2) - rubocop (0.46.0) - parser (>= 2.3.1.1, < 3.0) + rubocop (0.47.1) + parser (>= 2.3.3.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) @@ -296,7 +284,7 @@ GEM ruby-progressbar (1.8.1) ruby_dep (1.5.0) rubyzip (1.2.0) - rufus-scheduler (3.3.1) + rufus-scheduler (3.3.2) tzinfo sass (3.4.23) sass-rails (5.0.6) @@ -311,10 +299,10 @@ GEM childprocess (~> 0.5) rubyzip (~> 1.0) websocket (~> 1.0) - sequel (4.41.0) + sequel (4.42.1) serverengine (1.5.11) sigdump (~> 0.2.2) - sidekiq (4.2.7) + sidekiq (4.2.9) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) @@ -335,9 +323,9 @@ GEM actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sqlite3 (1.3.12) - sqlite3 (1.3.12-x64-mingw32) - sqlite3 (1.3.12-x86-mingw32) + sqlite3 (1.3.13) + sqlite3 (1.3.13-x64-mingw32) + sqlite3 (1.3.13-x86-mingw32) stackprof (0.2.10) sucker_punch (2.0.2) concurrent-ruby (~> 1.0.0) @@ -358,7 +346,7 @@ GEM tzinfo (>= 1.0.0) uglifier (3.0.4) execjs (>= 0.3.0, < 3) - unicode-display_width (1.1.2) + unicode-display_width (1.1.3) useragent (0.16.8) vegas (0.1.11) rack (>= 1.0.0) @@ -370,6 +358,8 @@ GEM websocket-driver (0.6.4) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) + xpath (2.0.0) + nokogiri (~> 1.3) PLATFORMS ruby @@ -380,18 +370,19 @@ DEPENDENCIES activerecord-jdbcmysql-adapter (>= 1.3.0) activerecord-jdbcpostgresql-adapter (>= 1.3.0) activerecord-jdbcsqlite3-adapter (>= 1.3.0) - arel! backburner bcrypt (~> 3.1.11) benchmark-ips blade blade-sauce_labs_plugin byebug + capybara (~> 2.7.0) coffee-rails dalli (>= 2.2.1) - delayed_job! - delayed_job_active_record! + delayed_job + delayed_job_active_record em-hiredis + erubis (~> 2.7.0) hiredis jquery-rails json (>= 2.0.0) @@ -415,9 +406,9 @@ DEPENDENCIES rb-inotify! redcarpet (~> 3.2.3) redis - resque! + resque resque-scheduler - rubocop + rubocop (>= 0.47) sass-rails sdoc (= 1.0.0.rc1) sequel @@ -434,4 +425,4 @@ DEPENDENCIES websocket-client-simple! BUNDLED WITH - 1.13.7 + 1.14.4 diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index 4f17274a01..7657a05077 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,3 +1,9 @@ +* Redis subscription adapters now support `channel_prefix` option in `cable.yml` + + Avoids channel name collisions when multiple apps use the same Redis server. + + *Chad Ingram* + * Permit same-origin connections by default. Added new option `config.action_cable.allow_same_origin_as_host = false` diff --git a/actioncable/README.md b/actioncable/README.md index cccb55a196..c55b7dc57b 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -536,6 +536,15 @@ cable.subscriptions.create 'AppearanceChannel', # normal channel code goes here... ``` +## Download and Installation + +The latest version of Action Cable can be installed with [RubyGems](#gem-usage), +or with [npm](#npm-usage). + +Source code can be downloaded as part of the Rails project on GitHub + +* https://github.com/rails/rails/tree/master/actioncable + ## License Action Cable is released under the MIT license: diff --git a/actioncable/app/assets/javascripts/action_cable/connection.coffee b/actioncable/app/assets/javascripts/action_cable/connection.coffee index 29ad676290..7fd68cad2f 100644 --- a/actioncable/app/assets/javascripts/action_cable/connection.coffee +++ b/actioncable/app/assets/javascripts/action_cable/connection.coffee @@ -23,7 +23,7 @@ class ActionCable.Connection open: => if @isActive() ActionCable.log("Attempted to open WebSocket, but existing socket is #{@getState()}") - throw new Error("Existing connection must be closed before opening") + false else ActionCable.log("Opening WebSocket, current state is #{@getState()}, subprotocols: #{protocols}") @uninstallEventHandlers() if @webSocket? diff --git a/actioncable/bin/test b/actioncable/bin/test new file mode 100755 index 0000000000..a7beb14b27 --- /dev/null +++ b/actioncable/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +COMPONENT_ROOT = File.expand_path("..", __dir__) +require File.expand_path("../tools/test", COMPONENT_ROOT) diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb index 6739a62ba0..718f630f58 100644 --- a/actioncable/lib/action_cable/channel/base.rb +++ b/actioncable/lib/action_cable/channel/base.rb @@ -205,7 +205,7 @@ module ActionCable # Transmit a hash of data to the subscriber. The hash will automatically be wrapped in a JSON envelope with # the proper channel identifier marked as the recipient. def transmit(data, via: nil) # :doc: - logger.info "#{self.class.name} transmitting #{data.inspect.truncate(300)}".tap { |m| m << " (via #{via})" if via } + logger.debug "#{self.class.name} transmitting #{data.inspect.truncate(300)}".tap { |m| m << " (via #{via})" if via } payload = { channel_class: self.class.name, data: data, via: via } ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do diff --git a/actioncable/lib/action_cable/connection/client_socket.rb b/actioncable/lib/action_cable/connection/client_socket.rb index 70a2bbecb1..c7e30e78c8 100644 --- a/actioncable/lib/action_cable/connection/client_socket.rb +++ b/actioncable/lib/action_cable/connection/client_socket.rb @@ -90,8 +90,8 @@ module ActionCable reason ||= "" unless code == 1000 || (code >= 3000 && code <= 4999) - raise ArgumentError, "Failed to execute 'close' on WebSocket: " + - "The code must be either 1000, or between 3000 and 4999. " + + raise ArgumentError, "Failed to execute 'close' on WebSocket: " \ + "The code must be either 1000, or between 3000 and 4999. " \ "#{code} is neither." end diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb index abf42c99d5..44bce1e195 100644 --- a/actioncable/lib/action_cable/connection/subscriptions.rb +++ b/actioncable/lib/action_cable/connection/subscriptions.rb @@ -19,7 +19,7 @@ module ActionCable logger.error "Received unrecognized command in #{data.inspect}" end rescue Exception => e - logger.error "Could not execute command from #{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}" + logger.error "Could not execute command from (#{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}" end def add(data) diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb index e23527b84e..63a26636a0 100644 --- a/actioncable/lib/action_cable/engine.rb +++ b/actioncable/lib/action_cable/engine.rb @@ -31,7 +31,7 @@ module ActionCable self.cable = Rails.application.config_for(config_path).with_indifferent_access end - previous_connection_class = self.connection_class + previous_connection_class = connection_class self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call } options.each { |k, v| send("#{k}=", v) } diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb index 1fc58baa3e..7fcd6c6587 100644 --- a/actioncable/lib/action_cable/server/broadcasting.rb +++ b/actioncable/lib/action_cable/server/broadcasting.rb @@ -38,7 +38,7 @@ module ActionCable end def broadcast(message) - server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" + server.logger.debug "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" payload = { broadcasting: broadcasting, message: message, coder: coder } ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do diff --git a/actioncable/lib/action_cable/subscription_adapter.rb b/actioncable/lib/action_cable/subscription_adapter.rb index 72e62f3daf..596269ab9b 100644 --- a/actioncable/lib/action_cable/subscription_adapter.rb +++ b/actioncable/lib/action_cable/subscription_adapter.rb @@ -4,5 +4,6 @@ module ActionCable autoload :Base autoload :SubscriberMap + autoload :ChannelPrefix end end diff --git a/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb b/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb new file mode 100644 index 0000000000..8b293cc785 --- /dev/null +++ b/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb @@ -0,0 +1,26 @@ +module ActionCable + module SubscriptionAdapter + module ChannelPrefix # :nodoc: + def broadcast(channel, payload) + channel = channel_with_prefix(channel) + super + end + + def subscribe(channel, callback, success_callback = nil) + channel = channel_with_prefix(channel) + super + end + + def unsubscribe(channel, callback) + channel = channel_with_prefix(channel) + super + end + + private + # Returns the channel name, including channel_prefix specified in cable.yml + def channel_with_prefix(channel) + [@server.config.cable[:channel_prefix], channel].compact.join(":") + end + end + end +end diff --git a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb index c3018c5281..56b068976b 100644 --- a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb +++ b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb @@ -11,6 +11,8 @@ EventMachine.kqueue if EventMachine.kqueue? module ActionCable module SubscriptionAdapter class EventedRedis < Base # :nodoc: + prepend ChannelPrefix + @@mutex = Mutex.new # Overwrite this factory method for EventMachine Redis connections if you want to use a different Redis connection library than EM::Hiredis. diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb index 62bd284a6b..41a6e55822 100644 --- a/actioncable/lib/action_cable/subscription_adapter/redis.rb +++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb @@ -6,6 +6,8 @@ require "redis" module ActionCable module SubscriptionAdapter class Redis < Base # :nodoc: + prepend ChannelPrefix + # Overwrite this factory method for redis connections if you want to use a different Redis library than Redis. # This is needed, for example, when using Makara proxies for distributed Redis. cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } } diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb index 04b787c3a4..984b78bc9c 100644 --- a/actioncable/lib/rails/generators/channel/channel_generator.rb +++ b/actioncable/lib/rails/generators/channel/channel_generator.rb @@ -13,7 +13,7 @@ module Rails template "channel.rb", File.join("app/channels", class_path, "#{file_name}_channel.rb") if options[:assets] - if self.behavior == :invoke + if behavior == :invoke template "assets/cable.js", "app/assets/javascripts/cable.js" end @@ -30,7 +30,7 @@ module Rails # FIXME: Change these files to symlinks once RubyGems 2.5.0 is required. def generate_application_cable_files - return if self.behavior != :invoke + return if behavior != :invoke files = [ "application_cable/channel.rb", diff --git a/actioncable/test/channel/periodic_timers_test.rb b/actioncable/test/channel/periodic_timers_test.rb index 0cc4992ef6..17a8e45a35 100644 --- a/actioncable/test/channel/periodic_timers_test.rb +++ b/actioncable/test/channel/periodic_timers_test.rb @@ -38,23 +38,26 @@ class ActionCable::Channel::PeriodicTimersTest < ActiveSupport::TestCase test "disallow negative and zero periods" do [ 0, 0.0, 0.seconds, -1, -1.seconds, "foo", :foo, Object.new ].each do |invalid| - assert_raise ArgumentError, /Expected every:/ do + e = assert_raise ArgumentError do ChatChannel.periodically :send_updates, every: invalid end + assert_match(/Expected every:/, e.message) end end test "disallow block and arg together" do - assert_raise ArgumentError, /not both/ do + e = assert_raise ArgumentError do ChatChannel.periodically(:send_updates, every: 1) { ping } end + assert_match(/not both/, e.message) end test "disallow unknown args" do [ "send_updates", Object.new, nil ].each do |invalid| - assert_raise ArgumentError, /Expected a Symbol/ do + e = assert_raise ArgumentError do ChatChannel.periodically invalid, every: 1 end + assert_match(/Expected a Symbol/, e.message) end end diff --git a/actioncable/test/javascript/src/unit/consumer_test.coffee b/actioncable/test/javascript/src/unit/consumer_test.coffee index cf8a592255..41445274eb 100644 --- a/actioncable/test/javascript/src/unit/consumer_test.coffee +++ b/actioncable/test/javascript/src/unit/consumer_test.coffee @@ -2,8 +2,11 @@ {consumerTest} = ActionCable.TestHelpers module "ActionCable.Consumer", -> - consumerTest "#connect", connect: false, ({consumer, server, done}) -> - server.on("connection", done) + consumerTest "#connect", connect: false, ({consumer, server, assert, done}) -> + server.on "connection", -> + assert.equal consumer.connect(), false + done() + consumer.connect() consumerTest "#disconnect", ({consumer, client, done}) -> diff --git a/actioncable/test/subscription_adapter/channel_prefix.rb b/actioncable/test/subscription_adapter/channel_prefix.rb new file mode 100644 index 0000000000..9ad659912e --- /dev/null +++ b/actioncable/test/subscription_adapter/channel_prefix.rb @@ -0,0 +1,36 @@ +require "test_helper" + +class ActionCable::Server::WithIndependentConfig < ActionCable::Server::Base + # ActionCable::Server::Base defines config as a class variable. + # Need config to be an instance variable here as we're testing 2 separate configs + def config + @config ||= ActionCable::Server::Configuration.new + end +end + +module ChannelPrefixTest + def test_channel_prefix + server2 = ActionCable::Server::WithIndependentConfig.new + server2.config.cable = alt_cable_config + server2.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } + + adapter_klass = server2.config.pubsub_adapter + + rx_adapter2 = adapter_klass.new(server2) + tx_adapter2 = adapter_klass.new(server2) + + subscribe_as_queue("channel") do |queue| + subscribe_as_queue("channel", rx_adapter2) do |queue2| + @tx_adapter.broadcast("channel", "hello world") + tx_adapter2.broadcast("channel", "hello world 2") + + assert_equal "hello world", queue.pop + assert_equal "hello world 2", queue2.pop + end + end + end + + def alt_cable_config + cable_config.merge(channel_prefix: "foo") + end +end diff --git a/actioncable/test/subscription_adapter/evented_redis_test.rb b/actioncable/test/subscription_adapter/evented_redis_test.rb index 2401950aa7..c55d35848e 100644 --- a/actioncable/test/subscription_adapter/evented_redis_test.rb +++ b/actioncable/test/subscription_adapter/evented_redis_test.rb @@ -1,8 +1,10 @@ require "test_helper" require_relative "./common" +require_relative "./channel_prefix" class EventedRedisAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest + include ChannelPrefixTest def setup super diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb index 2ba5636656..4df5e0cbcd 100644 --- a/actioncable/test/subscription_adapter/redis_test.rb +++ b/actioncable/test/subscription_adapter/redis_test.rb @@ -1,8 +1,10 @@ require "test_helper" require_relative "./common" +require_relative "./channel_prefix" class RedisAdapterTest < ActionCable::TestCase include CommonSubscriptionAdapterTest + include ChannelPrefixTest def cable_config { adapter: "redis", driver: "ruby", url: "redis://127.0.0.1:6379/12" } diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 3b9f503a0b..4f99bb1b7a 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,28 @@ +* Add `:args` to `process.action_mailer` event. + + *Yuji Yaginuma* + +* Add parameterized invocation of mailers as a way to share before filters and defaults between actions. + See `ActionMailer::Parameterized` for a full example of the benefit. + + *DHH* + +* Allow lambdas to be used as lazy defaults in addition to procs. + + *DHH* + +* Mime type: allow to custom content type when setting body in headers + and attachments. + + Example: + + def test_emails + attachments["invoice.pdf"] = "This is test File content" + mail(body: "Hello there", content_type: "text/html") + end + + *Minh Quy* + * Exception handling: use `rescue_from` to handle exceptions raised by mailer actions, by message delivery, and by deferred delivery jobs. diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index cf2c0f37e3..211190560a 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -42,6 +42,7 @@ module ActionMailer autoload :DeliveryMethods autoload :InlinePreviewInterceptor autoload :MailHelper + autoload :Parameterized autoload :Preview autoload :Previews, "action_mailer/preview" autoload :TestCase diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 19408f2a48..9b5d39faea 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -208,6 +208,19 @@ module ActionMailer # end # end # + # You can also send attachments with html template, in this case you need to add body, attachments, + # and custom content type like this: + # + # class NotifierMailer < ApplicationMailer + # def welcome(recipient) + # attachments["free_book.pdf"] = File.read("path/to/file.pdf") + # mail(to: recipient, + # subject: "New account information", + # content_type: "text/html", + # body: "<html><body>Hello there</body></html>") + # end + # end + # # = Inline Attachments # # You can also specify that a file should be displayed inline with other HTML. This is useful @@ -275,20 +288,19 @@ module ActionMailer # content_description: 'This is a description' # end # - # Finally, Action Mailer also supports passing <tt>Proc</tt> objects into the default hash, so you - # can define methods that evaluate as the message is being generated: + # Finally, Action Mailer also supports passing <tt>Proc</tt> and <tt>Lambda</tt> objects into the default hash, + # so you can define methods that evaluate as the message is being generated: # # class NotifierMailer < ApplicationMailer - # default 'X-Special-Header' => Proc.new { my_method } + # default 'X-Special-Header' => Proc.new { my_method }, to: -> { @inviter.email_address } # # private - # # def my_method # 'some complex call' # end # end # - # Note that the proc is evaluated right at the start of the mail message generation, so if you + # Note that the proc/lambda is evaluated right at the start of the mail message generation, so if you # set something in the default hash using a proc, and then set the same thing inside of your # mailer method, it will get overwritten by the mailer method. # @@ -311,7 +323,6 @@ module ActionMailer # end # # private - # # def add_inline_attachment! # attachments.inline["footer.jpg"] = File.read('/path/to/filename.jpg') # end @@ -417,10 +428,11 @@ module ActionMailer # * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with # <tt>delivery_method :test</tt>. Most useful for unit and functional testing. # - # * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>. + # * <tt>deliver_later_queue_name</tt> - The name of the queue used with <tt>deliver_later</tt>. Defaults to +mailers+. class Base < AbstractController::Base include DeliveryMethods include Rescuable + include Parameterized include Previews abstract! @@ -567,7 +579,7 @@ module ActionMailer end def respond_to_missing?(method, include_all = false) - action_methods.include?(method.to_s) + action_methods.include?(method.to_s) || super end end @@ -586,7 +598,8 @@ module ActionMailer def process(method_name, *args) #:nodoc: payload = { mailer: self.class.name, - action: method_name + action: method_name, + args: args } ActiveSupport::Notifications.instrument("process.action_mailer", payload) do @@ -875,7 +888,7 @@ module ActionMailer default_values = self.class.default.map do |key, value| [ key, - value.is_a?(Proc) ? instance_eval(&value) : value + value.is_a?(Proc) ? instance_exec(&value) : value ] end.to_h @@ -896,15 +909,19 @@ module ActionMailer yield(collector) collector.responses elsif headers[:body] - [{ - body: headers.delete(:body), - content_type: self.class.default[:content_type] || "text/plain" - }] + collect_responses_from_text(headers) else collect_responses_from_templates(headers) end end + def collect_responses_from_text(headers) + [{ + body: headers.delete(:body), + content_type: headers[:content_type] || "text/plain" + }] + end + def collect_responses_from_templates(headers) templates_path = headers[:template_path] || self.class.mailer_name templates_name = headers[:template_name] || action_name @@ -955,7 +972,7 @@ module ActionMailer end def instrument_name - "action_mailer" + "action_mailer".freeze end ActiveSupport.run_load_hooks(:action_mailer, self) diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb index be98f4c65e..bcc4ef03cf 100644 --- a/actionmailer/lib/action_mailer/delivery_methods.rb +++ b/actionmailer/lib/action_mailer/delivery_methods.rb @@ -59,7 +59,7 @@ module ActionMailer end def wrap_delivery_behavior(mail, method = nil, options = nil) # :nodoc: - method ||= self.delivery_method + method ||= delivery_method mail.delivery_handler = self case method diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb index 9087d335fa..980415afe0 100644 --- a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb +++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb @@ -26,7 +26,7 @@ module ActionMailer def transform! #:nodoc: return message if html_part.blank? - html_source.gsub!(PATTERN) do |match| + html_part.body = html_part.decoded.gsub(PATTERN) do |match| if part = find_part(match[9..-2]) %[src="#{data_url(part)}"] else @@ -46,10 +46,6 @@ module ActionMailer @html_part ||= message.html_part end - def html_source - html_part.body.raw_source - end - def data_url(part) "data:#{part.mime_type};base64,#{strict_encode64(part.body.raw_source)}" end diff --git a/actionmailer/lib/action_mailer/parameterized.rb b/actionmailer/lib/action_mailer/parameterized.rb new file mode 100644 index 0000000000..3acacc1f14 --- /dev/null +++ b/actionmailer/lib/action_mailer/parameterized.rb @@ -0,0 +1,152 @@ +module ActionMailer + # Provides the option to parameterize mailers in order to share instance variable + # setup, processing, and common headers. + # + # Consider this example that does not use parameterization: + # + # class InvitationsMailer < ApplicationMailer + # def account_invitation(inviter, invitee) + # @account = inviter.account + # @inviter = inviter + # @invitee = invitee + # + # subject = "#{@inviter.name} invited you to their Basecamp (#{@account.name})" + # + # mail \ + # subject: subject, + # to: invitee.email_address, + # from: common_address(inviter), + # reply_to: inviter.email_address_with_name + # end + # + # def project_invitation(project, inviter, invitee) + # @account = inviter.account + # @project = project + # @inviter = inviter + # @invitee = invitee + # @summarizer = ProjectInvitationSummarizer.new(@project.bucket) + # + # subject = "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})" + # + # mail \ + # subject: subject, + # to: invitee.email_address, + # from: common_address(inviter), + # reply_to: inviter.email_address_with_name + # end + # + # def bulk_project_invitation(projects, inviter, invitee) + # @account = inviter.account + # @projects = projects.sort_by(&:name) + # @inviter = inviter + # @invitee = invitee + # + # subject = "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})" + # + # mail \ + # subject: subject, + # to: invitee.email_address, + # from: common_address(inviter), + # reply_to: inviter.email_address_with_name + # end + # end + # + # InvitationsMailer.account_invitation(person_a, person_b).deliver_later + # + # Using parameterized mailers, this can be rewritten as: + # + # class InvitationsMailer < ApplicationMailer + # before_action { @inviter, @invitee = params[:inviter], params[:invitee] } + # before_action { @account = params[:inviter].account } + # + # default to: -> { @invitee.email_address }, + # from: -> { common_address(@inviter) }, + # reply_to: -> { @inviter.email_address_with_name } + # + # def account_invitation + # mail subject: "#{@inviter.name} invited you to their Basecamp (#{@account.name})" + # end + # + # def project_invitation + # @project = params[:project] + # @summarizer = ProjectInvitationSummarizer.new(@project.bucket) + # + # mail subject: "#{@inviter.name.familiar} added you to a project in Basecamp (#{@account.name})" + # end + # + # def bulk_project_invitation + # @projects = params[:projects].sort_by(&:name) + # + # mail subject: "#{@inviter.name.familiar} added you to some new stuff in Basecamp (#{@account.name})" + # end + # end + # + # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later + module Parameterized + extend ActiveSupport::Concern + + included do + attr_accessor :params + end + + module ClassMethods + # Provide the parameters to the mailer in order to use them in the instance methods and callbacks. + # + # InvitationsMailer.with(inviter: person_a, invitee: person_b).account_invitation.deliver_later + # + # See Parameterized documentation for full example. + def with(params) + ActionMailer::Parameterized::Mailer.new(self, params) + end + end + + class Mailer # :nodoc: + def initialize(mailer, params) + @mailer, @params = mailer, params + end + + private + def method_missing(method_name, *args) + if @mailer.action_methods.include?(method_name.to_s) + ActionMailer::Parameterized::MessageDelivery.new(@mailer, method_name, @params, *args) + else + super + end + end + + def respond_to_missing?(method, include_all = false) + @mailer.respond_to?(method, include_all) + end + end + + class MessageDelivery < ActionMailer::MessageDelivery # :nodoc: + def initialize(mailer_class, action, params, *args) + super(mailer_class, action, *args) + @params = params + end + + private + def processed_mailer + @processed_mailer ||= @mailer_class.new.tap do |mailer| + mailer.params = @params + mailer.process @action, *@args + end + end + + def enqueue_delivery(delivery_method, options = {}) + if processed? + super + else + args = @mailer_class.name, @action.to_s, delivery_method.to_s, @params, *@args + ActionMailer::Parameterized::DeliveryJob.set(options).perform_later(*args) + end + end + end + + class DeliveryJob < ActionMailer::DeliveryJob # :nodoc: + def perform(mailer, mail_method, delivery_method, params, *args) + mailer.constantize.with(params).public_send(mail_method, *args).send(delivery_method) + end + end + end +end diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb index c147ca78d0..b0152aff03 100644 --- a/actionmailer/lib/action_mailer/preview.rb +++ b/actionmailer/lib/action_mailer/preview.rb @@ -63,7 +63,7 @@ module ActionMailer # interceptors will be informed so that they can transform the message # as they would if the mail was actually being delivered. def call(email) - preview = self.new + preview = new message = preview.public_send(email) inform_preview_interceptors(message) message diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index ebc7b37961..9ead03a40c 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -4,8 +4,8 @@ require "rails-dom-testing" module ActionMailer class NonInferrableMailerError < ::StandardError def initialize(name) - super "Unable to determine the mailer to test from #{name}. " + - "You'll need to specify it using tests YourMailer in your " + + super "Unable to determine the mailer to test from #{name}. " \ + "You'll need to specify it using tests YourMailer in your " \ "test case definition" end end @@ -57,7 +57,7 @@ module ActionMailer end def mailer_class - if mailer = self._mailer_class + if mailer = _mailer_class mailer else tests determine_default_mailer(name) diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb index c17ecad4c6..c30fb1fc18 100644 --- a/actionmailer/lib/action_mailer/test_helper.rb +++ b/actionmailer/lib/action_mailer/test_helper.rb @@ -88,7 +88,7 @@ module ActionMailer # end # end def assert_enqueued_emails(number, &block) - assert_enqueued_jobs number, only: ActionMailer::DeliveryJob, &block + assert_enqueued_jobs number, only: [ ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob ], &block end # Asserts that no emails are enqueued for later delivery. @@ -107,7 +107,7 @@ module ActionMailer # end # end def assert_no_enqueued_emails(&block) - assert_no_enqueued_jobs only: ActionMailer::DeliveryJob, &block + assert_no_enqueued_jobs only: [ ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob ], &block end end end diff --git a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb index 4a8d0178de..99fe4544f1 100644 --- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb +++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb @@ -11,7 +11,7 @@ module Rails template "mailer.rb", File.join("app/mailers", class_path, "#{file_name}_mailer.rb") in_root do - if self.behavior == :invoke && !File.exist?(application_mailer_file_name) + if behavior == :invoke && !File.exist?(application_mailer_file_name) template "application_mailer.rb", application_mailer_file_name end end diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 82f03f5cba..a646cbd581 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -3,8 +3,8 @@ require "active_support/core_ext/kernel/reporting" # These are the normal settings that will be set up by Railties # TODO: Have these tests support other combinations of these values silence_warnings do - Encoding.default_internal = "UTF-8" - Encoding.default_external = "UTF-8" + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 end module Rails @@ -31,15 +31,15 @@ I18n.enforce_available_locales = false FIXTURE_LOAD_PATH = File.expand_path("fixtures", File.dirname(__FILE__)) ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" -end -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) -end - class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions + + # Skips the current run on Rubinius using Minitest::Assertions#skip + private def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + # Skips the current run on JRuby using Minitest::Assertions#skip + private def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 91049e33f9..61960d411d 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -140,6 +140,11 @@ class BaseTest < ActiveSupport::TestCase assert_equal("multipart/mixed", email.mime_type) end + test "set mime type to text/html when attachment is included and body is set" do + email = BaseMailer.attachment_with_content(body: "Hello there", content_type: "text/html") + assert_equal("text/html", email.mime_type) + end + test "adds the rendered template as part" do email = BaseMailer.attachment_with_content assert_equal(2, email.parts.length) @@ -829,6 +834,25 @@ class BaseTest < ActiveSupport::TestCase assert_equal "special indeed!", mail["X-Special-Header"].to_s end + test "notification for process" do + begin + events = [] + ActiveSupport::Notifications.subscribe("process.action_mailer") do |*args| + events << ActiveSupport::Notifications::Event.new(*args) + end + + BaseMailer.welcome(body: "Hello there").deliver_now + + assert_equal 1, events.length + assert_equal "process.action_mailer", events[0].name + assert_equal "BaseMailer", events[0].payload[:mailer] + assert_equal :welcome, events[0].payload[:action] + assert_equal [{ body: "Hello there" }], events[0].payload[:args] + ensure + ActiveSupport::Notifications.unsubscribe "process.action_mailer" + end + end + private # Execute the block setting the given values and restoring old values after diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb index a87a9c9861..4f09339800 100644 --- a/actionmailer/test/i18n_with_controller_test.rb +++ b/actionmailer/test/i18n_with_controller_test.rb @@ -57,9 +57,7 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest stub_any_instance(Mail::SMTP, instance: Mail::SMTP.new({})) do |instance| assert_called(instance, :deliver!) do with_translation "de", email_subject: "[Anmeldung] Willkommen" do - ActiveSupport::Deprecation.silence do - get "/test/send_mail" - end + get "/test/send_mail" assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body end end diff --git a/actionmailer/test/mail_helper_test.rb b/actionmailer/test/mail_helper_test.rb index a2e04b5f48..6042548aef 100644 --- a/actionmailer/test/mail_helper_test.rb +++ b/actionmailer/test/mail_helper_test.rb @@ -2,11 +2,11 @@ require "abstract_unit" class HelperMailer < ActionMailer::Base def use_mail_helper - @text = "But soft! What light through yonder window breaks? It is the east, " + - "and Juliet is the sun. Arise, fair sun, and kill the envious moon, " + - "which is sick and pale with grief that thou, her maid, art far more " + - "fair than she. Be not her maid, for she is envious! Her vestal " + - "livery is but sick and green, and none but fools do wear it. Cast " + + @text = "But soft! What light through yonder window breaks? It is the east, " \ + "and Juliet is the sun. Arise, fair sun, and kill the envious moon, " \ + "which is sick and pale with grief that thou, her maid, art far more " \ + "fair than she. Be not her maid, for she is envious! Her vestal " \ + "livery is but sick and green, and none but fools do wear it. Cast " \ "it off!" mail_with_defaults do |format| diff --git a/actionmailer/test/mailers/params_mailer.rb b/actionmailer/test/mailers/params_mailer.rb new file mode 100644 index 0000000000..4c0fae6d91 --- /dev/null +++ b/actionmailer/test/mailers/params_mailer.rb @@ -0,0 +1,11 @@ +class ParamsMailer < ActionMailer::Base + before_action { @inviter, @invitee = params[:inviter], params[:invitee] } + + default to: Proc.new { @invitee }, from: -> { @inviter } + + def invitation + mail(subject: "Welcome to the project!") do |format| + format.text { render plain: "So says #{@inviter}" } + end + end +end diff --git a/actionmailer/test/parameterized_test.rb b/actionmailer/test/parameterized_test.rb new file mode 100644 index 0000000000..914ed12312 --- /dev/null +++ b/actionmailer/test/parameterized_test.rb @@ -0,0 +1,55 @@ +require "abstract_unit" +require "active_job" +require "mailers/params_mailer" + +class ParameterizedTest < ActiveSupport::TestCase + include ActiveJob::TestHelper + + setup do + @previous_logger = ActiveJob::Base.logger + ActiveJob::Base.logger = Logger.new(nil) + + @previous_delivery_method = ActionMailer::Base.delivery_method + ActionMailer::Base.delivery_method = :test + + @previous_deliver_later_queue_name = ActionMailer::Base.deliver_later_queue_name + ActionMailer::Base.deliver_later_queue_name = :test_queue + ActionMailer::Base.delivery_method = :test + + @mail = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com").invitation + end + + teardown do + ActiveJob::Base.logger = @previous_logger + ParamsMailer.deliveries.clear + + ActionMailer::Base.delivery_method = @previous_delivery_method + ActionMailer::Base.deliver_later_queue_name = @previous_deliver_later_queue_name + end + + test "parameterized headers" do + assert_equal(["jason@basecamp.com"], @mail.to) + assert_equal(["david@basecamp.com"], @mail.from) + assert_equal("So says david@basecamp.com", @mail.body.encoded) + end + + test "enqueue the email with params" do + assert_performed_with(job: ActionMailer::Parameterized::DeliveryJob, args: ["ParamsMailer", "invitation", "deliver_now", { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" } ]) do + @mail.deliver_later + end + end + + test "respond_to?" do + mailer = ParamsMailer.with(inviter: "david@basecamp.com", invitee: "jason@basecamp.com") + + assert_respond_to mailer, :invitation + assert_not_respond_to mailer, :anything + + invitation = mailer.method(:invitation) + assert_equal Method, invitation.class + + assert_raises(NameError) do + invitation = mailer.method(:anything) + end + end +end diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index 31ac5a5211..876e9b0634 100644 --- a/actionmailer/test/test_helper_test.rb +++ b/actionmailer/test/test_helper_test.rb @@ -143,6 +143,16 @@ class TestHelperMailerTest < ActionMailer::TestCase end end + def test_assert_enqueued_parameterized_emails + assert_nothing_raised do + assert_enqueued_emails 1 do + silence_stream($stdout) do + TestHelperMailer.with(a: 1).test.deliver_later + end + end + end + end + def test_assert_enqueued_emails_too_few_sent error = assert_raise ActiveSupport::TestCase::Assertion do assert_enqueued_emails 2 do @@ -176,6 +186,14 @@ class TestHelperMailerTest < ActionMailer::TestCase end end + def test_assert_no_enqueued_parameterized_emails + assert_nothing_raised do + assert_no_enqueued_emails do + TestHelperMailer.with(a: 1).test.deliver_now + end + end + end + def test_assert_no_enqueued_emails_failure error = assert_raise ActiveSupport::TestCase::Assertion do assert_no_enqueued_emails do diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index a5f6426f21..0167dcbf96 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,72 @@ +* Prefer `remove_method` over `undef_method` when reloading routes + + When `undef_method` is used it prevents access to other implementations of that + url helper in the ancestor chain so use `remove_method` instead to restore access. + + *Andrew White* + +* Add the `resolve` method to the routing DSL + + This new method allows customization of the polymorphic mapping of models: + + ``` ruby + resource :basket + resolve(class: "Basket") { [:basket] } + ``` + + ``` erb + <%= form_for @basket do |form| %> + <!-- basket form --> + <% end %> + ``` + + This generates the correct singular URL for the form instead of the default + resources member url, e.g. `/basket` vs. `/basket/:id`. + + Fixes #1769. + + *Andrew White* + +* Add the `direct` method to the routing DSL + + This new method allows creation of custom url helpers, e.g: + + ``` ruby + direct(:apple) { "http://www.apple.com" } + + >> apple_url + => "http://www.apple.com" + ``` + + This has the advantage of being available everywhere url helpers are available + unlike custom url helpers defined in helper modules, etc. + + *Andrew White* + +* Add `ActionDispatch::SystemTestCase` to Action Pack + + Adds Capybara integration directly into Rails through Action Pack! + + See PR [#26703](https://github.com/rails/rails/pull/26703) + + *Eileen M. Uchitelle* + +* Remove deprecated `.to_prepare`, `.to_cleanup`, `.prepare!` and `.cleanup!` from `ActionDispatch::Reloader`. + + *Rafael Mendonça França* + +* Remove deprecated `ActionDispatch::Callbacks.to_prepare` and `ActionDispatch::Callbacks.to_cleanup`. + + *Rafael Mendonça França* + +* Remove deprecated `ActionController::Metal.call`. + + *Rafael Mendonça França* + +* Remove deprecated `ActionController::Metal#env`. + + *Rafael Mendonça França* + * Make `with_routing` test helper work when testing controllers inheriting from `ActionController::API` *Julia López* @@ -19,7 +88,7 @@ *Tawan Sierek* -* Fixes incorrect output from rails routes when using singular resources. +* Fixes incorrect output from `rails routes` when using singular resources. Fixes #26606. diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 603c2e9ea7..e7cb6347a2 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -1,4 +1,3 @@ -require "erubis" require "abstract_controller/error" require "active_support/configurable" require "active_support/descendants_tracker" @@ -22,7 +21,6 @@ module AbstractController include ActiveSupport::Configurable extend ActiveSupport::DescendantsTracker - undef_method :not_implemented class << self attr_reader :abstract alias_method :abstract?, :abstract diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index a9a8508abc..954265ad97 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -38,7 +38,7 @@ module ActionController end def instrument_name - "action_controller" + "action_controller".freeze end end end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 9dab7aeef4..337718afc0 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -118,11 +118,6 @@ module ActionController class Metal < AbstractController::Base abstract! - def env - @_request.env - end - deprecate :env - # Returns the last part of the controller's name, underscored, without the ending # <tt>Controller</tt>. For instance, PostsController returns <tt>posts</tt>. # Namespaces are left out, so Admin::PostsController returns <tt>posts</tt> as well. @@ -232,14 +227,6 @@ module ActionController middleware_stack end - # Makes the controller a Rack endpoint that runs the action in the given - # +env+'s +action_dispatch.request.path_parameters+ key. - def self.call(env) - req = ActionDispatch::Request.new env - action(req.path_parameters[:action]).call(env) - end - class << self; deprecate :call; end - # Returns a Rack endpoint for the given action name. def self.action(name) if middleware_stack.any? diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 4dff23dd85..0c50894bce 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -37,7 +37,7 @@ module ActionController if include_content?(response_code) self.content_type = content_type || (Mime[formats.first] if formats) - self.response.charset = false + response.charset = false end true diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index 8615c16c6f..dde924e682 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/string/strip" - module ActionController # Handles implicit rendering for a controller action that does not # explicitly respond with +render+, +respond_to+, +redirect+, or +head+. diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 30798c1d99..a349841082 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -50,13 +50,13 @@ module ActionController # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id } # redirect_to({ action: 'atom' }, alert: "Something serious happened") # - def redirect_to(options = {}, response_status = {}) #:doc: + def redirect_to(options = {}, response_status = {}) raise ActionControllerError.new("Cannot redirect to nil!") unless options raise AbstractController::DoubleRenderError if response_body self.status = _extract_redirect_to_status(options, response_status) self.location = _compute_redirect_to_location(request, options) - self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>" + self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>" end # Redirects the browser to the page that issued the request (the referrer) diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 4c01891d4c..6b17719381 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -114,7 +114,7 @@ module ActionController self.status = status if status self.content_type = content_type if content_type - self.headers["Location"] = url_for(location) if location + headers["Location"] = url_for(location) if location super end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index acfeca1fcb..d304dcf468 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -150,7 +150,7 @@ module ActionController # permitted flag. def ==(other) if other.respond_to?(:permitted?) - self.permitted? == other.permitted? && self.parameters == other.parameters + permitted? == other.permitted? && parameters == other.parameters else @parameters == other end @@ -398,8 +398,7 @@ module ActionController unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters - params.permitted = true - params + params.permit! end # Returns a parameter for the given +key+. If not found, @@ -818,7 +817,6 @@ module ActionController # Filter this one out. end end - sanitized.permitted = true end end diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb index 3ff80e6a39..acb400cd15 100644 --- a/actionpack/lib/action_controller/renderer.rb +++ b/actionpack/lib/action_controller/renderer.rb @@ -60,7 +60,8 @@ module ActionController end # Accepts a custom Rack environment to render templates in. - # It will be merged with ActionController::Renderer.defaults + # It will be merged with the default Rack environment defined by + # +ActionController::Renderer::DEFAULTS+. def initialize(controller, env, defaults) @controller = controller @defaults = defaults diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 441667e556..7229c67f30 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -354,7 +354,7 @@ module ActionController end def controller_class - if current_controller_class = self._controller_class + if current_controller_class = _controller_class current_controller_class else self.controller_class = determine_default_controller_class(name) @@ -534,6 +534,7 @@ module ActionController @request.delete_header "HTTP_ACCEPT" end @request.query_string = "" + @request.env.delete "PATH_INFO" @response.sent! end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 028177ace2..303790e96d 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -97,6 +97,8 @@ module ActionDispatch autoload :TestResponse autoload :AssertionResponse end + + autoload :SystemTestCase, "action_dispatch/system_test_case" end autoload :Mime, "action_dispatch/http/mime_type" diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index 6b718e3682..1583a8f87f 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -1,7 +1,6 @@ # -*- frozen-string-literal: true -*- require "singleton" -require "active_support/core_ext/module/attribute_accessors" require "active_support/core_ext/string/starts_ends_with" module Mime @@ -298,7 +297,7 @@ module Mime end def respond_to_missing?(method, include_private = false) - method.to_s.ends_with? "?" + (method.to_s.ends_with? "?") || super end end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index ad4aadacf5..8f21eca440 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -22,6 +22,7 @@ module ActionDispatch included do class << self + # Returns the parameter parsers. attr_reader :parameter_parsers end @@ -29,7 +30,16 @@ module ActionDispatch end module ClassMethods - def parameter_parsers=(parsers) # :nodoc: + # Configure the parameter parser for a given mime type. + # + # It accepts a hash where the key is the symbol of the mime type + # and the value is a proc. + # + # original_parsers = ActionDispatch::Request.parameter_parsers + # xml_parser = -> (raw_post) { Hash.from_xml(raw_post) || {} } + # new_parsers = original_parsers.merge(xml: xml_parser) + # ActionDispatch::Request.parameter_parsers = new_parsers + def parameter_parsers=(parsers) @parameter_parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key } end end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 516a2af69a..dc159596c4 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -425,7 +425,7 @@ module ActionDispatch # :nodoc: def set_content_type(content_type, charset) type = (content_type || "").dup - type << "; charset=#{charset}" if charset + type << "; charset=#{charset.to_s.downcase}" if charset set_header CONTENT_TYPE, type end diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb index db42b64c4b..e002755bcf 100644 --- a/actionpack/lib/action_dispatch/journey/parser.rb +++ b/actionpack/lib/action_dispatch/journey/parser.rb @@ -1,7 +1,7 @@ # # DO NOT MODIFY!!!! # This file is automatically generated by Racc 1.4.14 -# from Racc grammer file "". +# from Racc grammar file "". # require 'racc/parser.rb' diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb index 0902b9233e..cf0108ec32 100644 --- a/actionpack/lib/action_dispatch/journey/path/pattern.rb +++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb @@ -31,6 +31,13 @@ module ActionDispatch Visitors::FormatBuilder.new.accept(spec) end + def eager_load! + required_names + offsets + to_regexp + nil + end + def ast @spec.find_all(&:symbol?).each do |node| re = @requirements[node.to_sym] diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index f2ac4818d8..927fd369c4 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -73,6 +73,14 @@ module ActionDispatch @internal = internal end + def eager_load! + path.eager_load! + ast + parts + required_defaults + nil + end + def ast @decorated_ast ||= begin decorated_ast = path.ast diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index 084ae9325e..d55e1399e4 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -22,6 +22,13 @@ module ActionDispatch @routes = routes end + def eager_load! + # Eagerly trigger the simulator's initialization so + # it doesn't happen during a request cycle. + simulator + nil + end + def serve(req) find_routes(req).each do |match, parameters, route| set_params = req.path_parameters diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb index cda859cba4..1c50192867 100644 --- a/actionpack/lib/action_dispatch/journey/visitors.rb +++ b/actionpack/lib/action_dispatch/journey/visitors.rb @@ -5,7 +5,7 @@ module ActionDispatch ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) } ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) } - class Parameter < Struct.new(:name, :escaper) + Parameter = Struct.new(:name, :escaper) do def escape(value); escaper.call value; end end diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index fef246532b..ff129cf96a 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -1,4 +1,3 @@ - module ActionDispatch # Provides callbacks to be executed before and after dispatching the request. class Callbacks @@ -7,17 +6,6 @@ module ActionDispatch define_callbacks :call class << self - def to_prepare(*args, &block) - ActiveSupport::Reloader.to_prepare(*args, &block) - end - - def to_cleanup(*args, &block) - ActiveSupport::Reloader.to_complete(*args, &block) - end - - deprecate to_prepare: "use ActiveSupport::Reloader.to_prepare instead", - to_cleanup: "use ActiveSupport::Reloader.to_complete instead" - def before(*args, &block) set_callback(:call, :before, *args, &block) end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 956c53e813..c61cb3fd68 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -572,7 +572,7 @@ module ActionDispatch super if ActiveSupport::LegacyKeyGenerator === key_generator - raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + + raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " \ "Read the upgrade documentation to learn more about this new config option." end diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 90c64037aa..6d64b1424b 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -1,54 +1,10 @@ module ActionDispatch - # ActionDispatch::Reloader provides prepare and cleanup callbacks, - # intended to assist with code reloading during development. - # - # Prepare callbacks are run before each request, and cleanup callbacks - # after each request. In this respect they are analogs of ActionDispatch::Callback's - # before and after callbacks. However, cleanup callbacks are not called until the - # request is fully complete -- that is, after #close has been called on - # the response body. This is important for streaming responses such as the - # following: - # - # self.response_body = -> (response, output) do - # # code here which refers to application models - # end - # - # Cleanup callbacks will not be called until after the response_body lambda - # is evaluated, ensuring that it can refer to application models and other - # classes before they are unloaded. + # ActionDispatch::Reloader wraps the request with callbacks provided by ActiveSupport::Reloader + # callbacks, intended to assist with code reloading during development. # # By default, ActionDispatch::Reloader is included in the middleware stack # only in the development environment; specifically, when +config.cache_classes+ - # is false. Callbacks may be registered even when it is not included in the - # middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt> - # or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually. - # + # is false. class Reloader < Executor - def self.to_prepare(*args, &block) - ActiveSupport::Reloader.to_prepare(*args, &block) - end - - def self.to_cleanup(*args, &block) - ActiveSupport::Reloader.to_complete(*args, &block) - end - - def self.prepare! - default_reloader.prepare! - end - - def self.cleanup! - default_reloader.reload! - end - - class << self - attr_accessor :default_reloader # :nodoc: - - deprecate to_prepare: "use ActiveSupport::Reloader.to_prepare instead", - to_cleanup: "use ActiveSupport::Reloader.to_complete instead", - prepare!: "use Rails.application.reloader.prepare! instead", - cleanup!: "use Rails.application.reloader.reload! instead of cleanup + prepare" - end - - self.default_reloader = ActiveSupport::Reloader end end diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb index 9f1ae80b97..8bae5bfeff 100644 --- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb +++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb @@ -131,8 +131,8 @@ module ActionDispatch should_check_ip = @check_ip && client_ips.last && forwarded_ips.last if should_check_ip && !forwarded_ips.include?(client_ips.last) # We don't know which came from the proxy, and which from the user - raise IpSpoofAttackError, "IP spoofing attack?! " + - "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " + + raise IpSpoofAttackError, "IP spoofing attack?! " \ + "HTTP_CLIENT_IP=#{@req.client_ip.inspect} " \ "HTTP_X_FORWARDED_FOR=#{@req.x_forwarded_for.inspect}" end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index 97c937b0b1..d9f018c8ac 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -8,8 +8,8 @@ module ActionDispatch module Session class SessionRestoreError < StandardError #:nodoc: def initialize - super("Session contains objects whose class definition isn't available.\n" + - "Remember to require the classes for all objects kept in the session.\n" + + super("Session contains objects whose class definition isn't available.\n" \ + "Remember to require the classes for all objects kept in the session.\n" \ "(Original exception: #{$!.message} [#{$!.class}])\n") set_backtrace $!.backtrace end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 5c71f0fc48..5d10129d21 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -33,7 +33,7 @@ module ActionDispatch paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"] if match = paths.detect { |p| - path = File.join(@root, p.force_encoding("UTF-8".freeze)) + path = File.join(@root, p.force_encoding(Encoding::UTF_8)) begin File.file?(path) && File.readable?(path) rescue SystemCallError diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 48cc91bbfa..16a18a7f25 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -39,8 +39,6 @@ module ActionDispatch config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil? ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie - ActionDispatch::Reloader.default_reloader = app.reloader - ActionDispatch.test_app = app end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 089aa9f78e..073dabd0a8 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,6 +1,7 @@ require "active_support/core_ext/hash/slice" require "active_support/core_ext/enumerable" require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/regexp" require "action_dispatch/routing/redirection" require "action_dispatch/routing/endpoint" @@ -238,7 +239,7 @@ module ActionDispatch options[:controller] ||= /.+?/ end - if to.respond_to? :call + if to.respond_to?(:action) || to.respond_to?(:call) options else to_endpoint = split_to to @@ -290,16 +291,14 @@ module ActionDispatch end def app(blocks) - if to.is_a?(Class) && to < ActionController::Metal + if to.respond_to?(:action) Routing::RouteSet::StaticDispatcher.new to + elsif to.respond_to?(:call) + Constraints.new(to, blocks, Constraints::CALL) + elsif blocks.any? + Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE) else - if to.respond_to?(:call) - Constraints.new(to, blocks, Constraints::CALL) - elsif blocks.any? - Constraints.new(dispatcher(defaults.key?(:controller)), blocks, Constraints::SERVE) - else - dispatcher(defaults.key?(:controller)) - end + dispatcher(defaults.key?(:controller)) end end @@ -2021,6 +2020,111 @@ module ActionDispatch end end + module CustomUrls + # Define custom url helpers that will be added to the application's + # routes. This allows you to override and/or replace the default behavior + # of routing helpers, e.g: + # + # direct :homepage do + # "http://www.rubyonrails.org" + # end + # + # direct :commentable do |model| + # [ model, anchor: model.dom_id ] + # end + # + # direct :main do + # { controller: 'pages', action: 'index', subdomain: 'www' } + # end + # + # The return value from the block passed to `direct` must be a valid set of + # arguments for `url_for` which will actually build the url string. This can + # be one of the following: + # + # * A string, which is treated as a generated url + # * A hash, e.g. { controller: 'pages', action: 'index' } + # * An array, which is passed to `polymorphic_url` + # * An Active Model instance + # * An Active Model class + # + # NOTE: Other url helpers can be called in the block but be careful not to invoke + # your custom url helper again otherwise it will result in a stack overflow error + # + # You can also specify default options that will be passed through to + # your url helper definition, e.g: + # + # direct :browse, page: 1, size: 10 do |options| + # [ :products, options.merge(params.permit(:page, :size)) ] + # end + # + # NOTE: The `direct` method can't be used inside of a scope block such as + # `namespace` or `scope` and will raise an error if it detects that it is. + def direct(name, options = {}, &block) + unless @scope.root? + raise RuntimeError, "The direct method can't be used inside a routes scope block" + end + + @set.add_url_helper(name, options, &block) + end + + # Define custom polymorphic mappings of models to urls. This alters the + # behavior of `polymorphic_url` and consequently the behavior of + # `link_to` and `form_for` when passed a model instance, e.g: + # + # resource :basket + # + # resolve "Basket" do + # [:basket] + # end + # + # This will now generate "/basket" when a `Basket` instance is passed to + # `link_to` or `form_for` instead of the standard "/baskets/:id". + # + # NOTE: This custom behavior only applies to simple polymorphic urls where + # a single model instance is passed and not more complicated forms, e.g: + # + # # config/routes.rb + # resource :profile + # namespace :admin do + # resources :users + # end + # + # resolve("User") { [:profile] } + # + # # app/views/application/_menu.html.erb + # link_to "Profile", @current_user + # link_to "Profile", [:admin, @current_user] + # + # The first `link_to` will generate "/profile" but the second will generate + # the standard polymorphic url of "/admin/users/1". + # + # You can pass options to a polymorphic mapping - the arity for the block + # needs to be two as the instance is passed as the first argument, e.g: + # + # direct class: "Basket", anchor: "items" do |basket, options| + # [:basket, options] + # end + # + # This generates the url "/basket#items" because when the last item in an + # array passed to `polymorphic_url` is a hash then it's treated as options + # to the url helper that gets called. + # + # NOTE: The `resolve` method can't be used inside of a scope block such as + # `namespace` or `scope` and will raise an error if it detects that it is. + def resolve(*args, &block) + unless @scope.root? + raise RuntimeError, "The resolve method can't be used inside a routes scope block" + end + + options = args.extract_options! + args = args.flatten(1) + + args.each do |klass| + @set.add_polymorphic_mapping(klass, options, &block) + end + end + end + class Scope # :nodoc: OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, :controller, :action, :path_names, :constraints, @@ -2041,6 +2145,14 @@ module ActionDispatch scope_level == :nested end + def null? + @hash.nil? && @parent.nil? + end + + def root? + @parent.null? + end + def resources? scope_level == :resources end @@ -2114,6 +2226,7 @@ module ActionDispatch include Scoping include Concerns include Resources + include CustomUrls end end end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 432b9bf4c1..984ded1ff5 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -103,6 +103,10 @@ module ActionDispatch return polymorphic_url record, options end + if mapping = polymorphic_mapping(record_or_hash_or_array) + return mapping.call(self, [record_or_hash_or_array, options]) + end + opts = options.dup action = opts.delete :action type = opts.delete(:routing_type) || :url @@ -123,6 +127,10 @@ module ActionDispatch return polymorphic_path record, options end + if mapping = polymorphic_mapping(record_or_hash_or_array) + return mapping.call(self, [record_or_hash_or_array, options], only_path: true) + end + opts = options.dup action = opts.delete :action type = :path @@ -156,6 +164,14 @@ module ActionDispatch polymorphic_path(record_or_hash, options.merge(action: action)) end + def polymorphic_mapping(record) + if record.respond_to?(:to_model) + _routes.polymorphic_mappings[record.to_model.model_name.name] + else + _routes.polymorphic_mappings[record.class.name] + end + end + class HelperMethodBuilder # :nodoc: CACHE = { "path" => {}, "url" => {} } @@ -255,9 +271,13 @@ module ActionDispatch [named_route, args] end - def handle_model_call(target, model) - method, args = handle_model model - target.send(method, *args) + def handle_model_call(target, record) + if mapping = polymorphic_mapping(target, record) + mapping.call(target, [record], only_path: suffix == "path") + else + method, args = handle_model(record) + target.send(method, *args) + end end def handle_list(list) @@ -303,6 +323,14 @@ module ActionDispatch private + def polymorphic_mapping(target, record) + if record.respond_to?(:to_model) + target._routes.polymorphic_mappings[record.to_model.model_name.name] + else + target._routes.polymorphic_mappings[record.class.name] + end + end + def get_method_for_class(klass) name = @key_strategy.call klass.model_name get_method_for_string name diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb index 4e2318a45e..dabc045007 100644 --- a/actionpack/lib/action_dispatch/routing/redirection.rb +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -137,6 +137,9 @@ module ActionDispatch # # get "/stories" => redirect("/posts") # + # This will redirect the user, while ignoring certain parts of the request, including query string, etc. + # `/stories`, `/stories?foo=bar`, etc all redirect to `/posts`. + # # You can also use interpolation in the supplied redirect argument: # # get 'docs/:article', to: redirect('/wiki/%{article}') @@ -165,6 +168,11 @@ module ActionDispatch # # get 'stores/:name', to: redirect(subdomain: 'stores', path: '/%{name}') # get 'stores/:name(*all)', to: redirect(subdomain: 'stores', path: '/%{name}%{all}') + # get '/stories', to: redirect(path: '/posts') + # + # This will redirect the user, while changing only the specified parts of the request, + # for example the `path` option in the last example. + # `/stories`, `/stories?foo=bar`, redirect to `/posts` and `/posts?foo=bar` respectively. # # Finally, an object which responds to call can be supplied to redirect, allowing you to reuse # common redirect routes. The call method must accept two arguments, params and request, and return diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 5853adb110..2672cd24ed 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -73,6 +73,7 @@ module ActionDispatch @routes = {} @path_helpers = Set.new @url_helpers = Set.new + @custom_helpers = Set.new @url_helpers_module = Module.new @path_helpers_module = Module.new end @@ -88,16 +89,30 @@ module ActionDispatch def clear! @path_helpers.each do |helper| - @path_helpers_module.send :undef_method, helper + @path_helpers_module.send :remove_method, helper end @url_helpers.each do |helper| - @url_helpers_module.send :undef_method, helper + @url_helpers_module.send :remove_method, helper + end + + @custom_helpers.each do |helper| + path_name = :"#{helper}_path" + url_name = :"#{helper}_url" + + if @path_helpers_module.method_defined?(path_name) + @path_helpers_module.send :remove_method, path_name + end + + if @url_helpers_module.method_defined?(url_name) + @url_helpers_module.send :remove_method, url_name + end end @routes.clear @path_helpers.clear @url_helpers.clear + @custom_helpers.clear end def add(name, route) @@ -143,6 +158,23 @@ module ActionDispatch routes.length end + def add_url_helper(name, defaults, &block) + @custom_helpers << name + helper = CustomUrlHelper.new(name, defaults, &block) + + @path_helpers_module.module_eval do + define_method(:"#{name}_path") do |*args| + helper.call(self, args, only_path: true) + end + end + + @url_helpers_module.module_eval do + define_method(:"#{name}_url") do |*args| + helper.call(self, args) + end + end + end + class UrlHelper def self.create(route, options, route_name, url_strategy) if optimize_helper?(route) @@ -305,7 +337,7 @@ module ActionDispatch attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names attr_accessor :default_url_options - attr_reader :env_key + attr_reader :env_key, :polymorphic_mappings alias :routes :set @@ -347,6 +379,13 @@ module ActionDispatch @set = Journey::Routes.new @router = Journey::Router.new @set @formatter = Journey::Formatter.new self + @polymorphic_mappings = {} + end + + def eager_load! + router.eager_load! + routes.each(&:eager_load!) + nil end def relative_url_root @@ -402,6 +441,7 @@ module ActionDispatch named_routes.clear set.clear formatter.clear + @polymorphic_mappings.clear @prepend.each { |blk| eval_block(blk) } end @@ -446,17 +486,42 @@ module ActionDispatch # Define url_for in the singleton level so one can do: # Rails.application.routes.url_helpers.url_for(args) - @_routes = routes + proxy_class = Class.new do + include UrlFor + include routes.named_routes.path_helpers_module + include routes.named_routes.url_helpers_module + + attr_reader :_routes + + def initialize(routes) + @_routes = routes + end + + def optimize_routes_generation? + @_routes.optimize_routes_generation? + end + end + + @_proxy = proxy_class.new(routes) + class << self def url_for(options) - @_routes.url_for(options) + @_proxy.url_for(options) end def optimize_routes_generation? - @_routes.optimize_routes_generation? + @_proxy.optimize_routes_generation? end - attr_reader :_routes + def polymorphic_url(record_or_hash_or_array, options = {}) + @_proxy.polymorphic_url(record_or_hash_or_array, options) + end + + def polymorphic_path(record_or_hash_or_array, options = {}) + @_proxy.polymorphic_path(record_or_hash_or_array, options) + end + + def _routes; @_proxy._routes; end def url_options; {}; end end @@ -517,20 +582,70 @@ module ActionDispatch if route.segment_keys.include?(:controller) ActiveSupport::Deprecation.warn(<<-MSG.squish) Using a dynamic :controller segment in a route is deprecated and - will be removed in Rails 5.1. + will be removed in Rails 5.2. MSG end if route.segment_keys.include?(:action) ActiveSupport::Deprecation.warn(<<-MSG.squish) Using a dynamic :action segment in a route is deprecated and - will be removed in Rails 5.1. + will be removed in Rails 5.2. MSG end route end + def add_polymorphic_mapping(klass, options, &block) + @polymorphic_mappings[klass] = CustomUrlHelper.new(klass, options, &block) + end + + def add_url_helper(name, options, &block) + named_routes.add_url_helper(name, options, &block) + end + + class CustomUrlHelper + attr_reader :name, :defaults, :block + + def initialize(name, defaults, &block) + @name = name + @defaults = defaults + @block = block + end + + def call(t, args, outer_options = {}) + options = args.extract_options! + url_options = eval_block(t, args, options) + + case url_options + when String + t.url_for(url_options) + when Hash + t.url_for(url_options.merge(outer_options)) + when ActionController::Parameters + if url_options.permitted? + t.url_for(url_options.to_h.merge(outer_options)) + else + raise ArgumentError, "Generating a URL from non sanitized request parameters is insecure!" + end + when Array + opts = url_options.extract_options! + t.url_for(url_options.push(opts.merge(outer_options))) + else + t.url_for([url_options, outer_options]) + end + end + + private + def eval_block(t, args, options) + t.instance_exec(*args, merge_defaults(options), &block) + end + + def merge_defaults(options) + defaults ? defaults.merge(options) : options + end + end + class Generator PARAMETERIZE = lambda do |name, value| if name == :controller diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb new file mode 100644 index 0000000000..99c2be0a35 --- /dev/null +++ b/actionpack/lib/action_dispatch/system_test_case.rb @@ -0,0 +1,119 @@ +require "capybara/dsl" +require "action_controller" +require "action_dispatch/system_testing/driver" +require "action_dispatch/system_testing/server" +require "action_dispatch/system_testing/browser" +require "action_dispatch/system_testing/test_helpers/screenshot_helper" +require "action_dispatch/system_testing/test_helpers/setup_and_teardown" + +module ActionDispatch + class SystemTestCase < IntegrationTest + # = System Testing + # + # System tests let you test applications in the browser. Because system + # tests use a real browser experience, you can test all of your JavaScript + # easily from your test suite. + # + # To create a system test in your application, extend your test class + # from <tt>ApplicationSystemTestCase</tt>. System tests use Capybara as a + # base and allow you to configure the settings through your + # <tt>application_system_test_case.rb</tt> file that is generated with a new + # application or scaffold. + # + # Here is an example system test: + # + # require 'application_system_test_case' + # + # class Users::CreateTest < ApplicationSystemTestCase + # test "adding a new user" do + # visit users_path + # click_on 'New User' + # + # fill_in 'Name', with: 'Arya' + # click_on 'Create User' + # + # assert_text 'Arya' + # end + # end + # + # When generating an application or scaffold, an +application_system_test_case.rb+ + # file will also be generated containing the base class for system testing. + # This is where you can change the driver, add Capybara settings, and other + # configuration for your system tests. + # + # require "test_helper" + # + # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + # driven_by :selenium, using: :chrome, screen_size: [1400, 1400] + # end + # + # By default, <tt>ActionDispatch::SystemTestCase</tt> is driven by the + # Selenium driver, with the Chrome browser, and a browser size of 1400x1400. + # + # Changing the driver configuration options are easy. Let's say you want to use + # the Firefox browser instead of Chrome. In your +application_system_test_case.rb+ + # file add the following: + # + # require "test_helper" + # + # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + # driven_by :selenium, using: :firefox + # end + # + # +driven_by+ has a required argument for the driver name. The keyword + # arguments are +:using+ for the browser and +:screen_size+ to change the + # size of the browser screen. These two options are not applicable for + # headless drivers and will be silently ignored if passed. + # + # To use a headless driver, like Poltergeist, update your Gemfile to use + # Poltergeist instead of Selenium and then declare the driver name in the + # +application_system_test_case.rb+ file. In this case you would leave out the +:using+ + # option because the driver is headless. + # + # require "test_helper" + # require "capybara/poltergeist" + # + # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + # driven_by :poltergeist + # end + # + # Because <tt>ActionDispatch::SystemTestCase</tt> is a shim between Capybara + # and Rails, any driver that is supported by Capybara is supported by system + # tests as long as you include the required gems and files. + include Capybara::DSL + include SystemTesting::TestHelpers::SetupAndTeardown + include SystemTesting::TestHelpers::ScreenshotHelper + + def self.start_application # :nodoc: + Capybara.app = Rack::Builder.new do + map "/" do + run Rails.application + end + end + end + + # System Test configuration options + # + # The default settings are Selenium, using Chrome, with a screen size + # of 1400x1400. + # + # Examples: + # + # driven_by :poltergeist + # + # driven_by :selenium, using: :firefox + # + # driven_by :selenium, screen_size: [800, 800] + def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400]) + SystemTesting::Driver.new(driver).run + SystemTesting::Server.new.run + SystemTesting::Browser.new(using, screen_size).run if selenium?(driver) + end + + def self.selenium?(driver) # :nodoc: + driver == :selenium + end + end + + SystemTestCase.start_application +end diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb new file mode 100644 index 0000000000..c9a6628516 --- /dev/null +++ b/actionpack/lib/action_dispatch/system_testing/browser.rb @@ -0,0 +1,28 @@ +module ActionDispatch + module SystemTesting + class Browser # :nodoc: + def initialize(name, screen_size) + @name = name + @screen_size = screen_size + end + + def run + register + setup + end + + private + def register + Capybara.register_driver @name do |app| + Capybara::Selenium::Driver.new(app, browser: @name).tap do |driver| + driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size) + end + end + end + + def setup + Capybara.default_driver = @name.to_sym + end + end + end +end diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb new file mode 100644 index 0000000000..7c2ad84e19 --- /dev/null +++ b/actionpack/lib/action_dispatch/system_testing/driver.rb @@ -0,0 +1,18 @@ +module ActionDispatch + module SystemTesting + class Driver # :nodoc: + def initialize(name) + @name = name + end + + def run + register + end + + private + def register + Capybara.default_driver = @name + end + end + end +end diff --git a/actionpack/lib/action_dispatch/system_testing/server.rb b/actionpack/lib/action_dispatch/system_testing/server.rb new file mode 100644 index 0000000000..4a214ef713 --- /dev/null +++ b/actionpack/lib/action_dispatch/system_testing/server.rb @@ -0,0 +1,32 @@ +require "rack/handler/puma" + +module ActionDispatch + module SystemTesting + class Server # :nodoc: + def run + register + setup + end + + private + def register + Capybara.register_server :rails_puma do |app, port, host| + Rack::Handler::Puma.run(app, Port: port, Threads: "0:1") + end + end + + def setup + set_server + set_port + end + + def set_server + Capybara.server = :rails_puma + end + + def set_port + Capybara.always_include_port = true + end + end + end +end diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb new file mode 100644 index 0000000000..784005cb93 --- /dev/null +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb @@ -0,0 +1,57 @@ +module ActionDispatch + module SystemTesting + module TestHelpers + # Screenshot helper for system testing + module ScreenshotHelper + # Takes a screenshot of the current page in the browser. + # + # +take_screenshot+ can be used at any point in your system tests to take + # a screenshot of the current state. This can be useful for debugging or + # automating visual testing. + def take_screenshot + save_image + puts "[Screenshot]: #{image_path}" + puts display_image + end + + # Takes a screenshot of the current page in the browser if the test + # failed. + # + # +take_failed_screenshot+ is included in <tt>application_system_test_case.rb</tt> + # that is generated with the application. To take screenshots when a test + # fails add +take_failed_screenshot+ to the teardown block before clearing + # sessions. + def take_failed_screenshot + take_screenshot unless passed? + end + + private + def image_name + passed? ? method_name : "failures_#{method_name}" + end + + def image_path + "tmp/screenshots/#{image_name}.png" + end + + def save_image + page.save_screenshot(Rails.root.join(image_path)) + end + + def display_image + if ENV["CAPYBARA_INLINE_SCREENSHOT"] == "artifact" + "\e]1338;url=artifact://#{image_path}\a" + else + name = inline_base64(File.basename(image_path)) + image = inline_base64(File.read(image_path)) + "\e]1337;File=name=#{name};height=400px;inline=1:#{image}\a" + end + end + + def inline_base64(path) + Base64.encode64(path).gsub("\n", "") + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb new file mode 100644 index 0000000000..491559eedf --- /dev/null +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb @@ -0,0 +1,20 @@ +module ActionDispatch + module SystemTesting + module TestHelpers + module SetupAndTeardown # :nodoc: + DEFAULT_HOST = "127.0.0.1" + + def before_setup + host! DEFAULT_HOST + super + end + + def after_teardown + super + take_failed_screenshot + Capybara.reset_sessions! + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb index b362931ef7..4ea18d671d 100644 --- a/actionpack/lib/action_dispatch/testing/assertions.rb +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -12,7 +12,7 @@ module ActionDispatch include Rails::Dom::Testing::Assertions def html_document - @html_document ||= if @response.content_type.to_s =~ /xml\z/ + @html_document ||= if @response.content_type.to_s.end_with?("xml") Nokogiri::XML::Document.parse(@response.body) else Nokogiri::HTML::Document.parse(@response.body) diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 021ffec862..5fa0b727ab 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -2,7 +2,6 @@ require "stringio" require "uri" require "active_support/core_ext/kernel/singleton_class" require "active_support/core_ext/object/try" -require "active_support/core_ext/string/strip" require "rack/test" require "minitest" @@ -145,8 +144,8 @@ module ActionDispatch self.host = DEFAULT_HOST self.remote_addr = "127.0.0.1" - self.accept = "text/xml,application/xml,application/xhtml+xml," + - "text/html;q=0.9,text/plain;q=0.8,image/png," + + self.accept = "text/xml,application/xml,application/xhtml+xml," \ + "text/html;q=0.9,text/plain;q=0.8,image/png," \ "*/*;q=0.5" unless defined? @named_routes_configured diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 11a9092527..459b0d6c54 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -7,8 +7,8 @@ require "active_support/core_ext/kernel/reporting" # These are the normal settings that will be set up by Railties # TODO: Have these tests support other combinations of these values silence_warnings do - Encoding.default_internal = "UTF-8" - Encoding.default_external = "UTF-8" + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 end require "drb" @@ -259,9 +259,9 @@ module ActionDispatch host = uri_or_host.host unless path path ||= uri_or_host.path - params = { "PATH_INFO" => path, - "REQUEST_METHOD" => method, - "HTTP_HOST" => host } + params = { "PATH_INFO" => path, + "REQUEST_METHOD" => method, + "HTTP_HOST" => host } routes.call(params) end @@ -354,15 +354,6 @@ class CommentsController < ResourcesController; end class AccountsController < ResourcesController; end class ImagesController < ResourcesController; end -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" -end -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) -end - require "active_support/testing/method_call_assertions" class ForkingExecutor @@ -438,4 +429,13 @@ end class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions + + # Skips the current run on Rubinius using Minitest::Assertions#skip + private def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + # Skips the current run on JRuby using Minitest::Assertions#skip + private def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index f9701585a9..5f1463cfa8 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -584,7 +584,7 @@ class FilterTest < ActionController::TestCase assert @controller.instance_variable_get(:@was_audited) end - def test_running_anomolous_yet_valid_condition_actions + def test_running_anomalous_yet_valid_condition_actions test_process(AnomolousYetValidConditionController) assert_equal %w( ensure_login ), @controller.instance_variable_get(:@ran_filter) assert @controller.instance_variable_get(:@ran_class_action) @@ -704,7 +704,7 @@ class FilterTest < ActionController::TestCase def test_prepending_and_appending_around_action test_process(MixedFilterController) - assert_equal " before aroundfilter before procfilter before appended aroundfilter " + + assert_equal " before aroundfilter before procfilter before appended aroundfilter " \ " after appended aroundfilter after procfilter after aroundfilter ", MixedFilterController.execution_log end diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 4fae4071b1..57f58fd835 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -269,8 +269,8 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest test "response cookies are added to the cookie jar for the next request" do with_test_route_set do - self.cookies["cookie_1"] = "sugar" - self.cookies["cookie_2"] = "oatmeal" + cookies["cookie_1"] = "sugar" + cookies["cookie_2"] = "oatmeal" get "/cookie_monster" assert_equal "cookie_1=; path=/\ncookie_3=chocolate; path=/", headers["Set-Cookie"] assert_equal({ "cookie_1" => "", "cookie_2" => "oatmeal", "cookie_3" => "chocolate" }, cookies.to_hash) @@ -494,7 +494,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest assert_includes @response.headers, "c" end - def test_accept_not_overriden_when_xhr_true + def test_accept_not_overridden_when_xhr_true with_test_route_set do get "/get", headers: { "Accept" => "application/json" }, xhr: true assert_equal "application/json", request.accept diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index e76628b936..581081dd07 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -1,4 +1,5 @@ require "abstract_unit" +require "timeout" require "concurrent/atomic/count_down_latch" Thread.abort_on_exception = true diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 818dc119eb..61bd5c80c4 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -519,7 +519,7 @@ class RespondToControllerTest < ActionController::TestCase assert_equal "Whatever you ask for, I got it", @response.body end - def test_handle_any_any_unkown_format + def test_handle_any_any_unknown_format get :handle_any_any, format: "php" assert_equal "Whatever you ask for, I got it", @response.body end diff --git a/actionpack/test/controller/parameters/nested_parameters_permit_test.rb b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb index 5f86901e30..00e591d5a7 100644 --- a/actionpack/test/controller/parameters/nested_parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/nested_parameters_permit_test.rb @@ -140,6 +140,11 @@ class NestedParametersPermitTest < ActiveSupport::TestCase assert_equal "William Shakespeare", permitted[:book][:authors_attributes]["0"][:name] assert_equal "Unattributed Assistant", permitted[:book][:authors_attributes]["1"][:name] + assert_equal( + { "book" => { "authors_attributes" => { "0" => { "name" => "William Shakespeare" }, "1" => { "name" => "Unattributed Assistant" }, "2" => {} } } }, + permitted.to_h + ) + assert_filtered_out permitted[:book][:authors_attributes]["0"], :age_of_death end diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index b62a3d6d7b..8920914af1 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -66,12 +66,20 @@ class ParametersPermitTest < ActiveSupport::TestCase values.each do |value| params = ActionController::Parameters.new(id: value) permitted = params.permit(:id) - assert_equal value, permitted[:id] + if value.nil? + assert_nil permitted[:id] + else + assert_equal value, permitted[:id] + end @struct_fields.each do |sf| params = ActionController::Parameters.new(sf => value) permitted = params.permit(:sf) - assert_equal value, permitted[sf] + if value.nil? + assert_nil permitted[sf] + else + assert_equal value, permitted[sf] + end end end end @@ -187,11 +195,6 @@ class ParametersPermitTest < ActiveSupport::TestCase permitted = params.permit(:username, preferences: {}, hacked: {}) - assert permitted.permitted? - assert permitted[:preferences].permitted? - assert permitted[:preferences][:font].permitted? - assert permitted[:preferences][:dubious].all?(&:permitted?) - assert_equal "fxn", permitted[:username] assert_equal "Marazul", permitted[:preferences][:scheme] assert_equal "Source Code Pro", permitted[:preferences][:font][:name] diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index e4e968dfdb..f06a1f4d23 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -21,8 +21,8 @@ end class RedirectController < ActionController::Base # empty method not used anywhere to ensure methods like # `status` and `location` aren't called on `redirect_to` calls - def status; render plain: "called status"; end - def location; render plain: "called location"; end + def status; raise "Should not be called!"; end + def location; raise "Should not be called!"; end def simple_redirect redirect_to action: "hello_world" diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 0c0f18f200..3a0a0a8bde 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -313,7 +313,6 @@ class ExpiresInRenderTest < ActionController::TestCase end def test_permitted_dynamic_render_file_hash - skip "FIXME: this test passes on 4-2-stable but not master. Why?" assert File.exist?(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")) response = get :dynamic_render_permit, params: { id: { file: '../\\../test/abstract_unit.rb' } } assert_equal File.read(File.join(File.dirname(__FILE__), "../../test/abstract_unit.rb")), diff --git a/actionpack/test/controller/request/test_request_test.rb b/actionpack/test/controller/request/test_request_test.rb index b67ae72c0a..1440db00f6 100644 --- a/actionpack/test/controller/request/test_request_test.rb +++ b/actionpack/test/controller/request/test_request_test.rb @@ -21,16 +21,20 @@ class ActionController::TestRequestTest < ActionController::TestCase @request.get_header("CONTENT_LENGTH")) end - ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS.each_key do |option| - test "rack default session options #{option} exists in session options and is default" do - assert_equal(ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS[option], - @request.session_options[option], - "Missing rack session default option #{option} in request.session_options") + ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS.each_pair do |key, value| + test "rack default session options #{key} exists in session options and is default" do + if value.nil? + assert_nil(@request.session_options[key], + "Missing rack session default option #{key} in request.session_options") + else + assert_equal(value, @request.session_options[key], + "Missing rack session default option #{key} in request.session_options") + end end - test "rack default session options #{option} exists in session options" do - assert(@request.session_options.has_key?(option), - "Missing rack session option #{option} in request.session_options") + test "rack default session options #{key} exists in session options" do + assert(@request.session_options.has_key?(key), + "Missing rack session option #{key} in request.session_options") end end end diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 874f9c3c42..891ce0e905 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -134,7 +134,7 @@ XML end def create - head :created, location: "created resource" + head :created, location: "/resource" end def render_cookie @@ -728,6 +728,20 @@ XML assert_equal "text/html", @response.body end + def test_request_path_info_and_format_reset + get :test_format, format: "json" + assert_equal "application/json", @response.body + + get :test_uri, format: "json" + assert_equal "/test_case_test/test/test_uri.json", @response.body + + get :test_format + assert_equal "text/html", @response.body + + get :test_uri + assert_equal "/test_case_test/test/test_uri", @response.body + end + def test_request_format_kwarg_overrides_params get :test_format, format: "json", params: { format: "html" } assert_equal "application/json", @response.body @@ -796,7 +810,6 @@ XML new_content_type = "new content_type" file.content_type = new_content_type assert_equal new_content_type, file.content_type - end def test_fixture_path_is_accessed_from_self_instead_of_active_support_test_case @@ -880,12 +893,12 @@ XML assert_response :created # Redirect url doesn't care that it wasn't a :redirect response. - assert_equal "created resource", @response.redirect_url + assert_equal "/resource", @response.redirect_url assert_equal @response.redirect_url, redirect_to_url # Must be a :redirect response. assert_raise(ActiveSupport::TestCase::Assertion) do - assert_redirected_to "created resource" + assert_redirected_to "/resource" end end diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 382b4e273d..862dcf01c3 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -26,7 +26,7 @@ module AbstractController action: :index } }.url_helpers - self.default_url_options[:host] = "example.com" + default_url_options[:host] = "example.com" } path = klass.new.fun_path(controller: :articles, diff --git a/actionpack/test/dispatch/callbacks_test.rb b/actionpack/test/dispatch/callbacks_test.rb index 57e21a22c6..29a5dfc0ad 100644 --- a/actionpack/test/dispatch/callbacks_test.rb +++ b/actionpack/test/dispatch/callbacks_test.rb @@ -35,24 +35,6 @@ class DispatcherTest < ActiveSupport::TestCase assert_equal 6, Foo.b end - def test_to_prepare_and_cleanup_delegation - prepared = cleaned = false - assert_deprecated do - ActionDispatch::Callbacks.to_prepare { prepared = true } - ActionDispatch::Callbacks.to_prepare { cleaned = true } - end - - assert_deprecated do - ActionDispatch::Reloader.prepare! - end - assert prepared - - assert_deprecated do - ActionDispatch::Reloader.cleanup! - end - assert cleaned - end - private def dispatch(&block) diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index bb2fc53add..0e093d2188 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -22,8 +22,6 @@ module TestGenerationPrefix end class WithMountedEngine < ActionDispatch::IntegrationTest - include Rack::Test::Methods - class BlogEngine < Rails::Engine routes.draw do get "/posts/:id", to: "inside_engine_generating#show", as: :post @@ -153,114 +151,114 @@ module TestGenerationPrefix # Inside Engine test "[ENGINE] generating engine's url use SCRIPT_NAME from request" do get "/pure-awesomeness/blog/posts/1" - assert_equal "/pure-awesomeness/blog/posts/1", last_response.body + assert_equal "/pure-awesomeness/blog/posts/1", response.body end test "[ENGINE] generating application's url never uses SCRIPT_NAME from request" do get "/pure-awesomeness/blog/url_to_application" - assert_equal "/generate", last_response.body + assert_equal "/generate", response.body end test "[ENGINE] generating engine's url with polymorphic path" do get "/pure-awesomeness/blog/polymorphic_path_for_engine" - assert_equal "/pure-awesomeness/blog/posts/1", last_response.body + assert_equal "/pure-awesomeness/blog/posts/1", response.body end test "[ENGINE] url_helpers from engine have higher priority than application's url_helpers" do get "/awesome/blog/conflicting_url" - assert_equal "engine", last_response.body + assert_equal "engine", response.body end test "[ENGINE] relative path root uses SCRIPT_NAME from request" do get "/awesome/blog/relative_path_root" - verify_redirect "http://example.org/awesome/blog" + verify_redirect "http://www.example.com/awesome/blog" end test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do get "/awesome/blog/relative_path_redirect" - verify_redirect "http://example.org/awesome/blog/foo" + verify_redirect "http://www.example.com/awesome/blog/foo" end test "[ENGINE] relative option root uses SCRIPT_NAME from request" do get "/awesome/blog/relative_option_root" - verify_redirect "http://example.org/awesome/blog" + verify_redirect "http://www.example.com/awesome/blog" end test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do get "/awesome/blog/relative_option_redirect" - verify_redirect "http://example.org/awesome/blog/foo" + verify_redirect "http://www.example.com/awesome/blog/foo" end test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do get "/awesome/blog/relative_custom_root" - verify_redirect "http://example.org/awesome/blog" + verify_redirect "http://www.example.com/awesome/blog" end test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do get "/awesome/blog/relative_custom_redirect" - verify_redirect "http://example.org/awesome/blog/foo" + verify_redirect "http://www.example.com/awesome/blog/foo" end test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_path_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_path_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_option_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_option_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_custom_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do get "/awesome/blog/absolute_custom_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end # Inside Application test "[APP] generating engine's route includes prefix" do get "/generate" - assert_equal "/awesome/blog/posts/1", last_response.body + assert_equal "/awesome/blog/posts/1", response.body end test "[APP] generating engine's route includes default_url_options[:script_name]" do RailsApplication.routes.default_url_options = { script_name: "/something" } get "/generate" - assert_equal "/something/awesome/blog/posts/1", last_response.body + assert_equal "/something/awesome/blog/posts/1", response.body end test "[APP] generating engine's url with polymorphic path" do get "/polymorphic_path_for_engine" - assert_equal "/awesome/blog/posts/1", last_response.body + assert_equal "/awesome/blog/posts/1", response.body end test "polymorphic_path_for_app" do get "/polymorphic_path_for_app" - assert_equal "/posts/1", last_response.body + assert_equal "/posts/1", response.body end test "[APP] generating engine's url with url_for(@post)" do get "/polymorphic_with_url_for" - assert_equal "http://example.org/awesome/blog/posts/1", last_response.body + assert_equal "http://www.example.com/awesome/blog/posts/1", response.body end test "[APP] instance variable with same name as engine" do get "/ivar_usage" - assert_equal "/awesome/blog/posts/1", last_response.body + assert_equal "/awesome/blog/posts/1", response.body end # Inside any Object @@ -322,9 +320,9 @@ module TestGenerationPrefix private def verify_redirect(url, status = 301) - assert_equal status, last_response.status - assert_equal url, last_response.headers["Location"] - assert_equal expected_redirect_body(url), last_response.body + assert_equal status, response.status + assert_equal url, response.headers["Location"] + assert_equal expected_redirect_body(url), response.body end def expected_redirect_body(url) @@ -333,8 +331,6 @@ module TestGenerationPrefix end class EngineMountedAtRoot < ActionDispatch::IntegrationTest - include Rack::Test::Methods - class BlogEngine def self.routes @routes ||= begin @@ -388,74 +384,74 @@ module TestGenerationPrefix test "generating path inside engine" do get "/posts/1" - assert_equal "/posts/1", last_response.body + assert_equal "/posts/1", response.body end test "[ENGINE] relative path root uses SCRIPT_NAME from request" do get "/relative_path_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do get "/relative_path_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] relative option root uses SCRIPT_NAME from request" do get "/relative_option_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do get "/relative_option_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do get "/relative_custom_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do get "/relative_custom_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do get "/absolute_path_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do get "/absolute_path_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do get "/absolute_option_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do get "/absolute_option_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do get "/absolute_custom_root" - verify_redirect "http://example.org/" + verify_redirect "http://www.example.com/" end test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do get "/absolute_custom_redirect" - verify_redirect "http://example.org/foo" + verify_redirect "http://www.example.com/foo" end private def verify_redirect(url, status = 301) - assert_equal status, last_response.status - assert_equal url, last_response.headers["Location"] - assert_equal expected_redirect_body(url), last_response.body + assert_equal status, response.status + assert_equal url, response.headers["Location"] + assert_equal expected_redirect_body(url), response.body end def expected_redirect_body(url) diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb index db68549b84..9eb78fe059 100644 --- a/actionpack/test/dispatch/reloader_test.rb +++ b/actionpack/test/dispatch/reloader_test.rb @@ -1,32 +1,11 @@ require "abstract_unit" class ReloaderTest < ActiveSupport::TestCase - Reloader = ActionDispatch::Reloader - teardown do ActiveSupport::Reloader.reset_callbacks :prepare ActiveSupport::Reloader.reset_callbacks :complete end - def test_prepare_callbacks - a = b = c = nil - assert_deprecated do - Reloader.to_prepare { |*args| a = b = c = 1 } - Reloader.to_prepare { |*args| b = c = 2 } - Reloader.to_prepare { |*args| c = 3 } - end - - # Ensure to_prepare callbacks are not run when defined - assert_nil a || b || c - - # Run callbacks - call_and_return_body - - assert_equal 1, a - assert_equal 2, b - assert_equal 3, c - end - class MyBody < Array def initialize(&block) @on_close = block @@ -45,6 +24,23 @@ class ReloaderTest < ActiveSupport::TestCase end end + def test_prepare_callbacks + a = b = c = nil + reloader.to_prepare { |*args| a = b = c = 1 } + reloader.to_prepare { |*args| b = c = 2 } + reloader.to_prepare { |*args| c = 3 } + + # Ensure to_prepare callbacks are not run when defined + assert_nil a || b || c + + # Run callbacks + call_and_return_body + + assert_equal 1, a + assert_equal 2, b + assert_equal 3, c + end + def test_returned_body_object_always_responds_to_close body = call_and_return_body assert_respond_to body, :close @@ -62,15 +58,12 @@ class ReloaderTest < ActiveSupport::TestCase def test_condition_specifies_when_to_reload i, j = 0, 0, 0, 0 - assert_deprecated do - Reloader.to_prepare { |*args| i += 1 } - Reloader.to_cleanup { |*args| j += 1 } - end - x = Class.new(ActiveSupport::Reloader) - x.check = lambda { i < 3 } + reloader = reloader(lambda { i < 3 }) + reloader.to_prepare { |*args| i += 1 } + reloader.to_complete { |*args| j += 1 } - app = Reloader.new(lambda { |env| [200, {}, []] }, x) + app = middleware(lambda { |env| [200, {}, []] }, reloader) 5.times do resp = app.call({}) resp[2].close @@ -115,24 +108,20 @@ class ReloaderTest < ActiveSupport::TestCase assert_respond_to body, :bar end - def test_cleanup_callbacks_are_called_when_body_is_closed - cleaned = false - assert_deprecated do - Reloader.to_cleanup { cleaned = true } - end + def test_complete_callbacks_are_called_when_body_is_closed + completed = false + reloader.to_complete { completed = true } body = call_and_return_body - assert !cleaned + assert !completed body.close - assert cleaned + assert completed end def test_prepare_callbacks_arent_called_when_body_is_closed prepared = false - assert_deprecated do - Reloader.to_prepare { prepared = true } - end + reloader.to_prepare { prepared = true } body = call_and_return_body prepared = false @@ -141,45 +130,9 @@ class ReloaderTest < ActiveSupport::TestCase assert !prepared end - def test_manual_reloading - prepared = cleaned = false - assert_deprecated do - Reloader.to_prepare { prepared = true } - Reloader.to_cleanup { cleaned = true } - end - - assert_deprecated do - Reloader.prepare! - end - assert prepared - assert !cleaned - - prepared = cleaned = false - assert_deprecated do - Reloader.cleanup! - end - assert prepared - assert cleaned - end - - def test_prepend_prepare_callback - i = 10 - assert_deprecated do - Reloader.to_prepare { i += 1 } - Reloader.to_prepare(prepend: true) { i = 0 } - end - - assert_deprecated do - Reloader.prepare! - end - assert_equal 1, i - end - - def test_cleanup_callbacks_are_called_on_exceptions - cleaned = false - assert_deprecated do - Reloader.to_cleanup { cleaned = true } - end + def test_complete_callbacks_are_called_on_exceptions + completed = false + reloader.to_complete { completed = true } begin call_and_return_body do @@ -188,16 +141,25 @@ class ReloaderTest < ActiveSupport::TestCase rescue end - assert cleaned + assert completed end private def call_and_return_body(&block) - x = Class.new(ActiveSupport::Reloader) - x.check = lambda { true } + app = middleware(block || proc { [200, {}, "response"] }) + _, _, body = app.call("rack.input" => StringIO.new("")) + body + end + + def middleware(inner_app, reloader = reloader()) + ActionDispatch::Reloader.new(inner_app, reloader) + end - @response ||= "response" - @reloader ||= Reloader.new(block || proc { [200, {}, @response] }, x) - @reloader.call("rack.input" => StringIO.new(""))[2] + def reloader(check = lambda { true }) + @reloader ||= begin + reloader = Class.new(ActiveSupport::Reloader) + reloader.check = check + reloader + end end end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index e11b93b4f0..2f9228a62d 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -760,8 +760,8 @@ class RequestMethod < BaseRequestTest end test "post uneffected by local inflections" do - existing_acrnoyms = ActiveSupport::Inflector.inflections.acronyms.dup - existing_acrnoym_regex = ActiveSupport::Inflector.inflections.acronym_regex.dup + existing_acronyms = ActiveSupport::Inflector.inflections.acronyms.dup + existing_acronym_regex = ActiveSupport::Inflector.inflections.acronym_regex.dup begin ActiveSupport::Inflector.inflections do |inflect| inflect.acronym "POS" @@ -774,8 +774,8 @@ class RequestMethod < BaseRequestTest ensure # Reset original acronym set ActiveSupport::Inflector.inflections do |inflect| - inflect.send(:instance_variable_set, "@acronyms", existing_acrnoyms) - inflect.send(:instance_variable_set, "@acronym_regex", existing_acrnoym_regex) + inflect.send(:instance_variable_set, "@acronyms", existing_acronyms) + inflect.send(:instance_variable_set, "@acronym_regex", existing_acronym_regex) end end end diff --git a/actionpack/test/dispatch/routing/custom_url_helpers_test.rb b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb new file mode 100644 index 0000000000..6d230a2557 --- /dev/null +++ b/actionpack/test/dispatch/routing/custom_url_helpers_test.rb @@ -0,0 +1,291 @@ +require "abstract_unit" + +class TestCustomUrlHelpers < ActionDispatch::IntegrationTest + class Linkable + attr_reader :id + + def initialize(id) + @id = id + end + + def linkable_type + self.class.name.demodulize.underscore + end + end + + class Category < Linkable; end + class Collection < Linkable; end + class Product < Linkable; end + + class Model + extend ActiveModel::Naming + include ActiveModel::Conversion + + attr_reader :id + + def initialize(id = nil) + @id = id + end + + remove_method :model_name + def model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, self.class.name.demodulize) + end + + def persisted? + false + end + end + + class Basket < Model; end + class User < Model; end + class Video < Model; end + + class Article + attr_reader :id + + def self.name + "Article" + end + + def initialize(id) + @id = id + end + end + + class Page + attr_reader :id + + def self.name + super.demodulize + end + + def initialize(id) + @id = id + end + end + + class CategoryPage < Page; end + class ProductPage < Page; end + + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + default_url_options host: "www.example.com" + + root to: "pages#index" + get "/basket", to: "basket#show", as: :basket + get "/posts/:id", to: "posts#show", as: :post + get "/profile", to: "users#profile", as: :profile + get "/media/:id", to: "media#show", as: :media + get "/pages/:id", to: "pages#show", as: :page + + resources :categories, :collections, :products + + namespace :admin do + get "/dashboard", to: "dashboard#index" + end + + direct(:website) { "http://www.rubyonrails.org" } + direct("string") { "http://www.rubyonrails.org" } + direct(:helper) { basket_url } + direct(:linkable) { |linkable| [:"#{linkable.linkable_type}", { id: linkable.id }] } + direct(:params) { |params| params } + direct(:symbol) { :basket } + direct(:hash) { { controller: "basket", action: "show" } } + direct(:array) { [:admin, :dashboard] } + direct(:options) { |options| [:products, options] } + direct(:defaults, size: 10) { |options| [:products, options] } + + resolve("Article") { |article| [:post, { id: article.id }] } + resolve("Basket") { |basket| [:basket] } + resolve("User", anchor: "details") { |user, options| [:profile, options] } + resolve("Video") { |video| [:media, { id: video.id }] } + resolve(%w[Page CategoryPage ProductPage]) { |page| [:page, { id: page.id }] } + end + + APP = build_app Routes + + def app + APP + end + + include Routes.url_helpers + + def setup + @category = Category.new("1") + @collection = Collection.new("2") + @product = Product.new("3") + @basket = Basket.new + @user = User.new + @video = Video.new("4") + @article = Article.new("5") + @page = Page.new("6") + @category_page = CategoryPage.new("7") + @product_page = ProductPage.new("8") + @path_params = { "controller" => "pages", "action" => "index" } + @unsafe_params = ActionController::Parameters.new(@path_params) + @safe_params = ActionController::Parameters.new(@path_params).permit(:controller, :action) + end + + def test_direct_paths + assert_equal "http://www.rubyonrails.org", website_path + assert_equal "http://www.rubyonrails.org", Routes.url_helpers.website_path + + assert_equal "http://www.rubyonrails.org", string_path + assert_equal "http://www.rubyonrails.org", Routes.url_helpers.string_path + + assert_equal "http://www.example.com/basket", helper_url + assert_equal "http://www.example.com/basket", Routes.url_helpers.helper_url + + assert_equal "/categories/1", linkable_path(@category) + assert_equal "/categories/1", Routes.url_helpers.linkable_path(@category) + assert_equal "/collections/2", linkable_path(@collection) + assert_equal "/collections/2", Routes.url_helpers.linkable_path(@collection) + assert_equal "/products/3", linkable_path(@product) + assert_equal "/products/3", Routes.url_helpers.linkable_path(@product) + + assert_equal "/", params_path(@safe_params) + assert_equal "/", Routes.url_helpers.params_path(@safe_params) + assert_raises(ArgumentError) { params_path(@unsafe_params) } + assert_raises(ArgumentError) { Routes.url_helpers.params_path(@unsafe_params) } + + assert_equal "/basket", symbol_path + assert_equal "/basket", Routes.url_helpers.symbol_path + assert_equal "/basket", hash_path + assert_equal "/basket", Routes.url_helpers.hash_path + assert_equal "/admin/dashboard", array_path + assert_equal "/admin/dashboard", Routes.url_helpers.array_path + + assert_equal "/products?page=2", options_path(page: 2) + assert_equal "/products?page=2", Routes.url_helpers.options_path(page: 2) + assert_equal "/products?size=10", defaults_path + assert_equal "/products?size=10", Routes.url_helpers.defaults_path + assert_equal "/products?size=20", defaults_path(size: 20) + assert_equal "/products?size=20", Routes.url_helpers.defaults_path(size: 20) + end + + def test_direct_urls + assert_equal "http://www.rubyonrails.org", website_url + assert_equal "http://www.rubyonrails.org", Routes.url_helpers.website_url + + assert_equal "http://www.rubyonrails.org", string_url + assert_equal "http://www.rubyonrails.org", Routes.url_helpers.string_url + + assert_equal "http://www.example.com/basket", helper_url + assert_equal "http://www.example.com/basket", Routes.url_helpers.helper_url + + assert_equal "http://www.example.com/categories/1", linkable_url(@category) + assert_equal "http://www.example.com/categories/1", Routes.url_helpers.linkable_url(@category) + assert_equal "http://www.example.com/collections/2", linkable_url(@collection) + assert_equal "http://www.example.com/collections/2", Routes.url_helpers.linkable_url(@collection) + assert_equal "http://www.example.com/products/3", linkable_url(@product) + assert_equal "http://www.example.com/products/3", Routes.url_helpers.linkable_url(@product) + + assert_equal "http://www.example.com/", params_url(@safe_params) + assert_equal "http://www.example.com/", Routes.url_helpers.params_url(@safe_params) + assert_raises(ArgumentError) { params_url(@unsafe_params) } + assert_raises(ArgumentError) { Routes.url_helpers.params_url(@unsafe_params) } + + assert_equal "http://www.example.com/basket", symbol_url + assert_equal "http://www.example.com/basket", Routes.url_helpers.symbol_url + assert_equal "http://www.example.com/basket", hash_url + assert_equal "http://www.example.com/basket", Routes.url_helpers.hash_url + assert_equal "http://www.example.com/admin/dashboard", array_url + assert_equal "http://www.example.com/admin/dashboard", Routes.url_helpers.array_url + + assert_equal "http://www.example.com/products?page=2", options_url(page: 2) + assert_equal "http://www.example.com/products?page=2", Routes.url_helpers.options_url(page: 2) + assert_equal "http://www.example.com/products?size=10", defaults_url + assert_equal "http://www.example.com/products?size=10", Routes.url_helpers.defaults_url + assert_equal "http://www.example.com/products?size=20", defaults_url(size: 20) + assert_equal "http://www.example.com/products?size=20", Routes.url_helpers.defaults_url(size: 20) + end + + def test_resolve_paths + assert_equal "/basket", polymorphic_path(@basket) + assert_equal "/basket", Routes.url_helpers.polymorphic_path(@basket) + + assert_equal "/profile#details", polymorphic_path(@user) + assert_equal "/profile#details", Routes.url_helpers.polymorphic_path(@user) + + assert_equal "/profile#password", polymorphic_path(@user, anchor: "password") + assert_equal "/profile#password", Routes.url_helpers.polymorphic_path(@user, anchor: "password") + + assert_equal "/media/4", polymorphic_path(@video) + assert_equal "/media/4", Routes.url_helpers.polymorphic_path(@video) + assert_equal "/media/4", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @video) + + assert_equal "/posts/5", polymorphic_path(@article) + assert_equal "/posts/5", Routes.url_helpers.polymorphic_path(@article) + assert_equal "/posts/5", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @article) + + assert_equal "/pages/6", polymorphic_path(@page) + assert_equal "/pages/6", Routes.url_helpers.polymorphic_path(@page) + assert_equal "/pages/6", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @page) + + assert_equal "/pages/7", polymorphic_path(@category_page) + assert_equal "/pages/7", Routes.url_helpers.polymorphic_path(@category_page) + assert_equal "/pages/7", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @category_page) + + assert_equal "/pages/8", polymorphic_path(@product_page) + assert_equal "/pages/8", Routes.url_helpers.polymorphic_path(@product_page) + assert_equal "/pages/8", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.path.handle_model_call(self, @product_page) + end + + def test_resolve_urls + assert_equal "http://www.example.com/basket", polymorphic_url(@basket) + assert_equal "http://www.example.com/basket", Routes.url_helpers.polymorphic_url(@basket) + assert_equal "http://www.example.com/basket", polymorphic_url(@basket) + assert_equal "http://www.example.com/basket", Routes.url_helpers.polymorphic_url(@basket) + + assert_equal "http://www.example.com/profile#details", polymorphic_url(@user) + assert_equal "http://www.example.com/profile#details", Routes.url_helpers.polymorphic_url(@user) + + assert_equal "http://www.example.com/profile#password", polymorphic_url(@user, anchor: "password") + assert_equal "http://www.example.com/profile#password", Routes.url_helpers.polymorphic_url(@user, anchor: "password") + + assert_equal "http://www.example.com/media/4", polymorphic_url(@video) + assert_equal "http://www.example.com/media/4", Routes.url_helpers.polymorphic_url(@video) + assert_equal "http://www.example.com/media/4", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @video) + + assert_equal "http://www.example.com/posts/5", polymorphic_url(@article) + assert_equal "http://www.example.com/posts/5", Routes.url_helpers.polymorphic_url(@article) + assert_equal "http://www.example.com/posts/5", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @article) + + assert_equal "http://www.example.com/pages/6", polymorphic_url(@page) + assert_equal "http://www.example.com/pages/6", Routes.url_helpers.polymorphic_url(@page) + assert_equal "http://www.example.com/pages/6", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @page) + + assert_equal "http://www.example.com/pages/7", polymorphic_url(@category_page) + assert_equal "http://www.example.com/pages/7", Routes.url_helpers.polymorphic_url(@category_page) + assert_equal "http://www.example.com/pages/7", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @category_page) + + assert_equal "http://www.example.com/pages/8", polymorphic_url(@product_page) + assert_equal "http://www.example.com/pages/8", Routes.url_helpers.polymorphic_url(@product_page) + assert_equal "http://www.example.com/pages/8", ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.url.handle_model_call(self, @product_page) + end + + def test_defining_direct_inside_a_scope_raises_runtime_error + routes = ActionDispatch::Routing::RouteSet.new + + assert_raises RuntimeError do + routes.draw do + namespace :admin do + direct(:rubyonrails) { "http://www.rubyonrails.org" } + end + end + end + end + + def test_defining_resolve_inside_a_scope_raises_runtime_error + routes = ActionDispatch::Routing::RouteSet.new + + assert_raises RuntimeError do + routes.draw do + namespace :admin do + resolve("User") { "/profile" } + end + end + end + end +end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 474d053af6..53758a4fbc 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -1603,7 +1603,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get "account/login", to: redirect("/login") end - previous_host, self.host = self.host, "www.example.com:3000" + previous_host, self.host = host, "www.example.com:3000" get "/account/login" verify_redirect "http://www.example.com:3000/login" diff --git a/actionpack/test/dispatch/runner_test.rb b/actionpack/test/dispatch/runner_test.rb index 969933c9ed..b76bf4a320 100644 --- a/actionpack/test/dispatch/runner_test.rb +++ b/actionpack/test/dispatch/runner_test.rb @@ -4,7 +4,6 @@ class RunnerTest < ActiveSupport::TestCase test "runner preserves the setting of integration_session" do runner = Class.new do def before_setup - end end.new diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index 2a1053be16..63dfc07c0d 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -175,7 +175,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest def test_doesnt_write_session_cookie_if_session_is_unchanged with_test_route_set do - cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--" + + cookies[SessionKey] = "BAh7BjoIZm9vIghiYXI%3D--" \ "fef868465920f415f2c0652d6910d3af288a0367" get "/no_session_access" assert_response :success diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb index e29ffa750c..757e26973f 100644 --- a/actionpack/test/dispatch/ssl_test.rb +++ b/actionpack/test/dispatch/ssl_test.rb @@ -102,7 +102,11 @@ class StrictTransportSecurityTest < SSLTest def assert_hsts(expected, url: "https://example.org", hsts: { subdomains: true }, headers: {}) self.app = build_app ssl_options: { hsts: hsts }, headers: headers get url - assert_equal expected, response.headers["Strict-Transport-Security"] + if expected.nil? + assert_nil response.headers["Strict-Transport-Security"] + else + assert_equal expected, response.headers["Strict-Transport-Security"] + end end test "enabled by default" do @@ -130,7 +134,7 @@ class StrictTransportSecurityTest < SSLTest end test ":expires supports AS::Duration arguments" do - assert_hsts "max-age=31557600; includeSubDomains", hsts: { expires: 1.year } + assert_hsts "max-age=31556952; includeSubDomains", hsts: { expires: 1.year } end test "include subdomains" do diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index d8bc96e3e0..bd8318f5f6 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -163,7 +163,7 @@ module StaticTests assert_equal file_name, env["PATH_INFO"] end - def test_serves_gzip_with_propper_content_type_fallback + def test_serves_gzip_with_proper_content_type_fallback file_name = "/gzip/foo.zoo" response = get(file_name, "HTTP_ACCEPT_ENCODING" => "gzip") assert_gzip file_name, response diff --git a/actionpack/test/dispatch/system_testing/browser_test.rb b/actionpack/test/dispatch/system_testing/browser_test.rb new file mode 100644 index 0000000000..b0ad309492 --- /dev/null +++ b/actionpack/test/dispatch/system_testing/browser_test.rb @@ -0,0 +1,10 @@ +require "abstract_unit" +require "action_dispatch/system_testing/browser" + +class BrowserTest < ActiveSupport::TestCase + test "initializing the browser" do + browser = ActionDispatch::SystemTesting::Browser.new(:chrome, [ 1400, 1400 ]) + assert_equal :chrome, browser.instance_variable_get(:@name) + assert_equal [ 1400, 1400 ], browser.instance_variable_get(:@screen_size) + end +end diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb new file mode 100644 index 0000000000..f0ebdb38db --- /dev/null +++ b/actionpack/test/dispatch/system_testing/driver_test.rb @@ -0,0 +1,9 @@ +require "abstract_unit" +require "action_dispatch/system_testing/driver" + +class DriverTest < ActiveSupport::TestCase + test "initializing the driver" do + driver = ActionDispatch::SystemTesting::Driver.new(:selenium) + assert_equal :selenium, driver.instance_variable_get(:@name) + end +end diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb new file mode 100644 index 0000000000..8c14f799b0 --- /dev/null +++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb @@ -0,0 +1,18 @@ +require "abstract_unit" +require "action_dispatch/system_testing/test_helpers/screenshot_helper" + +class ScreenshotHelperTest < ActiveSupport::TestCase + test "image path is saved in tmp directory" do + new_test = ActionDispatch::SystemTestCase.new("x") + + assert_equal "tmp/screenshots/x.png", new_test.send(:image_path) + end + + test "image path includes failures text if test did not pass" do + new_test = ActionDispatch::SystemTestCase.new("x") + + new_test.stub :passed?, false do + assert_equal "tmp/screenshots/failures_x.png", new_test.send(:image_path) + end + end +end diff --git a/actionpack/test/dispatch/system_testing/server_test.rb b/actionpack/test/dispatch/system_testing/server_test.rb new file mode 100644 index 0000000000..10412d6367 --- /dev/null +++ b/actionpack/test/dispatch/system_testing/server_test.rb @@ -0,0 +1,17 @@ +require "abstract_unit" +require "capybara/dsl" +require "action_dispatch/system_testing/server" + +class ServerTest < ActiveSupport::TestCase + setup do + ActionDispatch::SystemTesting::Server.new.run + end + + test "initializing the server port" do + assert_includes Capybara.servers, :rails_puma + end + + test "port is always included" do + assert Capybara.always_include_port, "expected Capybara.always_include_port to be true" + end +end diff --git a/actionpack/test/dispatch/system_testing/system_test_case_test.rb b/actionpack/test/dispatch/system_testing/system_test_case_test.rb new file mode 100644 index 0000000000..a384902a14 --- /dev/null +++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb @@ -0,0 +1,21 @@ +require "abstract_unit" + +class SystemTestCaseTest < ActiveSupport::TestCase + test "driven_by sets Capybara's default driver to poltergeist" do + ActionDispatch::SystemTestCase.driven_by :poltergeist + + assert_equal :poltergeist, Capybara.default_driver + end + + test "driven_by sets Capybara's drivers respectively" do + ActionDispatch::SystemTestCase.driven_by :selenium, using: :chrome + + assert_includes Capybara.drivers, :selenium + assert_includes Capybara.drivers, :chrome + assert_equal :chrome, Capybara.default_driver + end + + test "selenium? returns false if driver is poltergeist" do + assert_not ActionDispatch::SystemTestCase.selenium?(:poltergeist) + end +end diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb index d61a8c023a..2c74617944 100644 --- a/actionpack/test/journey/path/pattern_test.rb +++ b/actionpack/test/journey/path/pattern_test.rb @@ -20,7 +20,7 @@ module ActionDispatch "/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar\Z}, "/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))\Z}, }.each do |path, expected| - define_method(:"test_to_regexp_#{path}") do + define_method(:"test_to_regexp_#{Regexp.escape(path)}") do path = Pattern.build( path, { controller: /.+/ }, @@ -44,7 +44,7 @@ module ActionDispatch "/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar}, "/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))}, }.each do |path, expected| - define_method(:"test_to_non_anchored_regexp_#{path}") do + define_method(:"test_to_non_anchored_regexp_#{Regexp.escape(path)}") do path = Pattern.build( path, { controller: /.+/ }, @@ -67,7 +67,7 @@ module ActionDispatch "/:controller/*foo" => %w{ controller foo }, "/:controller/*foo/bar" => %w{ controller foo }, }.each do |path, expected| - define_method(:"test_names_#{path}") do + define_method(:"test_names_#{Regexp.escape(path)}") do path = Pattern.build( path, { controller: /.+/ }, diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb index 046b4167bb..b768553e7a 100644 --- a/actionpack/test/lib/controller/fake_models.rb +++ b/actionpack/test/lib/controller/fake_models.rb @@ -1,6 +1,6 @@ require "active_model" -class Customer < Struct.new(:name, :id) +Customer = Struct.new(:name, :id) do extend ActiveModel::Naming include ActiveModel::Conversion @@ -28,7 +28,7 @@ class Customer < Struct.new(:name, :id) end end -class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) +Post = Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) do extend ActiveModel::Naming include ActiveModel::Conversion extend ActiveModel::Translation diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 2a38ca7b63..b071b260c9 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,27 @@ +* Change the ERB handler from Erubis to Erubi. + + Erubi is an Erubis fork that's svelte, simple, and currently maintained. + Plus it supports `--enable-frozen-string-literal` in Ruby 2.3+. + + Compatibility: Drops support for `<%===` tags for debug output. + These were an unused, undocumented side effect of the Erubis + implementation. + + Deprecation: The Erubis handler will be removed in Rails 5.2, for the + handful of folks using it directly. + + *Jeremy Evans* + +* Allow render locals to be assigned to instance variables in a view. + + Fixes #27480. + + *Andrew White* + +* Add `check_parameters` option to `current_page?` which makes it more strict. + + *Maksym Pugach* + * Return correct object name in form helper method after `fields_for`. Fixes #26931. diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index 7bfdfbe29a..cfaa5007a1 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -22,8 +22,8 @@ Gem::Specification.new do |s| s.add_dependency "activesupport", version s.add_dependency "builder", "~> 3.1" - s.add_dependency "erubis", "~> 2.7.0" - s.add_dependency "rails-html-sanitizer", "~> 1.0", ">= 1.0.2" + s.add_dependency "erubi", "~> 1.4" + s.add_dependency "rails-html-sanitizer", "~> 1.0", ">= 1.0.3" s.add_dependency "rails-dom-testing", "~> 2.0" s.add_development_dependency "actionpack", version diff --git a/actionview/app/assets/javascripts/config.coffee b/actionview/app/assets/javascripts/config.coffee index 3d4706b0e1..a93325e903 100644 --- a/actionview/app/assets/javascripts/config.coffee +++ b/actionview/app/assets/javascripts/config.coffee @@ -1,21 +1,21 @@ #= export Rails @Rails = - # Link elements bound by jquery-ujs + # Link elements bound by rails-ujs linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]' - # Button elements bound by jquery-ujs + # Button elements bound by rails-ujs buttonClickSelector: selector: 'button[data-remote]:not([form]), button[data-confirm]:not([form])' exclude: 'form button' - # Select elements bound by jquery-ujs + # Select elements bound by rails-ujs inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]' - # Form elements bound by jquery-ujs + # Form elements bound by rails-ujs formSubmitSelector: 'form' - # Form input elements bound by jquery-ujs + # Form input elements bound by rails-ujs formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])' # Form input elements disabled during form submission @@ -24,9 +24,6 @@ # Form input elements re-enabled after form submission formEnableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled' - # Form required input elements - requiredInputSelector: 'input[name][required]:not([disabled]), textarea[name][required]:not([disabled])' - # Form file input elements fileInputSelector: 'input[name][type=file]:not([disabled])' diff --git a/actionview/app/assets/javascripts/features/remote.coffee b/actionview/app/assets/javascripts/features/remote.coffee index 30a5dc21fa..852587042c 100644 --- a/actionview/app/assets/javascripts/features/remote.coffee +++ b/actionview/app/assets/javascripts/features/remote.coffee @@ -4,7 +4,7 @@ matches, getData, setData fire, stopEverything ajax, isCrossDomain - blankInputs, serializeElement + serializeElement } = Rails # Checks "data-remote" if true to handle the request through a XHR request. @@ -71,16 +71,6 @@ Rails.handleRemote = (e) -> ) stopEverything(e) -# Check whether any required fields are empty -# In both ajax mode and normal mode -Rails.validateForm = (e) -> - form = this - return if form.noValidate or getData(form, 'ujs:formnovalidate-button') - # Skip other logic when required values are missing or file upload is present - blankRequiredInputs = blankInputs(form, Rails.requiredInputSelector, false) - if blankRequiredInputs.length > 0 and fire(form, 'ajax:aborted:required', [blankRequiredInputs]) - stopEverything(e) - Rails.formSubmitButtonClick = (e) -> button = this form = button.form diff --git a/actionview/app/assets/javascripts/rails-ujs.coffee b/actionview/app/assets/javascripts/rails-ujs.coffee index f96d2eb6fd..df889ce067 100644 --- a/actionview/app/assets/javascripts/rails-ujs.coffee +++ b/actionview/app/assets/javascripts/rails-ujs.coffee @@ -14,7 +14,7 @@ refreshCSRFTokens, CSRFProtection enableElement, disableElement handleConfirm - handleRemote, validateForm, formSubmitButtonClick, handleMetaClick + handleRemote, formSubmitButtonClick, handleMetaClick handleMethod } = Rails @@ -25,9 +25,9 @@ if jQuery? and not jQuery.rails CSRFProtection(xhr) unless options.crossDomain Rails.start = -> - # Cut down on the number of issues from people inadvertently including jquery_ujs twice - # by detecting and raising an error when it happens. - throw new Error('jquery-ujs has already been loaded!') if window._rails_loaded + # Cut down on the number of issues from people inadvertently including + # rails-ujs twice by detecting and raising an error when it happens. + throw new Error('rails-ujs has already been loaded!') if window._rails_loaded # This event works the same as the load event, except that it fires every # time the page is loaded. @@ -58,7 +58,6 @@ Rails.start = -> delegate document, Rails.inputChangeSelector, 'change', handleRemote delegate document, Rails.formSubmitSelector, 'submit', handleConfirm - delegate document, Rails.formSubmitSelector, 'submit', validateForm delegate document, Rails.formSubmitSelector, 'submit', handleRemote # Normal mode submit # Slight timeout so that the submit button gets properly serialized diff --git a/actionview/app/assets/javascripts/utils/event.coffee b/actionview/app/assets/javascripts/utils/event.coffee index 049b2a3ecd..8d3ff007ea 100644 --- a/actionview/app/assets/javascripts/utils/event.coffee +++ b/actionview/app/assets/javascripts/utils/event.coffee @@ -6,14 +6,14 @@ # https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill CustomEvent = window.CustomEvent -if typeof CustomEvent is 'function' +if typeof CustomEvent isnt 'function' CustomEvent = (event, params) -> evt = document.createEvent('CustomEvent') evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail) evt CustomEvent.prototype = window.Event.prototype -# Triggers an custom event on an element and returns false if the event result is false +# Triggers a custom event on an element and returns false if the event result is false fire = Rails.fire = (obj, name, data) -> event = new CustomEvent( name, diff --git a/actionview/app/assets/javascripts/utils/form.coffee b/actionview/app/assets/javascripts/utils/form.coffee index 251113deda..5fa337b518 100644 --- a/actionview/app/assets/javascripts/utils/form.coffee +++ b/actionview/app/assets/javascripts/utils/form.coffee @@ -14,7 +14,7 @@ Rails.serializeElement = (element, additionalParam) -> if matches(input, 'select') toArray(input.options).forEach (option) -> params.push(name: input.name, value: option.value) if option.selected - else if input.type isnt 'radio' and input.type isnt 'checkbox' or input.checked + else if input.checked or ['radio', 'checkbox', 'submit'].indexOf(input.type) == -1 params.push(name: input.name, value: input.value) params.push(additionalParam) if additionalParam @@ -34,28 +34,3 @@ Rails.formElements = (form, selector) -> toArray(form.elements).filter (el) -> matches(el, selector) else toArray(form.querySelectorAll(selector)) - -# Helper function which checks for blank inputs in a form that match the specified CSS selector -Rails.blankInputs = (form, selector, nonBlank) -> - foundInputs = [] - requiredInputs = toArray(form.querySelectorAll(selector or 'input, textarea')) - checkedRadioButtonNames = {} - - requiredInputs.forEach (input) -> - if input.type is 'radio' - # Don't count unchecked required radio as blank if other radio with same name is checked, - # regardless of whether same-name radio input has required attribute or not. The spec - # states https://www.w3.org/TR/html5/forms.html#the-required-attribute - radioName = input.name - # Skip if we've already seen the radio with this name. - unless checkedRadioButtonNames[radioName] - # If none checked - if form.querySelectorAll("input[type=radio][name='#{radioName}']:checked").length == 0 - radios = form.querySelectorAll("input[type=radio][name='#{radioName}']") - foundInputs = foundInputs.concat(toArray(radios)) - # We only need to check each name once. - checkedRadioButtonNames[radioName] = radioName - else - valueToCheck = if input.type is 'checkbox' then input.checked else !!input.value - foundInputs.push(input) if valueToCheck is nonBlank - foundInputs diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index b7c05fdb88..5387174467 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -11,7 +11,7 @@ module ActionView #:nodoc: # = Action View Base # # Action View templates can be written in several ways. - # If the template file has a <tt>.erb</tt> extension, then it uses the erubis[https://rubygems.org/gems/erubis] + # If the template file has a <tt>.erb</tt> extension, then it uses the erubi[https://rubygems.org/gems/erubi] # template system which can embed Ruby into an HTML document. # If the template file has a <tt>.builder</tt> extension, then Jim Weirich's Builder::XmlMarkup library is used. # diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 72a094c629..750f96f29e 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -40,7 +40,7 @@ module ActionView # When the last parameter is a hash you can add HTML attributes using that # parameter. The following options are supported: # - # * <tt>:extname</tt> - Append a extention to the generated url unless the extension + # * <tt>:extname</tt> - Append an extension to the generated url unless the extension # already exists. This only applies for relative urls. # * <tt>:protocol</tt> - Sets the protocol of the generated url, this option only # applies when a relative url and +host+ options are provided. diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index c6a5e04aba..03bd1eb008 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -237,7 +237,11 @@ module ActionView def compute_asset_extname(source, options = {}) return if options[:extname] == false extname = options[:extname] || ASSET_EXTENSIONS[options[:type]] - extname if extname && File.extname(source) != extname + if extname && File.extname(source) != extname + extname + else + nil + end end # Maps asset types to public directory. @@ -406,7 +410,7 @@ module ActionView def video_url(source, options = {}) url_to_asset(source, { type: :video }.merge!(options)) end - alias_method :url_to_video, :video_url # aliased to avoid conflicts with an video_url named route + alias_method :url_to_video, :video_url # aliased to avoid conflicts with a video_url named route # Computes the path to an audio asset in the public audios directory. # Full paths from the document root will be passed through. @@ -445,7 +449,7 @@ module ActionView def font_path(source, options = {}) path_to_asset(source, { type: :font }.merge!(options)) end - alias_method :path_to_font, :font_path # aliased to avoid conflicts with an font_path named route + alias_method :path_to_font, :font_path # aliased to avoid conflicts with a font_path named route # Computes the full URL to a font asset. # This will use +font_path+ internally, so most of their behaviors will be the same. @@ -457,7 +461,7 @@ module ActionView def font_url(source, options = {}) url_to_asset(source, { type: :font }.merge!(options)) end - alias_method :url_to_font, :font_url # aliased to avoid conflicts with an font_url named route + alias_method :url_to_font, :font_url # aliased to avoid conflicts with a font_url named route end end end diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 7fdf0fd0e1..15ab7e304f 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -241,8 +241,6 @@ module ActionView end def write_fragment_for(name, options) - # VIEW TODO: Make #capture usable outside of ERB - # This dance is needed because Builder can't use capture pos = output_buffer.length yield output_safe = output_buffer.html_safe? diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index 9e80f0b2ee..b6bc5f4f6f 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -92,7 +92,7 @@ module ActionView # (defaults to "%u%n"). Fields are <tt>%u</tt> for the # currency, and <tt>%n</tt> for the number. # * <tt>:negative_format</tt> - Sets the format for negative - # numbers (defaults to prepending an hyphen to the formatted + # numbers (defaults to prepending a hyphen to the formatted # number given by <tt>:format</tt>). Accepts the same fields # than <tt>:format</tt>, except <tt>%n</tt> is here the # absolute value of the number. diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb index 9f1a890f6a..25defd1276 100644 --- a/actionview/lib/action_view/helpers/output_safety_helper.rb +++ b/actionview/lib/action_view/helpers/output_safety_helper.rb @@ -25,7 +25,7 @@ module ActionView #:nodoc: # safe_join([raw("<p>foo</p>"), "<p>bar</p>"], "<br />") # # => "<p>foo</p><br /><p>bar</p>" # - # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />") + # safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />")) # # => "<p>foo</p><br /><p>bar</p>" # def safe_join(array, sep = $,) @@ -60,7 +60,7 @@ module ActionView #:nodoc: when 2 safe_join([array[0], array[1]], options[:two_words_connector]) else - safe_join([safe_join(array[0...-1], options[:words_connector]), options[:last_word_connector], array[-1]]) + safe_join([safe_join(array[0...-1], options[:words_connector]), options[:last_word_connector], array[-1]], nil) end end end diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index 3d6ff598ee..1e9b813d3d 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -45,17 +45,15 @@ module ActionView # Providing a custom Rails::Html scrubber: # # class CommentScrubber < Rails::Html::PermitScrubber - # def allowed_node?(node) - # !%w(form script comment blockquote).include?(node.name) + # def initialize + # super + # self.tags = %w( form script comment blockquote ) + # self.attributes = %w( style ) # end # # def skip_node?(node) # node.text? # end - # - # def scrub_attribute?(name) - # name == 'style' - # end # end # # <%= sanitize @comment.body, scrubber: CommentScrubber.new %> diff --git a/actionview/lib/action_view/helpers/tags/date_select.rb b/actionview/lib/action_view/helpers/tags/date_select.rb index 006605885a..638c134deb 100644 --- a/actionview/lib/action_view/helpers/tags/date_select.rb +++ b/actionview/lib/action_view/helpers/tags/date_select.rb @@ -16,7 +16,7 @@ module ActionView class << self def select_type - @select_type ||= self.name.split("::").last.sub("Select", "").downcase + @select_type ||= name.split("::").last.sub("Select", "").downcase end end diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb index 4306c3543d..613cade7b3 100644 --- a/actionview/lib/action_view/helpers/tags/text_field.rb +++ b/actionview/lib/action_view/helpers/tags/text_field.rb @@ -17,7 +17,7 @@ module ActionView class << self def field_type - @field_type ||= self.name.split("::").last.sub("Field", "").downcase + @field_type ||= name.split("::").last.sub("Field", "").downcase end end diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 07dccf5b41..bc922f9ce8 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -187,7 +187,7 @@ module ActionView unless separator.empty? text.split(separator).each do |value| if value.match(regex) - regex = phrase = value + phrase = value break end end diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 58a4a04dcb..a306903c60 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -35,7 +35,7 @@ module ActionView when :back _back_url else - raise ArgumentError, "arguments passed to url_for can't be handled. Please require " + + raise ArgumentError, "arguments passed to url_for can't be handled. Please require " \ "routes or provide your own implementation" end end @@ -517,6 +517,9 @@ module ActionView # current_page?('http://www.example.com/shop/checkout') # # => true # + # current_page?('http://www.example.com/shop/checkout', check_parameters: true) + # # => false + # # current_page?('/shop/checkout') # # => true # @@ -530,7 +533,7 @@ module ActionView # # We can also pass in the symbol arguments instead of strings. # - def current_page?(options) + def current_page?(options, check_parameters: false) unless request raise "You cannot use helpers that need to determine the current " \ "page unless your view context provides a Request object " \ @@ -539,12 +542,14 @@ module ActionView return false unless request.get? || request.head? + check_parameters ||= !options.is_a?(String) && options.try(:delete, :check_parameters) url_string = URI.parser.unescape(url_for(options)).force_encoding(Encoding::BINARY) # We ignore any extra parameters in the request_uri if the # submitted url doesn't have any either. This lets the function # work with things like ?order=asc - request_uri = url_string.index("?") ? request.fullpath : request.path + # the behaviour can be disabled with check_parameters: true + request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path request_uri = URI.parser.unescape(request_uri).force_encoding(Encoding::BINARY) url_string.chomp!("/") if url_string.start_with?("/") && url_string != "/" diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb index 18e395a67f..81feb90486 100644 --- a/actionview/lib/action_view/layouts.rb +++ b/actionview/lib/action_view/layouts.rb @@ -319,7 +319,7 @@ module ActionView name_clause end - self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + class_eval <<-RUBY, __FILE__, __LINE__ + 1 def _layout(formats) if _conditional_layout? #{layout_definition} diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index b8a79da97f..647b15ea94 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -99,7 +99,7 @@ module ActionView # <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %> # # If the given <tt>:collection</tt> is +nil+ or empty, <tt>render</tt> will return nil. This will allow you - # to specify a text which will displayed instead by using this form: + # to specify a text which will be displayed instead by using this form: # # <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %> # @@ -458,7 +458,7 @@ module ActionView locals[counter] = index locals[iteration] = partial_iteration - template = (cache[path] ||= find_template(path, keys + [as, counter])) + template = (cache[path] ||= find_template(path, keys + [as, counter, iteration])) content = template.render(view, locals) partial_iteration.iterate! content @@ -532,11 +532,11 @@ module ActionView [variable, variable_counter, variable_iteration] end - IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " + + IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \ "make sure your partial name starts with underscore." - OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " + - "make sure it starts with lowercase letter, " + + OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \ + "make sure it starts with lowercase letter, " \ "and is followed by any combination of letters, numbers and underscores." def raise_invalid_identifier(path) diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index 7ede034492..62ce985243 100644 --- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb +++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb @@ -4,7 +4,6 @@ module ActionView # == TODO # # * Support streaming from child templates, partials and so on. - # * Integrate exceptions with exceptron # * Rack::Cache needs to support streaming bodies class StreamingTemplateRenderer < TemplateRenderer #:nodoc: # A valid Rack::Body (i.e. it responds to each). @@ -28,7 +27,6 @@ module ActionView private # This is the same logging logic as in ShowExceptions middleware. - # TODO Once "exceptron" is in, refactor this piece to simply re-use exceptron. def log_error(exception) logger = ActionView::Base.logger return unless logger diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index 0e72316eb7..cf18562c45 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -124,7 +124,11 @@ module ActionView key = action.include?(?/) ? :template : :action options[key] = action else - options[:partial] = action + if action.respond_to?(:permitted?) && action.permitted? + options = action + else + options[:partial] = action + end end options diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 4b793c3b16..b0e2f1e54e 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -326,7 +326,7 @@ module ActionView # Only locals with valid variable names get set directly. Others will # still be available in local_assigns. locals = @locals - Module::RUBY_RESERVED_KEYWORDS - locals = locals.grep(/\A(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/) + locals = locals.grep(/\A@?(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/) # Double assign to suppress the dreaded 'assigned but unused variable' warning locals.each_with_object("") { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" } @@ -345,7 +345,7 @@ module ActionView end def instrument(action, &block) # :doc: - ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, instrument_payload, &block) + ActiveSupport::Notifications.instrument("#{action}.action_view", instrument_payload, &block) end def instrument_render_template(&block) diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb index 494b543152..e99b921cb7 100644 --- a/actionview/lib/action_view/template/handlers/builder.rb +++ b/actionview/lib/action_view/template/handlers/builder.rb @@ -7,7 +7,7 @@ module ActionView def call(template) require_engine - "xml = ::Builder::XmlMarkup.new(:indent => 2);" + + "xml = ::Builder::XmlMarkup.new(:indent => 2);" \ "self.output_buffer = xml.target!;" + template.source + ";xml.target!;" diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 5d047a6991..58c7fd1a88 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -1,79 +1,12 @@ -require "erubis" - module ActionView class Template module Handlers - class Erubis < ::Erubis::Eruby - def add_preamble(src) - @newline_pending = 0 - src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" - end - - def add_text(src, text) - return if text.empty? - - if text == "\n" - @newline_pending += 1 - else - src << "@output_buffer.safe_append='" - src << "\n" * @newline_pending if @newline_pending > 0 - src << escape_text(text) - src << "'.freeze;" - - @newline_pending = 0 - end - end - - # Erubis toggles <%= and <%== behavior when escaping is enabled. - # We override to always treat <%== as escaped. - def add_expr(src, code, indicator) - case indicator - when "==" - add_expr_escaped(src, code) - else - super - end - end - - BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/ - - def add_expr_literal(src, code) - flush_newline_if_pending(src) - if BLOCK_EXPR.match?(code) - src << "@output_buffer.append= " << code - else - src << "@output_buffer.append=(" << code << ");" - end - end - - def add_expr_escaped(src, code) - flush_newline_if_pending(src) - if BLOCK_EXPR.match?(code) - src << "@output_buffer.safe_expr_append= " << code - else - src << "@output_buffer.safe_expr_append=(" << code << ");" - end - end - - def add_stmt(src, code) - flush_newline_if_pending(src) - super - end - - def add_postamble(src) - flush_newline_if_pending(src) - src << "@output_buffer.to_s" - end - - def flush_newline_if_pending(src) - if @newline_pending > 0 - src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;" - @newline_pending = 0 - end - end - end + autoload :Erubis, "action_view/template/handlers/erb/deprecated_erubis" class ERB + autoload :Erubi, "action_view/template/handlers/erb/erubi" + autoload :Erubis, "action_view/template/handlers/erb/erubis" + # Specify trim mode for the ERB compiler. Defaults to '-'. # See ERB documentation for suitable values. class_attribute :erb_trim_mode @@ -81,7 +14,7 @@ module ActionView # Default implementation used. class_attribute :erb_implementation - self.erb_implementation = Erubis + self.erb_implementation = Erubi # Do not escape templates of these mime types. class_attribute :escape_whitelist diff --git a/actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb b/actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb new file mode 100644 index 0000000000..427ea20064 --- /dev/null +++ b/actionview/lib/action_view/template/handlers/erb/deprecated_erubis.rb @@ -0,0 +1,9 @@ +::ActiveSupport::Deprecation.warn("ActionView::Template::Handlers::Erubis is deprecated and will be removed from Rails 5.2. Switch to ActionView::Template::Handlers::ERB::Erubi instead.") + +module ActionView + class Template + module Handlers + Erubis = ERB::Erubis + end + end +end diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb new file mode 100644 index 0000000000..755cc84015 --- /dev/null +++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb @@ -0,0 +1,81 @@ +require "erubi" + +module ActionView + class Template + module Handlers + class ERB + class Erubi < ::Erubi::Engine + # :nodoc: all + def initialize(input, properties = {}) + @newline_pending = 0 + + # Dup properties so that we don't modify argument + properties = Hash[properties] + properties[:preamble] = "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" + properties[:postamble] = "@output_buffer.to_s" + properties[:bufvar] = "@output_buffer" + properties[:escapefunc] = "" + + super + end + + def evaluate(action_view_erb_handler_context) + pr = eval("proc { #{@src} }", binding, @filename || "(erubi)") + action_view_erb_handler_context.instance_eval(&pr) + end + + private + def add_text(text) + return if text.empty? + + if text == "\n" + @newline_pending += 1 + else + src << "@output_buffer.safe_append='" + src << "\n" * @newline_pending if @newline_pending > 0 + src << text.gsub(/['\\]/, '\\\\\&') + src << "'.freeze;" + + @newline_pending = 0 + end + end + + BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/ + + def add_expression(indicator, code) + flush_newline_if_pending(src) + + if (indicator == "==") || @escape + src << "@output_buffer.safe_expr_append=" + else + src << "@output_buffer.append=" + end + + if BLOCK_EXPR.match?(code) + src << " " << code + else + src << "(" << code << ");" + end + end + + def add_code(code) + flush_newline_if_pending(src) + super + end + + def add_postamble(_) + flush_newline_if_pending(src) + super + end + + def flush_newline_if_pending(src) + if @newline_pending > 0 + src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;" + @newline_pending = 0 + end + end + end + end + end + end +end diff --git a/actionview/lib/action_view/template/handlers/erb/erubis.rb b/actionview/lib/action_view/template/handlers/erb/erubis.rb new file mode 100644 index 0000000000..f3c35e1aec --- /dev/null +++ b/actionview/lib/action_view/template/handlers/erb/erubis.rb @@ -0,0 +1,81 @@ +gem "erubis" +require "erubis" + +module ActionView + class Template + module Handlers + class ERB + class Erubis < ::Erubis::Eruby + # :nodoc: all + def add_preamble(src) + @newline_pending = 0 + src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" + end + + def add_text(src, text) + return if text.empty? + + if text == "\n" + @newline_pending += 1 + else + src << "@output_buffer.safe_append='" + src << "\n" * @newline_pending if @newline_pending > 0 + src << escape_text(text) + src << "'.freeze;" + + @newline_pending = 0 + end + end + + # Erubis toggles <%= and <%== behavior when escaping is enabled. + # We override to always treat <%== as escaped. + def add_expr(src, code, indicator) + case indicator + when "==" + add_expr_escaped(src, code) + else + super + end + end + + BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/ + + def add_expr_literal(src, code) + flush_newline_if_pending(src) + if BLOCK_EXPR.match?(code) + src << "@output_buffer.append= " << code + else + src << "@output_buffer.append=(" << code << ");" + end + end + + def add_expr_escaped(src, code) + flush_newline_if_pending(src) + if BLOCK_EXPR.match?(code) + src << "@output_buffer.safe_expr_append= " << code + else + src << "@output_buffer.safe_expr_append=(" << code << ");" + end + end + + def add_stmt(src, code) + flush_newline_if_pending(src) + super + end + + def add_postamble(src) + flush_newline_if_pending(src) + src << "@output_buffer.to_s" + end + + def flush_newline_if_pending(src) + if @newline_pending > 0 + src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;" + @newline_pending = 0 + end + end + end + end + end + end +end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 9da13663d7..d3905b5f23 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -164,8 +164,8 @@ module ActionView # This is what child classes implement. No defaults are needed # because Resolver guarantees that the arguments are present and # normalized. - def find_templates(name, prefix, partial, details) - raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) method" + def find_templates(name, prefix, partial, details, outside_app_allowed = false) + raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, outside_app_allowed = false) method" end # Helpers that builds a path. Useful for building virtual paths. @@ -226,7 +226,7 @@ module ActionView template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed template_paths.map do |template| - handler, format, variant = extract_handler_and_format_and_variant(template, formats) + handler, format, variant = extract_handler_and_format_and_variant(template) contents = File.binread(template) Template.new(contents, File.expand_path(template), handler, @@ -289,7 +289,7 @@ module ActionView # Extract handler, formats and variant from path. If a format cannot be found neither # from the path, or the handler, we should return the array of formats given # to the resolver. - def extract_handler_and_format_and_variant(path, default_formats) + def extract_handler_and_format_and_variant(path) pieces = File.basename(path).split(".".freeze) pieces.shift diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index 5fb7bb54b5..ae4fec4337 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -124,6 +124,10 @@ module ActionView @_rendered_views ||= RenderedViewsCollection.new end + def _routes + @controller._routes if @controller.respond_to?(:_routes) + end + # Need to experiment if this priority is the best one: rendered => output_buffer class RenderedViewsCollection def initialize @@ -206,8 +210,8 @@ module ActionView view = @controller.view_context view.singleton_class.include(_helpers) view.extend(Locals) - view.rendered_views = self.rendered_views - view.output_buffer = self.output_buffer + view.rendered_views = rendered_views + view.output_buffer = output_buffer view end end @@ -258,10 +262,6 @@ module ActionView end] end - def _routes - @controller._routes if @controller.respond_to?(:_routes) - end - def method_missing(selector, *args) begin routes = @controller.respond_to?(:_routes) && @controller._routes diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb index f4a7a9138c..3188526b63 100644 --- a/actionview/lib/action_view/testing/resolvers.rb +++ b/actionview/lib/action_view/testing/resolvers.rb @@ -17,35 +17,35 @@ module ActionView #:nodoc: @hash.keys.join(", ") end - private - - def query(path, exts, formats, _) - query = "" - EXTENSIONS.each_key do |ext| - query << "(" << exts[ext].map { |e| e && Regexp.escape(".#{e}") }.join("|") << "|)" - end - query = /^(#{Regexp.escape(path)})#{query}$/ - - templates = [] - @hash.each do |_path, array| - source, updated_at = array - next unless query.match?(_path) - handler, format, variant = extract_handler_and_format_and_variant(_path, formats) - templates << Template.new(source, _path, handler, - virtual_path: path.virtual, - format: format, - variant: variant, - updated_at: updated_at - ) + private + + def query(path, exts, _, _) + query = "" + EXTENSIONS.each_key do |ext| + query << "(" << exts[ext].map { |e| e && Regexp.escape(".#{e}") }.join("|") << "|)" + end + query = /^(#{Regexp.escape(path)})#{query}$/ + + templates = [] + @hash.each do |_path, array| + source, updated_at = array + next unless query.match?(_path) + handler, format, variant = extract_handler_and_format_and_variant(_path) + templates << Template.new(source, _path, handler, + virtual_path: path.virtual, + format: format, + variant: variant, + updated_at: updated_at + ) + end + + templates.sort_by { |t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size } end - - templates.sort_by { |t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size } - end end class NullResolver < PathResolver - def query(path, exts, formats, _) - handler, format, variant = extract_handler_and_format_and_variant(path, formats) + def query(path, exts, _, _) + handler, format, variant = extract_handler_and_format_and_variant(path) [ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, virtual_path: path.virtual, format: format, variant: variant)] end end diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb index a9638d1e6d..f0fe6831fa 100644 --- a/actionview/lib/action_view/view_paths.rb +++ b/actionview/lib/action_view/view_paths.rb @@ -5,7 +5,7 @@ module ActionView included do class_attribute :_view_paths self._view_paths = ActionView::PathSet.new - self._view_paths.freeze + _view_paths.freeze end delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=, diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 5bc4151087..dde66a7ba0 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -9,8 +9,8 @@ require "active_support/core_ext/kernel/reporting" # These are the normal settings that will be set up by Railties # TODO: Have these tests support other combinations of these values silence_warnings do - Encoding.default_internal = "UTF-8" - Encoding.default_external = "UTF-8" + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 end require "active_support/testing/autorun" @@ -271,15 +271,15 @@ module ActionDispatch end end -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" -end -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) -end - class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions + + # Skips the current run on Rubinius using Minitest::Assertions#skip + private def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + # Skips the current run on JRuby using Minitest::Assertions#skip + private def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index b4a757d6b8..51ec8899b1 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -1,45 +1,14 @@ require "abstract_unit" require "active_model" +require "controller/fake_models" class ApplicationController < ActionController::Base self.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack") end -class Customer < Struct.new(:name, :id) - extend ActiveModel::Naming - include ActiveModel::Conversion - - undef_method :to_json - - def to_xml(options = {}) - if options[:builder] - options[:builder].name name - else - "<name>#{name}</name>" - end - end - - def to_js(options = {}) - "name: #{name.inspect}" - end - alias :to_text :to_js - - def errors - [] - end - - def persisted? - id.present? - end - - def cache_key - name.to_s - end -end - module Quiz #Models - class Question < Struct.new(:name, :id) + Question = Struct.new(:name, :id) do extend ActiveModel::Naming include ActiveModel::Conversion @@ -56,9 +25,6 @@ module Quiz end end -class BadCustomer < Customer; end -class GoodCustomer < Customer; end - module Fun class GamesController < ApplicationController def hello_world; end diff --git a/actionview/test/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb index 0f7960b408..3b314588c7 100644 --- a/actionview/test/activerecord/form_helper_activerecord_test.rb +++ b/actionview/test/activerecord/form_helper_activerecord_test.rb @@ -45,7 +45,7 @@ class FormHelperActiveRecordTest < ActionView::TestCase end expected = whole_form("/developers/123", "edit_developer_123", "edit_developer", method: "patch") do - '<input id="developer_projects_attributes_abc_name" name="developer[projects_attributes][abc][name]" type="text" value="project #321" />' + + '<input id="developer_projects_attributes_abc_name" name="developer[projects_attributes][abc][name]" type="text" value="project #321" />' \ '<input id="developer_projects_attributes_abc_id" name="developer[projects_attributes][abc][id]" type="hidden" value="321" />' end diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb index 8495949975..e99c769dc2 100644 --- a/actionview/test/activerecord/polymorphic_routes_test.rb +++ b/actionview/test/activerecord/polymorphic_routes_test.rb @@ -45,7 +45,7 @@ class ModelDelegate end end -module Blog +module Weblog class Post < ActiveRecord::Base self.table_name = "projects" end @@ -61,7 +61,7 @@ end class PolymorphicRoutesTest < ActionController::TestCase include SharedTestRoutes.url_helpers - self.default_url_options[:host] = "example.com" + default_url_options[:host] = "example.com" def setup @project = Project.new @@ -72,8 +72,8 @@ class PolymorphicRoutesTest < ActionController::TestCase @fax = Fax.new @delegator = ModelDelegator.new @series = Series.new - @blog_post = Blog::Post.new - @blog_blog = Blog::Blog.new + @blog_post = Weblog::Post.new + @blog_blog = Weblog::Blog.new end def assert_url(url, args) @@ -86,8 +86,7 @@ class PolymorphicRoutesTest < ActionController::TestCase def test_string with_test_routes do - # FIXME: why are these different? Symbol case passes through to - # `polymorphic_url`, but the String case doesn't. + assert_equal "/projects", polymorphic_path("projects") assert_equal "http://example.com/projects", polymorphic_url("projects") assert_equal "projects", url_for("projects") end diff --git a/actionview/test/activerecord/render_partial_with_record_identification_test.rb b/actionview/test/activerecord/render_partial_with_record_identification_test.rb index 55886da30f..60c3ab3045 100644 --- a/actionview/test/activerecord/render_partial_with_record_identification_test.rb +++ b/actionview/test/activerecord/render_partial_with_record_identification_test.rb @@ -87,7 +87,7 @@ class RenderPartialWithRecordIdentificationTest < ActiveRecordTestCase end end -class Game < Struct.new(:name, :id) +Game = Struct.new(:name, :id) do extend ActiveModel::Naming include ActiveModel::Conversion def to_param diff --git a/actionview/test/fixtures/test/_partial_iteration_1.erb b/actionview/test/fixtures/test/_partial_iteration_1.erb new file mode 100644 index 0000000000..c0fdd4c22a --- /dev/null +++ b/actionview/test/fixtures/test/_partial_iteration_1.erb @@ -0,0 +1 @@ +<%= defined?(partial_iteration_1_iteration) %> diff --git a/actionview/test/fixtures/test/_partial_iteration_2.erb b/actionview/test/fixtures/test/_partial_iteration_2.erb new file mode 100644 index 0000000000..50dd11db27 --- /dev/null +++ b/actionview/test/fixtures/test/_partial_iteration_2.erb @@ -0,0 +1 @@ +<%= defined?(partial_iteration_2_iteration) -%> diff --git a/actionview/test/fixtures/test/render_file_instance_variable.erb b/actionview/test/fixtures/test/render_file_instance_variable.erb new file mode 100644 index 0000000000..5344ac8a66 --- /dev/null +++ b/actionview/test/fixtures/test/render_file_instance_variable.erb @@ -0,0 +1 @@ +<%= @foo %>
\ No newline at end of file diff --git a/actionview/test/lib/controller/fake_models.rb b/actionview/test/lib/controller/fake_models.rb index ddc915895d..5250101220 100644 --- a/actionview/test/lib/controller/fake_models.rb +++ b/actionview/test/lib/controller/fake_models.rb @@ -1,6 +1,6 @@ require "active_model" -class Customer < Struct.new(:name, :id) +Customer = Struct.new(:name, :id) do extend ActiveModel::Naming include ActiveModel::Conversion @@ -26,12 +26,16 @@ class Customer < Struct.new(:name, :id) def persisted? id.present? end -end -class GoodCustomer < Customer + def cache_key + name.to_s + end end -class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) +class BadCustomer < Customer; end +class GoodCustomer < Customer; end + +Post = Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost) do extend ActiveModel::Naming include ActiveModel::Conversion extend ActiveModel::Translation @@ -163,7 +167,7 @@ module Blog true end - class Post < Struct.new(:title, :id) + Post = Struct.new(:title, :id) do extend ActiveModel::Naming include ActiveModel::Conversion @@ -183,8 +187,7 @@ class ArelLike end end -class Car < Struct.new(:color) -end +Car = Struct.new(:color) class Plane attr_reader :to_key diff --git a/actionview/test/template/active_model_helper_test.rb b/actionview/test/template/active_model_helper_test.rb index 8b8f686f96..6b63aa25a5 100644 --- a/actionview/test/template/active_model_helper_test.rb +++ b/actionview/test/template/active_model_helper_test.rb @@ -4,7 +4,7 @@ class ActiveModelHelperTest < ActionView::TestCase tests ActionView::Helpers::ActiveModelHelper silence_warnings do - class Post < Struct.new(:author_name, :body, :updated_at) + Post = Struct.new(:author_name, :body, :updated_at) do include ActiveModel::Conversion include ActiveModel::Validations diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb index e9a923dd72..1245a1a966 100644 --- a/actionview/test/template/atom_feed_helper_test.rb +++ b/actionview/test/template/atom_feed_helper_test.rb @@ -1,6 +1,6 @@ require "abstract_unit" -class Scroll < Struct.new(:id, :to_param, :title, :body, :updated_at, :created_at) +Scroll = Struct.new(:id, :to_param, :title, :body, :updated_at, :created_at) do extend ActiveModel::Naming include ActiveModel::Conversion diff --git a/actionview/test/template/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb index 40ac867b38..adb2be9be4 100644 --- a/actionview/test/template/compiled_templates_test.rb +++ b/actionview/test/template/compiled_templates_test.rb @@ -38,6 +38,10 @@ class CompiledTemplatesTest < ActiveSupport::TestCase assert_equal "🎂", render(file: "test/render_file_unicode_local", locals: { 🎃: "🎂" }) end + def test_template_with_instance_variable_identifier + assert_equal "bar", render(file: "test/render_file_instance_variable", locals: { "@foo": "bar" }) + end + def test_template_gets_recompiled_when_using_different_keys_in_local_assigns assert_equal "one", render(file: "test/render_file_with_locals_and_default") assert_equal "two", render(file: "test/render_file_with_locals_and_default", locals: { secret: "two" }) diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index a84ac18bee..bfd3ecd6fd 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -1407,7 +1407,6 @@ class DateHelperTest < ActionView::TestCase end def test_select_datetime_with_custom_prompt - expected = %(<select id="date_first_year" name="date[first][year]">\n) expected << %(<option value="">Choose year</option>\n<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) expected << "</select>\n" diff --git a/actionview/test/template/erb/deprecated_erubis_implementation_test.rb b/actionview/test/template/erb/deprecated_erubis_implementation_test.rb new file mode 100644 index 0000000000..aaf99f85c0 --- /dev/null +++ b/actionview/test/template/erb/deprecated_erubis_implementation_test.rb @@ -0,0 +1,13 @@ +require "abstract_unit" + +module ERBTest + class DeprecatedErubisImplementationTest < ActionView::TestCase + test "Erubis implementation is deprecated" do + assert_deprecated "ActionView::Template::Handlers::Erubis is deprecated and will be removed from Rails 5.2. Switch to ActionView::Template::Handlers::ERB::Erubi instead." do + assert_equal "ActionView::Template::Handlers::ERB::Erubis", ActionView::Template::Handlers::Erubis.to_s + + assert_nothing_raised { Class.new(ActionView::Template::Handlers::Erubis) } + end + end + end +end diff --git a/actionview/test/template/erb/helper.rb b/actionview/test/template/erb/helper.rb index a1973068d5..bc1eedc15f 100644 --- a/actionview/test/template/erb/helper.rb +++ b/actionview/test/template/erb/helper.rb @@ -14,7 +14,7 @@ module ERBTest class BlockTestCase < ActiveSupport::TestCase def render_content(start, inside) template = block_helper(start, inside) - ActionView::Template::Handlers::Erubis.new(template).evaluate(ViewContext.new) + ActionView::Template::Handlers::ERB.erb_implementation.new(template).evaluate(ViewContext.new) end def block_helper(str, rest) diff --git a/actionview/test/template/erb/tag_helper_test.rb b/actionview/test/template/erb/tag_helper_test.rb index 84e328d8be..233f37c48a 100644 --- a/actionview/test/template/erb/tag_helper_test.rb +++ b/actionview/test/template/erb/tag_helper_test.rb @@ -18,8 +18,8 @@ module ERBTest end test "percent equals works with form tags" do - expected_output = %r{<form.*action="foo".*method="post">.*hello*</form>} - assert_match expected_output, render_content("form_tag('foo')", "<%= 'hello' %>") + expected_output = %r{<form.*action="/foo".*method="post">.*hello*</form>} + assert_match expected_output, render_content("form_tag('/foo')", "<%= 'hello' %>") end test "percent equals works with fieldset tags" do diff --git a/actionview/test/template/erb_util_test.rb b/actionview/test/template/erb_util_test.rb index 0a16364ed3..4412ea37fa 100644 --- a/actionview/test/template/erb_util_test.rb +++ b/actionview/test/template/erb_util_test.rb @@ -51,7 +51,12 @@ class ErbUtilTest < ActiveSupport::TestCase def test_json_escape_does_not_alter_json_string_meaning JSON_ESCAPE_TEST_CASES.each do |(raw, _)| - assert_equal ActiveSupport::JSON.decode(raw), ActiveSupport::JSON.decode(json_escape(raw)) + expected = ActiveSupport::JSON.decode(raw) + if expected.nil? + assert_nil ActiveSupport::JSON.decode(json_escape(raw)) + else + assert_equal expected, ActiveSupport::JSON.decode(json_escape(raw)) + end end end diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb index 3774dcf872..6160524eb3 100644 --- a/actionview/test/template/form_collections_helper_test.rb +++ b/actionview/test/template/form_collections_helper_test.rb @@ -1,7 +1,6 @@ require "abstract_unit" -class Category < Struct.new(:id, :name) -end +Category = Struct.new(:id, :name) class FormCollectionsHelperTest < ActionView::TestCase def assert_no_select(selector, value = nil) diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index 3a91c7dce7..a4a2966ff9 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -310,13 +310,13 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", "create-post", method: "patch") do - "<label for='post_title'>The Title</label>" + - "<input name='post[title]' type='text' value='Hello World' />" + - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + - "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" + - "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" + - "<button name='button' type='submit'>Create post</button>" + + "<label for='post_title'>The Title</label>" \ + "<input name='post[title]' type='text' value='Hello World' />" \ + "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" \ + "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" \ + "<button name='button' type='submit'>Create post</button>" \ "<button name='button' type='submit'><span>Create post</span></button>" end @@ -330,7 +330,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts") do - '<label for="title">Label me</label>' + + '<label for="title">Label me</label>' \ '<input type="text" name="title">' end @@ -344,7 +344,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123") do - '<label for="title">Label me</label>' + + '<label for="title">Label me</label>' \ '<input type="text" name="title">' end @@ -400,10 +400,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts") do - "<input type='hidden' name='post[active]' value='' />" + - "<input name='post[active]' type='radio' value='true' />" + - "<label for='post_active_true'>true</label>" + - "<input checked='checked' name='post[active]' type='radio' value='false' />" + + "<input type='hidden' name='post[active]' value='' />" \ + "<input name='post[active]' type='radio' value='true' />" \ + "<label for='post_active_true'>true</label>" \ + "<input checked='checked' name='post[active]' type='radio' value='false' />" \ "<label for='post_active_false'>false</label>" end @@ -422,12 +422,12 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts") do - "<input type='hidden' name='post[active]' value='' />" + - "<label for='post_active_true'>" + - "<input name='post[active]' type='radio' value='true' />" + - "true</label>" + - "<label for='post_active_false'>" + - "<input checked='checked' name='post[active]' type='radio' value='false' />" + + "<input type='hidden' name='post[active]' value='' />" \ + "<label for='post_active_true'>" \ + "<input name='post[active]' type='radio' value='true' />" \ + "true</label>" \ + "<label for='post_active_false'>" \ + "<input checked='checked' name='post[active]' type='radio' value='false' />" \ "false</label>" end @@ -448,13 +448,13 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts") do - "<input type='hidden' name='post[active]' value='' />" + - "<label for='post_active_true'>" + - "<input name='post[active]' type='radio' value='true' />" + - "true</label>" + - "<label for='post_active_false'>" + - "<input checked='checked' name='post[active]' type='radio' value='false' />" + - "false</label>" + + "<input type='hidden' name='post[active]' value='' />" \ + "<label for='post_active_true'>" \ + "<input name='post[active]' type='radio' value='true' />" \ + "true</label>" \ + "<label for='post_active_false'>" \ + "<input checked='checked' name='post[active]' type='radio' value='false' />" \ + "false</label>" \ "<input name='post[id]' type='hidden' value='1' />" end @@ -470,10 +470,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts") do - "<input type='hidden' name='post[1][active]' value='' />" + - "<input name='post[1][active]' type='radio' value='true' />" + - "<label for='post_1_active_true'>true</label>" + - "<input checked='checked' name='post[1][active]' type='radio' value='false' />" + + "<input type='hidden' name='post[1][active]' value='' />" \ + "<input name='post[1][active]' type='radio' value='true' />" \ + "<label for='post_1_active_true'>true</label>" \ + "<input checked='checked' name='post[1][active]' type='radio' value='false' />" \ "<label for='post_1_active_false'>false</label>" end @@ -489,12 +489,12 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts") do - "<input name='post[tag_ids][]' type='hidden' value='' />" + - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" + - "<label for='post_tag_ids_1'>Tag 1</label>" + - "<input name='post[tag_ids][]' type='checkbox' value='2' />" + - "<label for='post_tag_ids_2'>Tag 2</label>" + - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" + + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "<label for='post_tag_ids_1'>Tag 1</label>" \ + "<input name='post[tag_ids][]' type='checkbox' value='2' />" \ + "<label for='post_tag_ids_2'>Tag 2</label>" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \ "<label for='post_tag_ids_3'>Tag 3</label>" end @@ -513,15 +513,15 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts") do - "<input name='post[tag_ids][]' type='hidden' value='' />" + - "<label for='post_tag_ids_1'>" + - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" + - "Tag 1</label>" + - "<label for='post_tag_ids_2'>" + - "<input name='post[tag_ids][]' type='checkbox' value='2' />" + - "Tag 2</label>" + - "<label for='post_tag_ids_3'>" + - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" + + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<label for='post_tag_ids_1'>" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "Tag 1</label>" \ + "<label for='post_tag_ids_2'>" \ + "<input name='post[tag_ids][]' type='checkbox' value='2' />" \ + "Tag 2</label>" \ + "<label for='post_tag_ids_3'>" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \ "Tag 3</label>" end @@ -543,16 +543,16 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts") do - "<input name='post[tag_ids][]' type='hidden' value='' />" + - "<label for='post_tag_ids_1'>" + - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" + - "Tag 1</label>" + - "<label for='post_tag_ids_2'>" + - "<input name='post[tag_ids][]' type='checkbox' value='2' />" + - "Tag 2</label>" + - "<label for='post_tag_ids_3'>" + - "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" + - "Tag 3</label>" + + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<label for='post_tag_ids_1'>" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "Tag 1</label>" \ + "<label for='post_tag_ids_2'>" \ + "<input name='post[tag_ids][]' type='checkbox' value='2' />" \ + "Tag 2</label>" \ + "<label for='post_tag_ids_3'>" \ + "<input checked='checked' name='post[tag_ids][]' type='checkbox' value='3' />" \ + "Tag 3</label>" \ "<input name='post[id]' type='hidden' value='1' />" end @@ -569,8 +569,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts") do - "<input name='post[1][tag_ids][]' type='hidden' value='' />" + - "<input checked='checked' name='post[1][tag_ids][]' type='checkbox' value='1' />" + + "<input name='post[1][tag_ids][]' type='hidden' value='' />" \ + "<input checked='checked' name='post[1][tag_ids][]' type='checkbox' value='1' />" \ "<label for='post_1_tag_ids_1'>Tag 1</label>" end @@ -636,7 +636,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/44", method: "patch") do - "<input name='post[title]' type='text' value='And his name will be forty and four.' />" + + "<input name='post[title]' type='text' value='And his name will be forty and four.' />" \ "<input name='commit' data-disable-with='Edit post' type='submit' value='Edit post' />" end @@ -653,11 +653,11 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", "create-post", method: "patch") do - "<label for='other_name_title' class='post_title'>Title</label>" + - "<input name='other_name[title]' value='Hello World' type='text' />" + - "<textarea name='other_name[body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='other_name[secret]' value='0' type='hidden' />" + - "<input name='other_name[secret]' checked='checked' value='1' type='checkbox' />" + + "<label for='other_name_title' class='post_title'>Title</label>" \ + "<input name='other_name[title]' value='Hello World' type='text' />" \ + "<textarea name='other_name[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='other_name[secret]' value='0' type='hidden' />" \ + "<input name='other_name[secret]' checked='checked' value='1' type='checkbox' />" \ "<input name='commit' value='Create post' data-disable-with='Create post' type='submit' />" end @@ -672,9 +672,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/", "create-post", method: "delete") do - "<input name='post[title]' type='text' value='Hello World' />" + - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' value='Hello World' />" \ + "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" end @@ -689,9 +689,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/", "create-post", method: "delete") do - "<input name='post[title]' type='text' value='Hello World' />" + - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' value='Hello World' />" \ + "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" end @@ -720,9 +720,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/", "create-post", method: "patch") do - "<input name='post[title]' type='text' value='Hello World' />" + - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' value='Hello World' />" \ + "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" end @@ -761,9 +761,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/", "create-post") do - "<input name='post[title]' type='text' value='Hello World' />" + - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' value='Hello World' />" \ + "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" end @@ -779,10 +779,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<label for='post_123_title'>Title</label>" + - "<input name='post[123][title]' type='text' value='Hello World' />" + - "<textarea name='post[123][body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[123][secret]' type='hidden' value='0' />" + + "<label for='post_123_title'>Title</label>" \ + "<input name='post[123][title]' type='text' value='Hello World' />" \ + "<textarea name='post[123][body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[123][secret]' type='hidden' value='0' />" \ "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' />" end @@ -797,9 +797,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[][title]' type='text' value='Hello World' />" + - "<textarea name='post[][body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[][secret]' type='hidden' value='0' />" + + "<input name='post[][title]' type='text' value='Hello World' />" \ + "<textarea name='post[][body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[][secret]' type='hidden' value='0' />" \ "<input name='post[][secret]' checked='checked' type='checkbox' value='1' />" end @@ -814,8 +814,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" + - "<div class='field_with_errors'><input name='post[author_name]' type='text' value='' /></div>" + + "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" \ + "<div class='field_with_errors'><input name='post[author_name]' type='text' value='' /></div>" \ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" end @@ -832,8 +832,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" + - "<div class='field_with_errors'><input name='post[author_name]' type='text' value='' /></div>" + + "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" \ + "<div class='field_with_errors'><input name='post[author_name]' type='text' value='' /></div>" \ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" end @@ -847,7 +847,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" + + "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" \ "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" end @@ -863,9 +863,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do - "<input name='post[title]' type='text' value='Hello World' />" + - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' value='Hello World' />" \ + "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" end @@ -1009,7 +1009,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[123][title]' type='text' value='Hello World' />" + + "<input name='post[123][title]' type='text' value='Hello World' />" \ "<input name='post[123][comment][][name]' type='text' value='new comment' />" end @@ -1025,7 +1025,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<input name='post[1][title]' type='text' value='Hello World' />" + + "<input name='post[1][title]' type='text' value='Hello World' />" \ "<input name='post[1][comment][1][name]' type='text' value='new comment' />" end @@ -1135,7 +1135,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ '<input name="post[author_attributes][name]" type="text" value="new author" />' end @@ -1162,8 +1162,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" />' \ '<input name="post[author_attributes][id]" type="hidden" value="321" />' end @@ -1181,8 +1181,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" />' \ '<input name="post[author_attributes][id]" type="hidden" value="321" />' end @@ -1200,7 +1200,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ '<input name="post[author_attributes][name]" type="text" value="author #321" />' end @@ -1218,7 +1218,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ '<input name="post[author_attributes][name]" type="text" value="author #321" />' end @@ -1236,8 +1236,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" />' \ '<input name="post[author_attributes][id]" type="hidden" value="321" />' end @@ -1256,8 +1256,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[author_attributes][id]" type="hidden" value="321" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[author_attributes][id]" type="hidden" value="321" />' \ '<input name="post[author_attributes][name]" type="text" value="author #321" />' end @@ -1277,10 +1277,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -1304,10 +1304,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[author_attributes][name]" type="text" value="author #321" />' + - '<input name="post[author_attributes][id]" type="hidden" value="321" />' + - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" />' \ + '<input name="post[author_attributes][id]" type="hidden" value="321" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' end @@ -1331,9 +1331,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[author_attributes][name]" type="text" value="author #321" />' + - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' end @@ -1357,10 +1357,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[author_attributes][name]" type="text" value="author #321" />' + - '<input name="post[author_attributes][id]" type="hidden" value="321" />' + - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[author_attributes][name]" type="text" value="author #321" />' \ + '<input name="post[author_attributes][id]" type="hidden" value="321" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' end @@ -1380,10 +1380,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -1404,10 +1404,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' \ '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' end @@ -1427,8 +1427,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[comments_attributes][0][name]" type="text" value="new comment" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="new comment" />' \ '<input name="post[comments_attributes][1][name]" type="text" value="new comment" />' end @@ -1448,9 +1448,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" />' + - '<input name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="321" />' \ '<input name="post[comments_attributes][1][name]" type="text" value="new comment" />' end @@ -1483,10 +1483,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -1504,10 +1504,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -1539,10 +1539,10 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -1562,9 +1562,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[title]" type="text" value="Hello World" />' + - '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" />' + - '<input name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input name="post[title]" type="text" value="Hello World" />' \ + '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="321" />' \ '<input name="post[comments_attributes][1][name]" type="text" value="new comment" />' end @@ -1582,7 +1582,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' + + '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \ '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" />' end @@ -1599,7 +1599,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' + + '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \ '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" />' end @@ -1622,7 +1622,7 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' + + '<input name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \ '<input name="post[comments_attributes][abc][id]" type="hidden" value="321" />' end @@ -1708,17 +1708,17 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" />' + - '<input name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" />' + - '<input name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' + - '<input name="post[comments_attributes][0][id]" type="hidden" value="321" />' + - '<input name="post[tags_attributes][0][value]" type="text" value="tag #123" />' + - '<input name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" />' + - '<input name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' + - '<input name="post[tags_attributes][0][id]" type="hidden" value="123" />' + - '<input name="post[tags_attributes][1][value]" type="text" value="tag #456" />' + - '<input name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" />' + - '<input name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' + + '<input name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \ + '<input name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" />' \ + '<input name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' \ + '<input name="post[comments_attributes][0][id]" type="hidden" value="321" />' \ + '<input name="post[tags_attributes][0][value]" type="text" value="tag #123" />' \ + '<input name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" />' \ + '<input name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' \ + '<input name="post[tags_attributes][0][id]" type="hidden" value="123" />' \ + '<input name="post[tags_attributes][1][value]" type="text" value="tag #456" />' \ + '<input name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" />' \ + '<input name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' \ '<input name="post[tags_attributes][1][id]" type="hidden" value="456" />' end @@ -1749,9 +1749,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<input name='post[title]' type='text' value='Hello World' />" + - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' value='Hello World' />" \ + "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" assert_dom_equal expected, output_buffer @@ -1765,9 +1765,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<input name='post[123][title]' type='text' value='Hello World' />" + - "<textarea name='post[123][body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[123][secret]' type='hidden' value='0' />" + + "<input name='post[123][title]' type='text' value='Hello World' />" \ + "<textarea name='post[123][body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[123][secret]' type='hidden' value='0' />" \ "<input name='post[123][secret]' checked='checked' type='checkbox' value='1' />" assert_dom_equal expected, output_buffer @@ -1781,9 +1781,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<input name='post[][title]' type='text' value='Hello World' />" + - "<textarea name='post[][body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[][secret]' type='hidden' value='0' />" + + "<input name='post[][title]' type='text' value='Hello World' />" \ + "<textarea name='post[][body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[][secret]' type='hidden' value='0' />" \ "<input name='post[][secret]' checked='checked' type='checkbox' value='1' />" assert_dom_equal expected, output_buffer @@ -1797,9 +1797,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<input name='post[abc][title]' type='text' value='Hello World' />" + - "<textarea name='post[abc][body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[abc][secret]' type='hidden' value='0' />" + + "<input name='post[abc][title]' type='text' value='Hello World' />" \ + "<textarea name='post[abc][body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[abc][secret]' type='hidden' value='0' />" \ "<input name='post[abc][secret]' checked='checked' type='checkbox' value='1' />" assert_dom_equal expected, output_buffer @@ -1813,9 +1813,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<input name='post[title]' type='text' value='Hello World' />" + - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' value='Hello World' />" \ + "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" assert_dom_equal expected, output_buffer @@ -1829,9 +1829,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<input name='post[title]' type='text' value='Hello World' />" + - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' value='Hello World' />" \ + "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' value='1' />" assert_dom_equal expected, output_buffer @@ -1843,7 +1843,7 @@ class FormWithActsLikeFormForTest < FormWithTest concat f.text_field(:title) end - assert_dom_equal "<label for=\"author_post_title\">Title</label>" + + assert_dom_equal "<label for=\"author_post_title\">Title</label>" \ "<input name='author[post][title]' type='text' value='Hello World' />", output_buffer end @@ -1854,7 +1854,7 @@ class FormWithActsLikeFormForTest < FormWithTest concat f.text_field(:title) end - assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" + + assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" \ "<input name='author[post][1][title]' type='text' value='Hello World' />", output_buffer end @@ -1874,9 +1874,9 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", "create-post", method: "patch") do - "<input name='post[title]' type='text' value='Hello World' />" + - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" + - "<input name='parent_post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' value='Hello World' />" \ + "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ + "<input name='parent_post[secret]' type='hidden' value='0' />" \ "<input name='parent_post[secret]' checked='checked' type='checkbox' value='1' />" end @@ -1894,8 +1894,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", "create-post", method: "patch") do - "<input name='post[title]' type='text' value='Hello World' />" + - "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" + + "<input name='post[title]' type='text' value='Hello World' />" \ + "<textarea name='post[body]'>\nBack to the hill and over it again!</textarea>" \ "<input name='post[comment][name]' type='text' value='new comment' />" end @@ -1934,8 +1934,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" + - "<label for='body'>Body:</label> <textarea name='post[body]'>\nBack to the hill and over it again!</textarea><br/>" + + "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" \ + "<label for='body'>Body:</label> <textarea name='post[body]'>\nBack to the hill and over it again!</textarea><br/>" \ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' /><br/>" end @@ -1953,8 +1953,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = whole_form("/posts/123", method: "patch") do - "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" + - "<label for='body'>Body:</label> <textarea name='post[body]'>\nBack to the hill and over it again!</textarea><br/>" + + "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" \ + "<label for='body'>Body:</label> <textarea name='post[body]'>\nBack to the hill and over it again!</textarea><br/>" \ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' /><br/>" end @@ -2012,8 +2012,8 @@ class FormWithActsLikeFormForTest < FormWithTest end expected = - "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" + - "<label for='body'>Body:</label> <textarea name='post[body]'>\nBack to the hill and over it again!</textarea><br/>" + + "<label for='title'>Title:</label> <input name='post[title]' type='text' value='Hello World' /><br/>" \ + "<label for='body'>Body:</label> <textarea name='post[body]'>\nBack to the hill and over it again!</textarea><br/>" \ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' value='1' /><br/>" assert_dom_equal expected, output_buffer diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 2bc0434771..b3a180b28a 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -257,11 +257,11 @@ class FormHelperTest < ActionView::TestCase end def test_label_with_non_active_record_object - form_for(OpenStruct.new(name: "ok"), as: "person", url: "an_url", html: { id: "create-person" }) do |f| + form_for(OpenStruct.new(name: "ok"), as: "person", url: "/an", html: { id: "create-person" }) do |f| f.label(:name) end - expected = whole_form("an_url", "create-person", "new_person", method: "post") do + expected = whole_form("/an", "create-person", "new_person", method: "post") do '<label for="person_name">Name</label>' end @@ -767,7 +767,6 @@ class FormHelperTest < ActionView::TestCase '<input name="post[bar][comment_ids][]" type="hidden" value="0" /><input checked="checked" id="post_bar_comment_ids_3" name="post[bar][comment_ids][]" type="checkbox" value="3" />', check_box("post", "comment_ids", { multiple: true, index: "bar" }, 3) ) - end def test_checkbox_disabled_disables_hidden_field @@ -1515,13 +1514,13 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do - "<label for='post_title'>The Title</label>" + - "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + - "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + - "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" + - "<button name='button' type='submit'>Create post</button>" + + "<label for='post_title'>The Title</label>" \ + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ + "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" \ + "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" \ + "<button name='button' type='submit'>Create post</button>" \ "<button name='button' type='submit'><span>Create post</span></button>" end @@ -1536,10 +1535,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do - "<input type='hidden' name='post[active]' value='' />" + - "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + - "<label for='post_active_true'>true</label>" + - "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + + "<input type='hidden' name='post[active]' value='' />" \ + "<input id='post_active_true' name='post[active]' type='radio' value='true' />" \ + "<label for='post_active_true'>true</label>" \ + "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" \ "<label for='post_active_false'>false</label>" end @@ -1558,12 +1557,12 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do - "<input type='hidden' name='post[active]' value='' />" + - "<label for='post_active_true'>" + - "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + - "true</label>" + - "<label for='post_active_false'>" + - "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + + "<input type='hidden' name='post[active]' value='' />" \ + "<label for='post_active_true'>" \ + "<input id='post_active_true' name='post[active]' type='radio' value='true' />" \ + "true</label>" \ + "<label for='post_active_false'>" \ + "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" \ "false</label>" end @@ -1584,13 +1583,13 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post_1", "new_post") do - "<input type='hidden' name='post[active]' value='' />" + - "<label for='post_active_true'>" + - "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + - "true</label>" + - "<label for='post_active_false'>" + - "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + - "false</label>" + + "<input type='hidden' name='post[active]' value='' />" \ + "<label for='post_active_true'>" \ + "<input id='post_active_true' name='post[active]' type='radio' value='true' />" \ + "true</label>" \ + "<label for='post_active_false'>" \ + "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" \ + "false</label>" \ "<input id='post_id' name='post[id]' type='hidden' value='1' />" end @@ -1606,10 +1605,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "foo_new_post", "new_post") do - "<input type='hidden' name='post[active]' value='' />" + - "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" + - "<label for='foo_post_active_true'>true</label>" + - "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" + + "<input type='hidden' name='post[active]' value='' />" \ + "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" \ + "<label for='foo_post_active_true'>true</label>" \ + "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" \ "<label for='foo_post_active_false'>false</label>" end @@ -1625,10 +1624,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do - "<input type='hidden' name='post[1][active]' value='' />" + - "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" + - "<label for='post_1_active_true'>true</label>" + - "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" + + "<input type='hidden' name='post[1][active]' value='' />" \ + "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" \ + "<label for='post_1_active_true'>true</label>" \ + "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" \ "<label for='post_1_active_false'>false</label>" end @@ -1644,12 +1643,12 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do - "<input name='post[tag_ids][]' type='hidden' value='' />" + - "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + - "<label for='post_tag_ids_1'>Tag 1</label>" + - "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" + - "<label for='post_tag_ids_2'>Tag 2</label>" + - "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "<label for='post_tag_ids_1'>Tag 1</label>" \ + "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" \ + "<label for='post_tag_ids_2'>Tag 2</label>" \ + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" \ "<label for='post_tag_ids_3'>Tag 3</label>" end @@ -1668,15 +1667,15 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do - "<input name='post[tag_ids][]' type='hidden' value='' />" + - "<label for='post_tag_ids_1'>" + - "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + - "Tag 1</label>" + - "<label for='post_tag_ids_2'>" + - "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" + - "Tag 2</label>" + - "<label for='post_tag_ids_3'>" + - "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<label for='post_tag_ids_1'>" \ + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "Tag 1</label>" \ + "<label for='post_tag_ids_2'>" \ + "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" \ + "Tag 2</label>" \ + "<label for='post_tag_ids_3'>" \ + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" \ "Tag 3</label>" end @@ -1698,16 +1697,16 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post_1", "new_post") do - "<input name='post[tag_ids][]' type='hidden' value='' />" + - "<label for='post_tag_ids_1'>" + - "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + - "Tag 1</label>" + - "<label for='post_tag_ids_2'>" + - "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" + - "Tag 2</label>" + - "<label for='post_tag_ids_3'>" + - "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + - "Tag 3</label>" + + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<label for='post_tag_ids_1'>" \ + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \ + "Tag 1</label>" \ + "<label for='post_tag_ids_2'>" \ + "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" \ + "Tag 2</label>" \ + "<label for='post_tag_ids_3'>" \ + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" \ + "Tag 3</label>" \ "<input id='post_id' name='post[id]' type='hidden' value='1' />" end @@ -1724,8 +1723,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "foo_new_post", "new_post") do - "<input name='post[tag_ids][]' type='hidden' value='' />" + - "<input checked='checked' id='foo_post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + + "<input name='post[tag_ids][]' type='hidden' value='' />" \ + "<input checked='checked' id='foo_post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" \ "<label for='foo_post_tag_ids_1'>Tag 1</label>" end @@ -1742,8 +1741,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do - "<input name='post[1][tag_ids][]' type='hidden' value='' />" + - "<input checked='checked' id='post_1_tag_ids_1' name='post[1][tag_ids][]' type='checkbox' value='1' />" + + "<input name='post[1][tag_ids][]' type='hidden' value='' />" \ + "<input checked='checked' id='post_1_tag_ids_1' name='post[1][tag_ids][]' type='checkbox' value='1' />" \ "<label for='post_1_tag_ids_1'>Tag 1</label>" end @@ -1797,7 +1796,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/44", "edit_post_44", "edit_post", method: "patch") do - "<input name='post[title]' type='text' id='post_title' value='And his name will be forty and four.' />" + + "<input name='post[title]' type='text' id='post_title' value='And his name will be forty and four.' />" \ "<input name='commit' data-disable-with='Edit post' type='submit' value='Edit post' />" end @@ -1814,11 +1813,11 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "create-post", "edit_other_name", method: "patch") do - "<label for='other_name_title' class='post_title'>Title</label>" + - "<input name='other_name[title]' id='other_name_title' value='Hello World' type='text' />" + - "<textarea name='other_name[body]' id='other_name_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='other_name[secret]' value='0' type='hidden' />" + - "<input name='other_name[secret]' checked='checked' id='other_name_secret' value='1' type='checkbox' />" + + "<label for='other_name_title' class='post_title'>Title</label>" \ + "<input name='other_name[title]' id='other_name_title' value='Hello World' type='text' />" \ + "<textarea name='other_name[body]' id='other_name_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='other_name[secret]' value='0' type='hidden' />" \ + "<input name='other_name[secret]' checked='checked' id='other_name_secret' value='1' type='checkbox' />" \ "<input name='commit' value='Create post' data-disable-with='Create post' type='submit' />" end @@ -1847,9 +1846,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/", "create-post", "edit_post", method: "delete") do - "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" end @@ -1864,9 +1863,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/", "create-post", "edit_post", method: "delete") do - "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" end @@ -1895,9 +1894,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/", "create-post", "edit_post", method: "patch", remote: true) do - "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" end @@ -1936,9 +1935,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/", "create-post", "edit_post", method: "patch", remote: true) do - "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" end @@ -1955,9 +1954,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post", remote: true) do - "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" end @@ -1973,9 +1972,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/", "create-post") do - "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" end @@ -1991,10 +1990,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post[]", "edit_post[]", method: "patch") do - "<label for='post_123_title'>Title</label>" + - "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" + - "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[123][secret]' type='hidden' value='0' />" + + "<label for='post_123_title'>Title</label>" \ + "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" \ + "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[123][secret]' type='hidden' value='0' />" \ "<input name='post[123][secret]' checked='checked' type='checkbox' id='post_123_secret' value='1' />" end @@ -2009,9 +2008,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post[]", "edit_post[]", method: "patch") do - "<input name='post[][title]' type='text' id='post__title' value='Hello World' />" + - "<textarea name='post[][body]' id='post__body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[][secret]' type='hidden' value='0' />" + + "<input name='post[][title]' type='text' id='post__title' value='Hello World' />" \ + "<textarea name='post[][body]' id='post__body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[][secret]' type='hidden' value='0' />" \ "<input name='post[][secret]' checked='checked' type='checkbox' id='post__secret' value='1' />" end @@ -2026,8 +2025,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" + - "<div class='field_with_errors'><input name='post[author_name]' type='text' id='post_author_name' value='' /></div>" + + "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" \ + "<div class='field_with_errors'><input name='post[author_name]' type='text' id='post_author_name' value='' /></div>" \ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" end @@ -2044,8 +2043,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" + - "<div class='field_with_errors'><input name='post[author_name]' type='text' id='post_author_name' value='' /></div>" + + "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" \ + "<div class='field_with_errors'><input name='post[author_name]' type='text' id='post_author_name' value='' /></div>" \ "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />" end @@ -2059,7 +2058,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" + + "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" \ "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" end @@ -2074,9 +2073,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do - "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" + - "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' id='namespace_post_secret' value='1' />" end @@ -2098,7 +2097,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do - "<label for='namespace_post_title'>Title</label>" + + "<label for='namespace_post_title'>Title</label>" \ "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" end @@ -2124,7 +2123,7 @@ class FormHelperTest < ActionView::TestCase end expected_1 = whole_form("/posts/123", "namespace_1_edit_post_123", "edit_post", method: "patch") do - "<label for='namespace_1_post_title'>Title</label>" + + "<label for='namespace_1_post_title'>Title</label>" \ "<input name='post[title]' type='text' id='namespace_1_post_title' value='Hello World' />" end @@ -2136,7 +2135,7 @@ class FormHelperTest < ActionView::TestCase end expected_2 = whole_form("/posts/123", "namespace_2_edit_post_123", "edit_post", method: "patch") do - "<label for='namespace_2_post_title'>Title</label>" + + "<label for='namespace_2_post_title'>Title</label>" \ "<input name='post[title]' type='text' id='namespace_2_post_title' value='Hello World' />" end @@ -2154,8 +2153,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "namespace_edit_post_123", "edit_post", method: "patch") do - "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" + - "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" + + "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" \ "<input name='post[comment][body]' type='text' id='namespace_post_comment_body' value='Hello World' />" end @@ -2266,8 +2265,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post[]", "edit_post[]", method: "patch") do - "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" + - "<input name='post[123][comment][][name]' type='text' id='post_123_comment__name' value='new comment' />" + + "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" \ + "<input name='post[123][comment][][name]' type='text' id='post_123_comment__name' value='new comment' />" \ "<input name='post[123][body]' type='text' id='post_123_body' value='Back to the hill and over it again!' />" end @@ -2283,7 +2282,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - "<input name='post[1][title]' type='text' id='post_1_title' value='Hello World' />" + + "<input name='post[1][title]' type='text' id='post_1_title' value='Hello World' />" \ "<input name='post[1][comment][1][name]' type='text' id='post_1_comment_1_name' value='new comment' />" end @@ -2393,7 +2392,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="new author" />' end @@ -2420,8 +2419,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' end @@ -2439,8 +2438,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' end @@ -2458,7 +2457,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' end @@ -2476,7 +2475,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' end @@ -2494,8 +2493,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' end @@ -2514,8 +2513,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' \ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' end @@ -2535,10 +2534,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -2562,10 +2561,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + - '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \ + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' end @@ -2589,9 +2588,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' end @@ -2615,10 +2614,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' + - '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' \ + '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' end @@ -2638,10 +2637,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -2662,10 +2661,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' end @@ -2685,8 +2684,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="new comment" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="new comment" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />' end @@ -2706,9 +2705,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />' end @@ -2741,10 +2740,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -2762,10 +2761,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -2797,10 +2796,10 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' + - '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' \ + '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' \ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' end @@ -2820,9 +2819,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input name="post[title]" type="text" id="post_title" value="Hello World" />' + - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + + '<input name="post[title]" type="text" id="post_title" value="Hello World" />' \ + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' \ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />' end @@ -2840,7 +2839,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \ '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' end @@ -2857,7 +2856,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \ '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' end @@ -2880,7 +2879,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' + + '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' \ '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />' end @@ -2966,17 +2965,17 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' + - '<input id="post_comments_attributes_0_relevances_attributes_0_value" name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" />' + - '<input id="post_comments_attributes_0_relevances_attributes_0_id" name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' + - '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' + - '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" type="text" value="tag #123" />' + - '<input id="post_tags_attributes_0_relevances_attributes_0_value" name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" />' + - '<input id="post_tags_attributes_0_relevances_attributes_0_id" name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' + - '<input id="post_tags_attributes_0_id" name="post[tags_attributes][0][id]" type="hidden" value="123" />' + - '<input id="post_tags_attributes_1_value" name="post[tags_attributes][1][value]" type="text" value="tag #456" />' + - '<input id="post_tags_attributes_1_relevances_attributes_0_value" name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" />' + - '<input id="post_tags_attributes_1_relevances_attributes_0_id" name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' + + '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' \ + '<input id="post_comments_attributes_0_relevances_attributes_0_value" name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" />' \ + '<input id="post_comments_attributes_0_relevances_attributes_0_id" name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' \ + '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' \ + '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" type="text" value="tag #123" />' \ + '<input id="post_tags_attributes_0_relevances_attributes_0_value" name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" />' \ + '<input id="post_tags_attributes_0_relevances_attributes_0_id" name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' \ + '<input id="post_tags_attributes_0_id" name="post[tags_attributes][0][id]" type="hidden" value="123" />' \ + '<input id="post_tags_attributes_1_value" name="post[tags_attributes][1][value]" type="text" value="tag #456" />' \ + '<input id="post_tags_attributes_1_relevances_attributes_0_value" name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" />' \ + '<input id="post_tags_attributes_1_relevances_attributes_0_id" name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' \ '<input id="post_tags_attributes_1_id" name="post[tags_attributes][1][id]" type="hidden" value="456" />' end @@ -3007,9 +3006,9 @@ class FormHelperTest < ActionView::TestCase end expected = - "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" assert_dom_equal expected, output_buffer @@ -3023,9 +3022,9 @@ class FormHelperTest < ActionView::TestCase end expected = - "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" + - "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[123][secret]' type='hidden' value='0' />" + + "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" \ + "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[123][secret]' type='hidden' value='0' />" \ "<input name='post[123][secret]' checked='checked' type='checkbox' id='post_123_secret' value='1' />" assert_dom_equal expected, output_buffer @@ -3039,9 +3038,9 @@ class FormHelperTest < ActionView::TestCase end expected = - "<input name='post[][title]' type='text' id='post__title' value='Hello World' />" + - "<textarea name='post[][body]' id='post__body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[][secret]' type='hidden' value='0' />" + + "<input name='post[][title]' type='text' id='post__title' value='Hello World' />" \ + "<textarea name='post[][body]' id='post__body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[][secret]' type='hidden' value='0' />" \ "<input name='post[][secret]' checked='checked' type='checkbox' id='post__secret' value='1' />" assert_dom_equal expected, output_buffer @@ -3055,9 +3054,9 @@ class FormHelperTest < ActionView::TestCase end expected = - "<input name='post[abc][title]' type='text' id='post_abc_title' value='Hello World' />" + - "<textarea name='post[abc][body]' id='post_abc_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[abc][secret]' type='hidden' value='0' />" + + "<input name='post[abc][title]' type='text' id='post_abc_title' value='Hello World' />" \ + "<textarea name='post[abc][body]' id='post_abc_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[abc][secret]' type='hidden' value='0' />" \ "<input name='post[abc][secret]' checked='checked' type='checkbox' id='post_abc_secret' value='1' />" assert_dom_equal expected, output_buffer @@ -3071,9 +3070,9 @@ class FormHelperTest < ActionView::TestCase end expected = - "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" assert_dom_equal expected, output_buffer @@ -3087,9 +3086,9 @@ class FormHelperTest < ActionView::TestCase end expected = - "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='post[secret]' type='hidden' value='0' />" \ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" assert_dom_equal expected, output_buffer @@ -3101,7 +3100,7 @@ class FormHelperTest < ActionView::TestCase concat f.text_field(:title) end - assert_dom_equal "<label for=\"author_post_title\">Title</label>" + + assert_dom_equal "<label for=\"author_post_title\">Title</label>" \ "<input name='author[post][title]' type='text' id='author_post_title' value='Hello World' />", output_buffer end @@ -3112,7 +3111,7 @@ class FormHelperTest < ActionView::TestCase concat f.text_field(:title) end - assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" + + assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" \ "<input name='author[post][1][title]' type='text' id='author_post_1_title' value='Hello World' />", output_buffer end @@ -3132,9 +3131,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do - "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + - "<input name='parent_post[secret]' type='hidden' value='0' />" + + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ + "<input name='parent_post[secret]' type='hidden' value='0' />" \ "<input name='parent_post[secret]' checked='checked' type='checkbox' id='parent_post_secret' value='1' />" end @@ -3152,8 +3151,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do - "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + - "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" + + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" \ + "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" \ "<input name='post[comment][name]' type='text' id='post_comment_name' value='new comment' />" end @@ -3192,8 +3191,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + - "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" + + "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" \ + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" \ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>" end @@ -3211,8 +3210,8 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch") do - "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + - "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" + + "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" \ + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" \ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>" end @@ -3270,8 +3269,8 @@ class FormHelperTest < ActionView::TestCase end expected = - "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" + - "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" + + "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" \ + "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" \ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>" assert_dom_equal expected, output_buffer diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb index 51f5d25958..258dcdb806 100644 --- a/actionview/test/template/form_options_helper_test.rb +++ b/actionview/test/template/form_options_helper_test.rb @@ -393,30 +393,30 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_options_no_params opts = time_zone_options_for_select - assert_dom_equal "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\">D</option>\n" + + assert_dom_equal "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\">D</option>\n" \ "<option value=\"E\">E</option>", opts end def test_time_zone_options_with_selected opts = time_zone_options_for_select("D") - assert_dom_equal "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + + assert_dom_equal "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ "<option value=\"E\">E</option>", opts end def test_time_zone_options_with_unknown_selected opts = time_zone_options_for_select("K") - assert_dom_equal "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\">D</option>\n" + + assert_dom_equal "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\">D</option>\n" \ "<option value=\"E\">E</option>", opts end @@ -424,11 +424,11 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_options_with_priority_zones zones = [ ActiveSupport::TimeZone.new("B"), ActiveSupport::TimeZone.new("E") ] opts = time_zone_options_for_select(nil, zones) - assert_dom_equal "<option value=\"B\">B</option>\n" + - "<option value=\"E\">E</option>" + - "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"C\">C</option>\n" + + assert_dom_equal "<option value=\"B\">B</option>\n" \ + "<option value=\"E\">E</option>" \ + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"C\">C</option>\n" \ "<option value=\"D\">D</option>", opts end @@ -436,11 +436,11 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_options_with_selected_priority_zones zones = [ ActiveSupport::TimeZone.new("B"), ActiveSupport::TimeZone.new("E") ] opts = time_zone_options_for_select("E", zones) - assert_dom_equal "<option value=\"B\">B</option>\n" + - "<option value=\"E\" selected=\"selected\">E</option>" + - "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"C\">C</option>\n" + + assert_dom_equal "<option value=\"B\">B</option>\n" \ + "<option value=\"E\" selected=\"selected\">E</option>" \ + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"C\">C</option>\n" \ "<option value=\"D\">D</option>", opts end @@ -448,11 +448,11 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_options_with_unselected_priority_zones zones = [ ActiveSupport::TimeZone.new("B"), ActiveSupport::TimeZone.new("E") ] opts = time_zone_options_for_select("C", zones) - assert_dom_equal "<option value=\"B\">B</option>\n" + - "<option value=\"E\">E</option>" + - "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"C\" selected=\"selected\">C</option>\n" + + assert_dom_equal "<option value=\"B\">B</option>\n" \ + "<option value=\"E\">E</option>" \ + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"C\" selected=\"selected\">C</option>\n" \ "<option value=\"D\">D</option>", opts end @@ -1031,12 +1031,12 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_select @firm = Firm.new("D") html = time_zone_select("firm", "time_zone") - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1049,12 +1049,12 @@ class FormOptionsHelperTest < ActionView::TestCase end assert_dom_equal( - "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", output_buffer ) @@ -1068,12 +1068,12 @@ class FormOptionsHelperTest < ActionView::TestCase end assert_dom_equal( - "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", output_buffer ) @@ -1088,12 +1088,12 @@ class FormOptionsHelperTest < ActionView::TestCase end assert_dom_equal( - "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", output_buffer ) @@ -1102,13 +1102,13 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_select_with_blank @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, include_blank: true) - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"\"></option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"\"></option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1116,13 +1116,13 @@ class FormOptionsHelperTest < ActionView::TestCase def test_time_zone_select_with_blank_as_string @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, include_blank: "No Zone") - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"\">No Zone</option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"\">No Zone</option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1131,12 +1131,12 @@ class FormOptionsHelperTest < ActionView::TestCase @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, {}, "style" => "color: red") - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html assert_dom_equal html, time_zone_select("firm", "time_zone", nil, {}, @@ -1147,13 +1147,13 @@ class FormOptionsHelperTest < ActionView::TestCase @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, { include_blank: true }, "style" => "color: red") - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" + - "<option value=\"\"></option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" \ + "<option value=\"\"></option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html assert_dom_equal html, time_zone_select("firm", "time_zone", nil, @@ -1164,13 +1164,13 @@ class FormOptionsHelperTest < ActionView::TestCase @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, { include_blank: "No Zone" }, "style" => "color: red") - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" + - "<option value=\"\">No Zone</option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" \ + "<option value=\"\">No Zone</option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html assert_dom_equal html, time_zone_select("firm", "time_zone", nil, @@ -1181,13 +1181,13 @@ class FormOptionsHelperTest < ActionView::TestCase @firm = Firm.new("D") zones = [ ActiveSupport::TimeZone.new("A"), ActiveSupport::TimeZone.new("D") ] html = time_zone_select("firm", "time_zone", zones) - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>" + - "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>" \ + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1200,13 +1200,13 @@ class FormOptionsHelperTest < ActionView::TestCase end html = time_zone_select("firm", "time_zone", /A|D/) - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>" + - "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>" \ + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1221,13 +1221,13 @@ class FormOptionsHelperTest < ActionView::TestCase end html = time_zone_select("firm", "time_zone", /A|D/) - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"\" disabled=\"disabled\">-------------</option>\n" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"\" disabled=\"disabled\">-------------</option>\n" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1237,12 +1237,12 @@ class FormOptionsHelperTest < ActionView::TestCase @firm.time_zone = nil html = time_zone_select("firm", "time_zone", nil, default: "B") - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\" selected=\"selected\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\" selected=\"selected\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end @@ -1251,12 +1251,12 @@ class FormOptionsHelperTest < ActionView::TestCase @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, default: "B") - assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" + - "<option value=\"A\">A</option>\n" + - "<option value=\"B\">B</option>\n" + - "<option value=\"C\">C</option>\n" + - "<option value=\"D\" selected=\"selected\">D</option>\n" + - "<option value=\"E\">E</option>" + + assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" \ + "<option value=\"A\">A</option>\n" \ + "<option value=\"B\">B</option>\n" \ + "<option value=\"C\">C</option>\n" \ + "<option value=\"D\" selected=\"selected\">D</option>\n" \ + "<option value=\"E\">E</option>" \ "</select>", html end diff --git a/actionview/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb index 8691bb11ee..537b4393ee 100644 --- a/actionview/test/template/output_safety_helper_test.rb +++ b/actionview/test/template/output_safety_helper_test.rb @@ -33,6 +33,22 @@ class OutputSafetyHelperTest < ActionView::TestCase assert_equal ""a" <br/> <b> <br/> <c>", joined end + test "safe_join should return the safe string separated by $, when second argument is not passed" do + default_delimeter = $, + + begin + $, = nil + joined = safe_join(["a", "b"]) + assert_equal "ab", joined + + $, = "|" + joined = safe_join(["a", "b"]) + assert_equal "a|b", joined + ensure + $, = default_delimeter + end + end + test "to_sentence should escape non-html_safe values" do actual = to_sentence(%w(< > & ' ")) assert actual.html_safe? @@ -87,4 +103,15 @@ class OutputSafetyHelperTest < ActionView::TestCase assert_equal "one, two three", to_sentence(["one", "two", "three"], last_word_connector: " ") assert_equal "one, two and three", to_sentence(["one", "two", "three"], last_word_connector: " and ") end + + test "to_sentence is not affected by $," do + separator_was = $, + $, = "|" + begin + assert_equal "one and two", to_sentence(["one", "two"]) + assert_equal "one, two, and three", to_sentence(["one", "two", "three"]) + ensure + $, = separator_was + end + end end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index e7e0b147c7..412948719c 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -220,15 +220,15 @@ module RenderTestCases def test_render_partial_with_invalid_option_as e = assert_raises(ArgumentError) { @view.render(partial: "test/partial_only", as: "a-in") } - assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " + - "make sure it starts with lowercase letter, " + + assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " \ + "make sure it starts with lowercase letter, " \ "and is followed by any combination of letters, numbers and underscores.", e.message end def test_render_partial_with_hyphen_and_invalid_option_as e = assert_raises(ArgumentError) { @view.render(partial: "test/a-in", as: "a-in") } - assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " + - "make sure it starts with lowercase letter, " + + assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " \ + "make sure it starts with lowercase letter, " \ "and is followed by any combination of letters, numbers and underscores.", e.message end @@ -253,7 +253,7 @@ module RenderTestCases def test_render_sub_template_with_errors e = assert_raises(ActionView::Template::Error) { @view.render(template: "test/sub_template_raise") } assert_match %r!method.*doesnt_exist!, e.message - assert_equal "Trace of template inclusion: #{File.expand_path("#{FIXTURE_LOAD_PATH}/test/sub_template_raise.html.erb")}", e.sub_template_message + assert_match %r{Trace of template inclusion: .*test/sub_template_raise.html.erb}, e.sub_template_message assert_equal "1", e.line_number assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name end @@ -301,6 +301,15 @@ module RenderTestCases @view.render(partial: "test/local_inspector", collection: [ Customer.new("mary") ]) end + def test_render_partial_collection_with_different_partials_still_provides_partial_iteration + a = {} + b = {} + def a.to_partial_path; "test/partial_iteration_1"; end + def b.to_partial_path; "test/partial_iteration_2"; end + + assert_equal "local-variable\nlocal-variable", @controller_view.render([a, b]) + end + def test_render_partial_with_empty_collection_should_return_nil assert_nil @view.render(partial: "test/customer", collection: []) end @@ -393,13 +402,11 @@ module RenderTestCases assert_equal :partial_name_local_variable, exception.cause.name end - # TODO: The reason for this test is unclear, improve documentation - def test_render_partial_and_fallback_to_layout + def test_render_partial_with_no_block_given_to_yield assert_equal "Before (Josh)\n\nAfter", @view.render(partial: "test/layout_for_partial", locals: { name: "Josh" }) end - # TODO: The reason for this test is unclear, improve documentation - def test_render_missing_xml_partial_and_raise_missing_template + def test_render_partial_with_non_existent_format_and_raise_missing_template @view.formats = [:xml] assert_raises(ActionView::MissingTemplate) { @view.render(partial: "test/layout_for_partial") } ensure @@ -424,7 +431,7 @@ module RenderTestCases end CustomHandler = lambda do |template| - "@output_buffer = ''\n" + + "@output_buffer = ''\n" \ "@output_buffer << 'source: #{template.source.inspect}'\n" end @@ -475,11 +482,9 @@ module RenderTestCases end def test_render_ignores_templates_with_malformed_template_handlers - ActiveSupport::Deprecation.silence do - %w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name| - assert File.exist?(File.expand_path("#{FIXTURE_LOAD_PATH}/test/malformed/#{name}~")), "Malformed file (#{name}~) which should be ignored does not exists" - assert_raises(ActionView::MissingTemplate) { @view.render(file: "test/malformed/#{name}") } - end + %w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name| + assert File.exist?(File.expand_path("#{FIXTURE_LOAD_PATH}/test/malformed/#{name}~")), "Malformed file (#{name}~) which should be ignored does not exists" + assert_raises(ActionView::MissingTemplate) { @view.render(file: "test/malformed/#{name}") } end end diff --git a/actionview/test/template/text_test.rb b/actionview/test/template/text_test.rb index ee526dc367..72ffd5f3be 100644 --- a/actionview/test/template/text_test.rb +++ b/actionview/test/template/text_test.rb @@ -4,4 +4,20 @@ class TextTest < ActiveSupport::TestCase test "formats always return :text" do assert_equal [:text], ActionView::Template::Text.new("").formats end + + test "identifier should return 'text template'" do + assert_equal "text template", ActionView::Template::Text.new("").identifier + end + + test "inspect should return 'text template'" do + assert_equal "text template", ActionView::Template::Text.new("").inspect + end + + test "to_str should return a given string" do + assert_equal "a cat", ActionView::Template::Text.new("a cat").to_str + end + + test "render should return a given string" do + assert_equal "a dog", ActionView::Template::Text.new("a dog").render + end end diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 1e64385b52..09454b32cc 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -235,14 +235,14 @@ class UrlHelperTest < ActiveSupport::TestCase end end - def test_button_to_with_permited_strong_params + def test_button_to_with_permitted_strong_params assert_dom_equal( %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="baz" value="quux" /><input type="hidden" name="foo" value="bar" /></form>}, button_to("Hello", "http://www.example.com", params: FakeParams.new) ) end - def test_button_to_with_unpermited_strong_params + def test_button_to_with_unpermitted_strong_params assert_raises(ArgumentError) do button_to("Hello", "http://www.example.com", params: FakeParams.new(false)) end @@ -496,6 +496,15 @@ class UrlHelperTest < ActiveSupport::TestCase assert current_page?("http://www.example.com/") end + def test_current_page_considering_params + @request = request_for_url("/?order=desc&page=1") + + assert !current_page?(url_hash, check_parameters: true) + assert !current_page?(url_hash.merge(check_parameters: true)) + assert !current_page?(ActionController::Parameters.new(url_hash.merge(check_parameters: true)).permit!) + assert !current_page?("http://www.example.com/", check_parameters: true) + end + def test_current_page_with_params_that_match @request = request_for_url("/?order=desc&page=1") @@ -812,7 +821,7 @@ class TasksController < ActionController::Base private def render_default - render inline: "<%= link_to_unless_current('tasks', tasks_path) %>\n" + + render inline: "<%= link_to_unless_current('tasks', tasks_path) %>\n" \ "<%= link_to_unless_current('tasks', tasks_url) %>" end end diff --git a/actionview/test/ujs/public/test/call-remote-callbacks.js b/actionview/test/ujs/public/test/call-remote-callbacks.js index 082d10bfbd..707e21541d 100644 --- a/actionview/test/ujs/public/test/call-remote-callbacks.js +++ b/actionview/test/ujs/public/test/call-remote-callbacks.js @@ -108,202 +108,6 @@ asyncTest('stopping the "ajax:beforeSend" event aborts the request', 1, function }) }) -asyncTest('blank required form input field should abort request and trigger "ajax:aborted:required" event', 5, function() { - $(document).bind('iframe:loading', function() { - ok(false, 'form should not get submitted') - }) - - var form = $('form[data-remote]') - .append($('<input type="text" name="user_name" required="required">')) - .append($('<textarea name="user_bio" required="required"></textarea>')) - .bindNative('ajax:beforeSend', function() { - ok(false, 'ajax:beforeSend should not run') - }) - .bindNative('ajax:aborted:required', function(e, data) { - data = $(data) - ok(data.length == 2, 'ajax:aborted:required event is passed all blank required inputs (jQuery objects)') - ok(data.first().is('input[name="user_name"]'), 'ajax:aborted:required adds blank required input to data') - ok(data.last().is('textarea[name="user_bio"]'), 'ajax:aborted:required adds blank required textarea to data') - ok(true, 'ajax:aborted:required should run') - }) - .triggerNative('submit') - - setTimeout(function() { - form.find('input[required],textarea[required]').val('Tyler') - form.unbind('ajax:beforeSend') - submit() - }, 13) -}) - -asyncTest('blank required form input for non-remote form should abort normal submission', 1, function() { - var form = $('form[data-remote]') - .append($('<input type="text" name="user_name" required="required">')) - .removeAttr('data-remote') - .bindNative('ujs:everythingStopped', function() { - ok(true, 'ujs:everythingStopped should run') - }) - .triggerNative('submit') - - setTimeout(function() { - start() - }, 13) -}) - -asyncTest('form should be submitted with blank required fields if handler is bound to "ajax:aborted:required" event that returns false', 1, function() { - var form = $('form[data-remote]') - .append($('<input type="text" name="user_name" required="required">')) - .bindNative('ajax:beforeSend', function() { - ok(true, 'ajax:beforeSend should run') - }) - .bindNative('ajax:aborted:required', function() { - return false - }) - .triggerNative('submit') - - setTimeout(function() { - start() - }, 13) -}) - -asyncTest('disabled fields should not be included in blank required check', 2, function() { - var form = $('form[data-remote]') - .append($('<input type="text" name="user_name" required="required" disabled="disabled">')) - .append($('<textarea name="user_bio" required="required" disabled="disabled"></textarea>')) - .bindNative('ajax:beforeSend', function() { - ok(true, 'ajax:beforeSend should run') - }) - .bindNative('ajax:aborted:required', function() { - ok(false, 'ajax:aborted:required should not run') - }) - - submit() -}) - -asyncTest('form should be submitted with blank required fields if it has the "novalidate" attribute', 2, function() { - var form = $('form[data-remote]') - .append($('<input type="text" name="user_name" required="required">')) - .attr('novalidate', 'novalidate') - .bindNative('ajax:beforeSend', function() { - ok(true, 'ajax:beforeSend should run') - }) - .bindNative('ajax:aborted:required', function() { - ok(false, 'ajax:aborted:required should not run') - }) - - submit() -}) - -asyncTest('form should be submitted with blank required fields if the button has the "formnovalidate" attribute', 2, function() { - var submit_button = $('<input type="submit" formnovalidate>') - var form = $('form[data-remote]') - .append($('<input type="text" name="user_name" required="required">')) - .append(submit_button) - .bindNative('ajax:beforeSend', function() { - ok(true, 'ajax:beforeSend should run') - }) - .bindNative('ajax:aborted:required', function() { - ok(false, 'ajax:aborted:required should not run') - }) - - submit_with_button(submit_button) -}) - -asyncTest('blank required form input for non-remote form with "novalidate" attribute should not abort normal submission', 1, function() { - $(document).bind('iframe:loading', function() { - ok(true, 'form should get submitted') - }) - - var form = $('form[data-remote]') - .append($('<input type="text" name="user_name" required="required">')) - .removeAttr('data-remote') - .attr('novalidate', 'novalidate') - .triggerNative('submit') - - setTimeout(function() { - start() - }, 13) -}) - -asyncTest('unchecked required checkbox should abort form submission', 1, function() { - var form = $('form[data-remote]') - .append($('<input type="checkbox" name="agree" required="required">')) - .removeAttr('data-remote') - .bindNative('ujs:everythingStopped', function() { - ok(true, 'ujs:everythingStopped should run') - }) - .triggerNative('submit') - - setTimeout(function() { - start() - }, 13) -}) - -asyncTest('unchecked required radio should abort form submission', 3, function() { - var form = $('form[data-remote]') - .append($('<input type="radio" name="yes_no_none" required="required" value=1>')) - .append($('<input type="radio" name="yes_no_none" required="required" value=2>')) - .removeAttr('data-remote') - .bindNative('ujs:everythingStopped', function() { - ok(true, 'ujs:everythingStopped should run') - }) - .bindNative('ajax:aborted:required', function(e, data) { - data = $(data) - equal(data.length, 2, 'blankRequiredInputs should include both radios') - ok(data.first().is('input[type=radio][value=1]'), 'blankRequiredInputs[0] should be the first radio') - }) - .triggerNative('submit') - - setTimeout(function() { - start() - }, 13) -}) - -asyncTest('required radio should only require one to be checked', 1, function() { - $(document).bind('iframe:loading', function() { - ok(true, 'form should get submitted') - }) - - var form = $('form[data-remote]') - .append($('<input type="radio" name="yes_no" required="required" value=1 id="checkme">')) - .append($('<input type="radio" name="yes_no" required="required" value=2>')) - .removeAttr('data-remote') - .bindNative('ujs:everythingStopped', function() { - ok(false, 'ujs:everythingStopped should not run') - }) - .find('#checkme').prop('checked', true) - .end() - .triggerNative('submit') - - setTimeout(function() { - start() - }, 13) -}) - -asyncTest('required radio should only require one to be checked if not all radios are required', 1, function() { - $(document).bind('iframe:loading', function() { - ok(true, 'form should get submitted') - }) - - var form = $('form[data-remote]') - // Check the radio that is not required - .append($('<input type="radio" name="yes_no_maybe" value=1 >')) - // Check the radio that is not required - .append($('<input type="radio" name="yes_no_maybe" value=2 id="checkme">')) - // Only one needs to be required - .append($('<input type="radio" name="yes_no_maybe" required="required" value=3>')) - .removeAttr('data-remote') - .bindNative('ujs:everythingStopped', function() { - ok(false, 'ujs:everythingStopped should not run') - }) - .find('#checkme').prop('checked', true) - .end() - .triggerNative('submit') - - setTimeout(function() { - start() - }, 13) -}) - function skipIt() { // This test cannot work due to the security feature in browsers which makes the value // attribute of file input fields readonly, so it cannot be set with default value. diff --git a/actionview/test/ujs/public/test/data-remote.js b/actionview/test/ujs/public/test/data-remote.js index a51aa10417..b756add24e 100644 --- a/actionview/test/ujs/public/test/data-remote.js +++ b/actionview/test/ujs/public/test/data-remote.js @@ -73,7 +73,6 @@ asyncTest('clicking on a link with data-remote attribute', 5, function() { .bindNative('ajax:success', function(e, data, status, xhr) { App.assertCallbackInvoked('ajax:success') App.assertRequestPath(data, '/echo') - console.log(data.params) equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value') equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value') App.assertGetRequest(data) @@ -398,3 +397,19 @@ asyncTest('form should be serialized correctly', 6, function() { }) .triggerNative('submit') }) + +asyncTest('form buttons should only be serialized when clicked', 4, function() { + $('form') + .append('<input type="submit" name="submit1" value="submit1" />') + .append('<button name="submit2" value="submit2" />') + .append('<button name="submit3" value="submit3" />') + .bindNative('ajax:success', function(e, data, status, xhr) { + equal(data.params.submit1, undefined) + equal(data.params.submit2, 'submit2') + equal(data.params.submit3, undefined) + equal(data['rack.request.form_vars'], 'user_name=john&submit2=submit2') + + start() + }) + .find('[name=submit2]').triggerNative('click') +}) diff --git a/actionview/test/ujs/public/test/override.js b/actionview/test/ujs/public/test/override.js index be6ec7749b..299c7018cc 100644 --- a/actionview/test/ujs/public/test/override.js +++ b/actionview/test/ujs/public/test/override.js @@ -46,7 +46,7 @@ asyncTest('the event selector strings are overridable', 1, function() { start() }) -asyncTest('including jquery-ujs multiple times throws error', 1, function() { +asyncTest('including rails-ujs multiple times throws error', 1, function() { throws(function() { Rails.start() }, 'appending rails.js again throws error') diff --git a/actionview/test/ujs/public/test/settings.js b/actionview/test/ujs/public/test/settings.js index 3375456f11..299c71bb00 100644 --- a/actionview/test/ujs/public/test/settings.js +++ b/actionview/test/ujs/public/test/settings.js @@ -63,12 +63,12 @@ $(document).bind('submit', function(e) { } }) -var MouseEvent = window.MouseEvent +var _MouseEvent = window.MouseEvent try { - new MouseEvent() + new _MouseEvent() } catch (e) { - MouseEvent = function(type, options) { + _MouseEvent = function(type, options) { var evt = document.createEvent('MouseEvents') evt.initMouseEvent(type, options.bubbles, options.cancelable, window, options.detail, 0, 0, 80, 20, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 0, null) return evt @@ -76,12 +76,12 @@ try { } $.fn.extend({ - // trigger an native click event + // trigger a native click event triggerNative: function(type, options) { var el = this[0], event, Evt = { - 'click': MouseEvent, + 'click': _MouseEvent, 'change': Event, 'pageshow': PageTransitionEvent, 'submit': Event diff --git a/actionview/test/ujs/public/vendor/jquery.metadata.js b/actionview/test/ujs/public/vendor/jquery.metadata.js index ad8bfba404..5b5253cdf7 100644 --- a/actionview/test/ujs/public/vendor/jquery.metadata.js +++ b/actionview/test/ujs/public/vendor/jquery.metadata.js @@ -111,7 +111,7 @@ $.extend({ * * @name metadata * @descr Returns element's metadata object - * @param Object opts An object contianing settings to override the defaults + * @param Object opts An object containing settings to override the defaults * @type jQuery * @cat Plugins/Metadata */ diff --git a/actionview/test/ujs/views/layouts/application.html.erb b/actionview/test/ujs/views/layouts/application.html.erb index 74fa3bd06d..e09b213b72 100644 --- a/actionview/test/ujs/views/layouts/application.html.erb +++ b/actionview/test/ujs/views/layouts/application.html.erb @@ -21,7 +21,7 @@ <%= script_tag jquery_src %> <script> // This is for test in override.js. - // Must go after jQuery is loaded, but before jquery-ujs. + // Must go before rails-ujs. $(document).bind('rails:attachBindings', function() { $.rails.linkClickSelector += ', a[data-custom-remote-link]'; // Hijacks link click before ujs binds any handlers diff --git a/actionview/test/ujs/views/tests/index.html.erb b/actionview/test/ujs/views/tests/index.html.erb index 393a5ee235..2ac44eeb81 100644 --- a/actionview/test/ujs/views/tests/index.html.erb +++ b/actionview/test/ujs/views/tests/index.html.erb @@ -1,4 +1,4 @@ -<% @title = "jquery-ujs test" %> +<% @title = "rails-ujs test" %> <%= test_to 'data-confirm', 'data-remote', 'data-disable', 'data-disable-with', 'call-remote', 'call-remote-callbacks', 'data-method', 'override', 'csrf-refresh', 'csrf-token' %> diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 5e8d8cb5c9..786b508e38 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,13 @@ +* Correctly set test adapter when configure the queue adapter on a per job. + + Fixes #26360. + + *Yuji Yaginuma* + +* Push skipped jobs to `enqueued_jobs` when using `perform_enqueued_jobs` with a `only` filter in tests + + *Alexander Pauly* + * Removed deprecated support to passing the adapter class to `.queue_adapter`. *Rafael Mendonça França* diff --git a/activejob/bin/test b/activejob/bin/test new file mode 100755 index 0000000000..a7beb14b27 --- /dev/null +++ b/activejob/bin/test @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby + +COMPONENT_ROOT = File.expand_path("..", __dir__) +require File.expand_path("../tools/test", COMPONENT_ROOT) diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb index bcc555d33e..9dae80ffc2 100644 --- a/activejob/lib/active_job/queue_adapter.rb +++ b/activejob/lib/active_job/queue_adapter.rb @@ -1,4 +1,3 @@ -require "active_job/queue_adapters/inline_adapter" require "active_support/core_ext/string/inflections" module ActiveJob diff --git a/activejob/lib/active_job/queue_adapters/test_adapter.rb b/activejob/lib/active_job/queue_adapters/test_adapter.rb index da042cfebf..ec825f12cd 100644 --- a/activejob/lib/active_job/queue_adapters/test_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/test_adapter.rb @@ -24,15 +24,11 @@ module ActiveJob end def enqueue(job) #:nodoc: - return if filtered?(job) - job_data = job_to_hash(job) enqueue_or_perform(perform_enqueued_jobs, job, job_data) end def enqueue_at(job, timestamp) #:nodoc: - return if filtered?(job) - job_data = job_to_hash(job, at: timestamp) enqueue_or_perform(perform_enqueued_at_jobs, job, job_data) end @@ -44,11 +40,11 @@ module ActiveJob end def enqueue_or_perform(perform, job, job_data) - if perform + if !perform || filtered?(job) + enqueued_jobs << job_data + else performed_jobs << job_data Base.execute job.serialize - else - enqueued_jobs << job_data end end diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index d01795f0c5..a61e4f59a5 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -8,16 +8,35 @@ module ActiveJob :performed_jobs, :performed_jobs=, to: :queue_adapter + module TestQueueAdapter + extend ActiveSupport::Concern + + included do + class_attribute :_test_adapter, instance_accessor: false, instance_predicate: false + end + + module ClassMethods + def queue_adapter + self._test_adapter.nil? ? super : self._test_adapter + end + + def disable_test_adapter + self._test_adapter = nil + end + + def enable_test_adapter(test_adapter) + self._test_adapter = test_adapter + end + end + end + + ActiveJob::Base.include(TestQueueAdapter) + def before_setup # :nodoc: test_adapter = queue_adapter_for_test - @old_queue_adapters = (ActiveJob::Base.descendants << ActiveJob::Base).select do |klass| - # only override explicitly set adapters, a quirk of `class_attribute` - klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter) - end.map do |klass| - [klass, klass.queue_adapter].tap do - klass.queue_adapter = test_adapter - end + queue_adapter_changed_jobs.each do |klass| + klass.enable_test_adapter(test_adapter) end clear_enqueued_jobs @@ -27,9 +46,8 @@ module ActiveJob def after_teardown # :nodoc: super - @old_queue_adapters.each do |(klass, adapter)| - klass.queue_adapter = adapter - end + + queue_adapter_changed_jobs.each { |klass| klass.disable_test_adapter } end # Specifies the queue adapter to use with all active job test helpers. @@ -55,7 +73,7 @@ module ActiveJob # assert_enqueued_jobs 2 # end # - # If a block is passed, that block should cause the specified number of + # If a block is passed, that block will cause the specified number of # jobs to be enqueued. # # def test_jobs_again @@ -77,14 +95,23 @@ module ActiveJob # HelloJob.perform_later('jeremy') # end # end - def assert_enqueued_jobs(number, only: nil) + # + # The number of times a job is enqueued to a specific queue can also be asserted. + # + # def test_logging_job + # assert_enqueued_jobs 2, queue: 'default' do + # LoggingJob.perform_later + # HelloJob.perform_later('elfassy') + # end + # end + def assert_enqueued_jobs(number, only: nil, queue: nil) if block_given? - original_count = enqueued_jobs_size(only: only) + original_count = enqueued_jobs_size(only: only, queue: queue) yield - new_count = enqueued_jobs_size(only: only) + new_count = enqueued_jobs_size(only: only, queue: queue) assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were enqueued" else - actual_count = enqueued_jobs_size(only: only) + actual_count = enqueued_jobs_size(only: only, queue: queue) assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued" end end @@ -284,7 +311,7 @@ module ActiveJob # def test_perform_enqueued_jobs_with_only # perform_enqueued_jobs(only: MyJob) do # MyJob.perform_later(1, 2, 3) # will be performed - # HelloJob.perform_later(1, 2, 3) # will not be perfomed + # HelloJob.perform_later(1, 2, 3) # will not be performed # end # assert_performed_jobs 1 # end @@ -323,11 +350,16 @@ module ActiveJob performed_jobs.clear end - def enqueued_jobs_size(only: nil) - if only - enqueued_jobs.count { |job| Array(only).include?(job.fetch(:job)) } - else - enqueued_jobs.count + def enqueued_jobs_size(only: nil, queue: nil) + enqueued_jobs.count do |job| + job_class = job.fetch(:job) + if only + next false unless Array(only).include?(job_class) + end + if queue + next false unless queue.to_s == job.fetch(:queue, job_class.queue_name) + end + true end end @@ -344,5 +376,12 @@ module ActiveJob job.queue_name = payload[:queue] job end + + def queue_adapter_changed_jobs + (ActiveJob::Base.descendants << ActiveJob::Base).select do |klass| + # only override explicitly set adapters, a quirk of `class_attribute` + klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter) + end + end end end diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb index 97c11a9ea6..50476a2e50 100644 --- a/activejob/lib/rails/generators/job/job_generator.rb +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -19,7 +19,7 @@ module Rails # :nodoc: template "job.rb", File.join("app/jobs", class_path, "#{file_name}_job.rb") in_root do - if self.behavior == :invoke && !File.exist?(application_job_file_name) + if behavior == :invoke && !File.exist?(application_job_file_name) template "application_job.rb", application_job_file_name end end diff --git a/activejob/test/cases/queue_adapter_test.rb b/activejob/test/cases/queue_adapter_test.rb index f1e0cf78ad..9611b0909b 100644 --- a/activejob/test/cases/queue_adapter_test.rb +++ b/activejob/test/cases/queue_adapter_test.rb @@ -21,6 +21,7 @@ class QueueAdapterTest < ActiveJob::TestCase end test "should allow overriding the queue_adapter at the child class level without affecting the parent or its sibling" do + ActiveJob::Base.disable_test_adapter base_queue_adapter = ActiveJob::Base.queue_adapter child_job_one = Class.new(ActiveJob::Base) diff --git a/activejob/test/cases/queue_naming_test.rb b/activejob/test/cases/queue_naming_test.rb index 0e1e0decd1..0247bc111e 100644 --- a/activejob/test/cases/queue_naming_test.rb +++ b/activejob/test/cases/queue_naming_test.rb @@ -56,7 +56,7 @@ class QueueNamingTest < ActiveSupport::TestCase original_queue_name = HelloJob.queue_name begin - HelloJob.queue_as { self.arguments.first == "1" ? :one : :two } + HelloJob.queue_as { arguments.first == "1" ? :one : :two } assert_equal "one", HelloJob.new("1").queue_name assert_equal "two", HelloJob.new("3").queue_name ensure diff --git a/activejob/test/cases/queue_priority_test.rb b/activejob/test/cases/queue_priority_test.rb index b0d0e903fc..171fb1e593 100644 --- a/activejob/test/cases/queue_priority_test.rb +++ b/activejob/test/cases/queue_priority_test.rb @@ -32,7 +32,7 @@ class QueuePriorityTest < ActiveSupport::TestCase original_priority = HelloJob.priority begin - HelloJob.queue_with_priority { self.arguments.first == "1" ? 99 : 11 } + HelloJob.queue_with_priority { arguments.first == "1" ? 99 : 11 } assert_equal 99, HelloJob.new("1").priority assert_equal 11, HelloJob.new("3").priority ensure diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb index 51fc6cc0c4..2e6357f824 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -56,6 +56,17 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end + def test_assert_enqueued_jobs_when_performing_with_only_option + assert_nothing_raised do + assert_enqueued_jobs 1, only: HelloJob do + perform_enqueued_jobs only: LoggingJob do + HelloJob.perform_later("sean") + LoggingJob.perform_later("yves") + end + end + end + end + def test_assert_no_enqueued_jobs_with_no_block assert_nothing_raised do assert_no_enqueued_jobs @@ -110,6 +121,27 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end + def test_assert_enqueued_jobs_with_only_and_queue_option + assert_nothing_raised do + assert_enqueued_jobs 1, only: HelloJob, queue: :some_queue do + HelloJob.set(queue: :some_queue).perform_later + HelloJob.set(queue: :other_queue).perform_later + LoggingJob.perform_later + end + end + end + + def test_assert_enqueued_jobs_with_queue_option + assert_nothing_raised do + assert_enqueued_jobs 2, queue: :default do + HelloJob.perform_later + LoggingJob.perform_later + HelloJob.set(queue: :other_queue).perform_later + LoggingJob.set(queue: :other_queue).perform_later + end + end + end + def test_assert_enqueued_jobs_with_only_option_and_none_sent error = assert_raise ActiveSupport::TestCase::Assertion do assert_enqueued_jobs 1, only: HelloJob do @@ -528,3 +560,20 @@ class InheritedJobTest < ActiveJob::TestCase assert_instance_of ActiveJob::QueueAdapters::TestAdapter, InheritedJob.queue_adapter end end + +class QueueAdapterJobTest < ActiveJob::TestCase + def before_setup + @original_autoload_paths = ActiveSupport::Dependencies.autoload_paths + ActiveSupport::Dependencies.autoload_paths = %w(test/jobs) + super + end + + def after_teardown + ActiveSupport::Dependencies.autoload_paths = @original_autoload_paths + super + end + + def test_queue_adapter_is_test_adapter + assert_instance_of ActiveJob::QueueAdapters::TestAdapter, QueueAdapterJob.queue_adapter + end +end diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb index 776f7788de..db07ecab7a 100644 --- a/activejob/test/helper.rb +++ b/activejob/test/helper.rb @@ -1,7 +1,6 @@ require "active_job" require "support/job_buffer" -ActiveSupport.halt_callback_chains_on_return_false = false GlobalID.app = "aj" @adapter = ENV["AJ_ADAPTER"] || "inline" diff --git a/activejob/test/jobs/queue_adapter_job.rb b/activejob/test/jobs/queue_adapter_job.rb new file mode 100644 index 0000000000..160dfd74ec --- /dev/null +++ b/activejob/test/jobs/queue_adapter_job.rb @@ -0,0 +1,3 @@ +class QueueAdapterJob < ActiveJob::Base + self.queue_adapter = :inline +end diff --git a/activejob/test/support/integration/adapters/backburner.rb b/activejob/test/support/integration/adapters/backburner.rb index 263097c792..2e194933a1 100644 --- a/activejob/test/support/integration/adapters/backburner.rb +++ b/activejob/test/support/integration/adapters/backburner.rb @@ -23,12 +23,12 @@ module BackburnerJobsManager end def tube - @tube ||= Beaneater::Tube.new(Backburner::Worker.connection, "backburner.worker.queue.integration-tests") # backburner dasherizes the queue name + @tube ||= Beaneater::Tube.new(@worker.connection, "backburner.worker.queue.integration-tests") # backburner dasherizes the queue name end def can_run? begin - Backburner::Worker.connection.send :connect! + @worker = Backburner::Worker.new rescue return false end diff --git a/activejob/test/support/sneakers/inline.rb b/activejob/test/support/sneakers/inline.rb index 3cdc54e6d5..cf102ae5c2 100644 --- a/activejob/test/support/sneakers/inline.rb +++ b/activejob/test/support/sneakers/inline.rb @@ -4,7 +4,7 @@ module Sneakers module Worker module ClassMethods def enqueue(msg) - worker = self.new(nil, nil, {}) + worker = new(nil, nil, {}) worker.work(*msg) end end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 853a1e7d9d..edaac8c7cd 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,11 @@ +* Remove deprecated behavior that halts callbacks when the return is false. + + *Rafael Mendonça França* + +* Remove unused `ActiveModel::TestCase` class. + + *Yuji Yaginuma* + * Moved DecimalWithoutScale, Text, and UnsignedInteger from Active Model to Active Record *Iain Beeston* diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 8163856a46..2389c858d5 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -42,7 +42,6 @@ module ActiveModel autoload :Naming autoload :SecurePassword autoload :Serialization - autoload :TestCase autoload :Translation autoload :Validations autoload :Validator diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 09825cf861..166c6ac21f 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -289,7 +289,7 @@ module ActiveModel generate_method = "define_method_#{matcher.method_missing_target}" if respond_to?(generate_method, true) - send(generate_method, attr_name) + send(generate_method, attr_name.to_s) else define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s end diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index e99bfab6fd..eac2761433 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -103,7 +103,6 @@ module ActiveModel def define_model_callbacks(*callbacks) options = callbacks.extract_options! options = { - terminator: deprecated_false_terminator, skip_after_callbacks_if_terminated: true, scope: [:kind, :name], only: [:before, :around, :after] diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 9532c63b65..9853cf38fe 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -234,7 +234,7 @@ module ActiveModel # Person.model_name.plural # => "people" def model_name @_model_name ||= begin - namespace = self.parents.detect do |n| + namespace = parents.detect do |n| n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming? end ActiveModel::Name.new(self, namespace) diff --git a/activemodel/lib/active_model/test_case.rb b/activemodel/lib/active_model/test_case.rb deleted file mode 100644 index 5004855d56..0000000000 --- a/activemodel/lib/active_model/test_case.rb +++ /dev/null @@ -1,4 +0,0 @@ -module ActiveModel #:nodoc: - class TestCase < ActiveSupport::TestCase #:nodoc: - end -end diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index b8cf43cc10..35fc7cf743 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -44,7 +44,7 @@ module ActiveModel parts = attribute.to_s.split(".") attribute = parts.pop namespace = parts.join("/") unless parts.empty? - attributes_scope = "#{self.i18n_scope}.attributes" + attributes_scope = "#{i18n_scope}.attributes" if namespace defaults = lookup_ancestors.map do |klass| diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb index 6c5c0451c6..541a12c8a1 100644 --- a/activemodel/lib/active_model/type/decimal.rb +++ b/activemodel/lib/active_model/type/decimal.rb @@ -4,6 +4,7 @@ module ActiveModel module Type class Decimal < Value # :nodoc: include Helpers::Numeric + BIGDECIMAL_PRECISION = 18 def type :decimal @@ -21,7 +22,7 @@ module ActiveModel when ::Float convert_float_to_big_decimal(value) when ::Numeric, ::String - BigDecimal(value, precision.to_i) + BigDecimal(value, precision || BIGDECIMAL_PRECISION) else if value.respond_to?(:to_d) value.to_d diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb index 230e45ba60..106b5d966c 100644 --- a/activemodel/lib/active_model/type/integer.rb +++ b/activemodel/lib/active_model/type/integer.rb @@ -48,7 +48,7 @@ module ActiveModel def ensure_in_range(value) unless range.cover?(value) - raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit}" + raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes" end end diff --git a/activemodel/lib/active_model/validations/absence.rb b/activemodel/lib/active_model/validations/absence.rb index 75bf655578..4618f46e30 100644 --- a/activemodel/lib/active_model/validations/absence.rb +++ b/activemodel/lib/active_model/validations/absence.rb @@ -22,7 +22,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_absence_of(*attr_names) validates_with AbsenceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index e11005b9ba..a26c37daa5 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -95,7 +95,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information. + # See <tt>ActiveModel::Validations#validates</tt> for more information. def validates_acceptance_of(*attr_names) validates_with AcceptanceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index 70bc1a0624..4e94422cf1 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -23,7 +23,6 @@ module ActiveModel included do include ActiveSupport::Callbacks define_callbacks :validation, - terminator: deprecated_false_terminator, skip_after_callbacks_if_terminated: true, scope: [:kind, :name] end diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index 33ca6f6946..03585bf5e1 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -69,7 +69,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_confirmation_of(*attr_names) validates_with ConfirmationValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 82a1893823..b7156ba802 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -38,7 +38,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_exclusion_of(*attr_names) validates_with ExclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index fa183885ab..b4b8d9f33c 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -104,7 +104,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_format_of(*attr_names) validates_with FormatValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 0ea6012a5d..c6c5bae649 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -36,7 +36,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_inclusion_of(*attr_names) validates_with InclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index de1524829e..940c58f3a7 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/string/strip" - module ActiveModel module Validations class LengthValidator < EachValidator # :nodoc: @@ -112,7 +110,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+ and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_length_of(*attr_names) validates_with LengthValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 2297faaee5..30a9ef472d 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -63,27 +63,27 @@ module ActiveModel private - def is_number?(raw_value) # :doc: + def is_number?(raw_value) !parse_raw_value_as_a_number(raw_value).nil? rescue ArgumentError, TypeError false end - def parse_raw_value_as_a_number(raw_value) # :doc: + def parse_raw_value_as_a_number(raw_value) Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/ end - def is_integer?(raw_value) # :doc: + def is_integer?(raw_value) /\A[+-]?\d+\z/ === raw_value.to_s end - def filtered_options(value) # :doc: + def filtered_options(value) filtered = options.except(*RESERVED_OPTIONS) filtered[:value] = value filtered end - def allow_only_integer?(record) # :doc: + def allow_only_integer?(record) case options[:only_integer] when Symbol record.send(options[:only_integer]) @@ -134,7 +134,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ . - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information # # The following checks can also be supplied with a proc or a symbol which # corresponds to a method: diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 0c11cf4265..6e8a434bbe 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -29,7 +29,7 @@ module ActiveModel # # There is also a list of default options supported by every validator: # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+. - # See <tt>ActiveModel::Validation#validates</tt> for more information + # See <tt>ActiveModel::Validations#validates</tt> for more information def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index e3f7a9bcb2..9227dd06ff 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -61,7 +61,7 @@ module ActiveModel # The method, proc or string should return or evaluate to a +true+ or # +false+ value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # See <tt>ActiveModel::Validations#validates!</tt> for more information. # # If you pass any additional configuration options, they will be passed # to the class and available as +options+: diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 8212744170..98234e9b6b 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -141,8 +141,8 @@ module ActiveModel end # Performs validation on the supplied record. By default this will call - # +validates_each+ to determine validity therefore subclasses should - # override +validates_each+ with validation logic. + # +validate_each+ to determine validity therefore subclasses should + # override +validate_each+ with validation logic. def validate(record) attributes.each do |attribute| value = record.read_attribute_for_validation(attribute) diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb index 63b6c56f8c..f85cd7dec4 100644 --- a/activemodel/test/cases/callbacks_test.rb +++ b/activemodel/test/cases/callbacks_test.rb @@ -27,7 +27,7 @@ class CallbacksTest < ActiveModel::TestCase false end - ActiveSupport::Deprecation.silence { after_create "@callbacks << :final_callback" } + after_create { |model| model.callbacks << :final_callback } def initialize(options = {}) @callbacks = [] @@ -63,12 +63,10 @@ class CallbacksTest < ActiveModel::TestCase assert_equal model.callbacks.last, :final_callback end - test "the callback chain is halted when a before callback returns false (deprecated)" do + test "the callback chain is not halted when a before callback returns false)" do model = ModelCallbacks.new(before_create_returns: false) - assert_deprecated do - model.create - assert_equal model.callbacks.last, :before_create - end + model.create + assert_equal model.callbacks.last, :final_callback end test "the callback chain is halted when a callback throws :abort" do @@ -127,6 +125,7 @@ class CallbacksTest < ActiveModel::TestCase test "after_create callbacks with both callbacks declared in one line" do assert_equal ["callback1", "callback2"], Violin1.new.create.history end + test "after_create callbacks with both callbacks declared in different lines" do assert_equal ["callback1", "callback2"], Violin2.new.create.history end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index e6ba06301d..0872084cf5 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require "active_support/core_ext/string/strip" require "yaml" class ErrorsTest < ActiveModel::TestCase diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 4e9f43ad86..eeb5c85a48 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -9,15 +9,15 @@ I18n.enforce_available_locales = false require "active_support/testing/autorun" require "active_support/testing/method_call_assertions" -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" -end -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) -end - -class ActiveModel::TestCase +class ActiveModel::TestCase < ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions + + # Skips the current run on Rubinius using Minitest::Assertions#skip + private def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + # Skips the current run on JRuby using Minitest::Assertions#skip + private def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb index 83e8ac9522..f2e4a5946d 100644 --- a/activemodel/test/cases/validations/callbacks_test.rb +++ b/activemodel/test/cases/validations/callbacks_test.rb @@ -29,7 +29,7 @@ class DogWithTwoValidators < Dog before_validation { history << "before_validation_marker2" } end -class DogDeprecatedBeforeValidatorReturningFalse < Dog +class DogBeforeValidatorReturningFalse < Dog before_validation { false } before_validation { history << "before_validation_marker2" } end @@ -121,13 +121,11 @@ class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase assert_equal false, output end - def test_deprecated_further_callbacks_should_not_be_called_if_before_validation_returns_false - d = DogDeprecatedBeforeValidatorReturningFalse.new - assert_deprecated do - output = d.valid? - assert_equal [], d.history - assert_equal false, output - end + def test_further_callbacks_should_be_called_if_before_validation_returns_false + d = DogBeforeValidatorReturningFalse.new + output = d.valid? + assert_equal ["before_validation_marker2"], d.history + assert_equal true, output end def test_further_callbacks_should_be_called_if_after_validation_returns_false diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb index 4881008017..048d27446e 100644 --- a/activemodel/test/cases/validations/conditional_validation_test.rb +++ b/activemodel/test/cases/validations/conditional_validation_test.rb @@ -43,7 +43,9 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_if_validation_using_string_true # When the evaluated string returns true - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "a = 1; a == 1") + ActiveSupport::Deprecation.silence do + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "a = 1; a == 1") + end t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -52,7 +54,9 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_unless_validation_using_string_true # When the evaluated string returns true - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1") + ActiveSupport::Deprecation.silence do + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1") + end t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? assert_empty t.errors[:title] @@ -60,7 +64,9 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_if_validation_using_string_false # When the evaluated string returns false - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false") + ActiveSupport::Deprecation.silence do + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false") + end t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.valid? assert_empty t.errors[:title] @@ -68,7 +74,9 @@ class ConditionalValidationTest < ActiveModel::TestCase def test_unless_validation_using_string_false # When the evaluated string returns false - Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "false") + ActiveSupport::Deprecation.silence do + Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "false") + end t = Topic.new("title" => "uhohuhoh", "content" => "whatever") assert t.invalid? assert t.errors[:title].any? @@ -118,7 +126,9 @@ class ConditionalValidationTest < ActiveModel::TestCase # ensure that it works correctly def test_validation_with_if_as_string Topic.validates_presence_of(:title) - Topic.validates_presence_of(:author_name, if: "title.to_s.match('important')") + ActiveSupport::Deprecation.silence do + Topic.validates_presence_of(:author_name, if: "title.to_s.match('important')") + end t = Topic.new assert t.invalid?, "A topic without a title should not be valid" diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb index 20c11dd852..5ce86738cd 100644 --- a/activemodel/test/cases/validations/with_validation_test.rb +++ b/activemodel/test/cases/validations/with_validation_test.rb @@ -69,26 +69,34 @@ class ValidatesWithTest < ActiveModel::TestCase end test "with if statements that return false" do - Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 2") + ActiveSupport::Deprecation.silence do + Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 2") + end topic = Topic.new assert topic.valid? end test "with if statements that return true" do - Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 1") + ActiveSupport::Deprecation.silence do + Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 1") + end topic = Topic.new assert topic.invalid? assert_includes topic.errors[:base], ERROR_MESSAGE end test "with unless statements that return true" do - Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 1") + ActiveSupport::Deprecation.silence do + Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 1") + end topic = Topic.new assert topic.valid? end test "with unless statements that returns false" do - Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 2") + ActiveSupport::Deprecation.silence do + Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 2") + end topic = Topic.new assert topic.invalid? assert_includes topic.errors[:base], ERROR_MESSAGE @@ -102,7 +110,9 @@ class ValidatesWithTest < ActiveModel::TestCase validator.expect(:is_a?, false, [Symbol]) validator.expect(:is_a?, false, [String]) - Topic.validates_with(validator, if: "1 == 1", foo: :bar) + ActiveSupport::Deprecation.silence do + Topic.validates_with(validator, if: "1 == 1", foo: :bar) + end assert topic.valid? validator.verify end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 743a38b087..b27c03d935 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,12 +1,122 @@ +* All integer-like PKs are autoincrement unless they have an explicit default. + + *Matthew Draper* + +* Omit redundant `using: :btree` for schema dumping. + + *Ryuta Kamizono* + +* Deprecate passing `default` to `index_name_exists?`. + + *Ryuta Kamizono* + +* PostgreSQL: schema dumping support for interval and OID columns. + + *Ryuta Kamizono* + +* Deprecate `supports_primary_key?` on connection adapters since it's + been long unused and unsupported. + + *Ryuta Kamizono* + +* Make `table_name=` reset current statement cache, + so queries are not run against the previous table name. + + *namusyaka* + +* Allow `ActiveRecord::Base#as_json` to be passed a frozen Hash. + + *Isaac Betesh* + +* Fix inspection behavior when the :id column is not primary key. + + *namusyaka* + +* Deprecate locking records with unpersisted changes. + + *Marc Schütz* + +* Remove deprecated behavior that halts callbacks when the return is false. + + *Rafael Mendonça França* + +* Deprecate `ColumnDumper#migration_keys`. + + *Ryuta Kamizono* + +* Fix `association_primary_key_type` for reflections with symbol primary key. + + Fixes #27864. + + *Daniel Colson* + +* Virtual/generated column support for MySQL 5.7.5+ and MariaDB 5.2.0+. + + MySQL generated columns: https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html + MariaDB virtual columns: https://mariadb.com/kb/en/mariadb/virtual-computed-columns/ + + Declare virtual columns with `t.virtual name, type: …, as: "expression"`. + Pass `stored: true` to persist the generated value (false by default). + + Example: + + create_table :generated_columns do |t| + t.string :name + t.virtual :upper_name, type: :string, as: "UPPER(name)" + t.virtual :name_length, type: :integer, as: "LENGTH(name)", stored: true + t.index :name_length # May be indexed, too! + end + + *Ryuta Kamizono* + +* Deprecate `initialize_schema_migrations_table` and `initialize_internal_metadata_table`. + + *Ryuta Kamizono* + +* Support foreign key creation for SQLite3. + + *Ryuta Kamizono* + +* Place generated migrations into the path set by `config.paths["db/migrate"]`. + + *Kevin Glowacz* + +* Raise `ActiveRecord::InvalidForeignKey` when a foreign key constraint fails on SQLite3. + + *Ryuta Kamizono* + +* Add the touch option to `#increment!` and `#decrement!`. + + *Hiroaki Izu* + +* Deprecate passing a class to the `class_name` because it eagerloads more classes than + necessary and potentially creates circular dependencies. + + *Kir Shatrov* + +* Raise error when has_many through is defined before through association. + + Fixes #26834. + + *Chris Holmes* + +* Deprecate passing `name` to `indexes`. + + *Ryuta Kamizono* + +* Remove deprecated tasks: `db:test:clone`, `db:test:clone_schema`, `db:test:clone_structure`. + + *Rafel Mendonça França* + * Compare deserialized values for `PostgreSQL::OID::Hstore` types when - calling `ActiveRecord::Dirty#changed_in_place?` + calling `ActiveRecord::Dirty#changed_in_place?`. Fixes #27502. *Jon Moss* -* Raise `ArgumentError` when passing an `ActiveRecord::Base` instance to `.find` - and `.exists?`. +* Raise `ArgumentError` when passing an `ActiveRecord::Base` instance to `.find`, + `.exists?` and `.update`. *Rafael Mendonça França* @@ -107,7 +217,7 @@ *Rafael Mendonça França* * Set `:time` as a timezone aware type and remove deprecation when - `config.active_record.time_zone_aware_types` is not explictly set. + `config.active_record.time_zone_aware_types` is not explicitly set. *Rafael Mendonça França* @@ -191,7 +301,7 @@ *Sean Griffin* -* Fix that unsigned with zerofill is treated as signed. +* Don't treat unsigned integers with zerofill as signed. Fixes #27125. @@ -397,7 +507,7 @@ *Ryuta Kamizono* -* Sqlite3 migrations to add a column to an existing table can now be +* SQLite3 migrations to add a column to an existing table can now be successfully rolled back when the column was given and invalid column type. diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 250f48fad9..96b8545dfc 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -44,7 +44,6 @@ module ActiveRecord autoload :Explain autoload :Inheritance autoload :Integration - autoload :LegacyYamlAdapter autoload :Migration autoload :Migrator, "active_record/migration" autoload :ModelSchema @@ -85,6 +84,8 @@ module ActiveRecord autoload :AttributeMethods autoload :AutosaveAssociation + autoload :LegacyYamlAdapter + autoload :Relation autoload :AssociationRelation autoload :NullRelation diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 1db5fc0dd1..4606c91ffd 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -97,6 +97,16 @@ module ActiveRecord end end + class HasManyThroughOrderError < ActiveRecordError #:nodoc: + def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil) + if owner_class_name && reflection && through_reflection + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through '#{owner_class_name}##{through_reflection.name}' before the through association is defined.") + else + super("Cannot have a has_many :through association before the through association is defined.") + end + end + end + class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: def initialize(owner = nil, reflection = nil) if owner && reflection @@ -1817,7 +1827,7 @@ module ActiveRecord builder = Builder::HasAndBelongsToMany.new name, self, options - join_model = builder.through_model + join_model = ActiveSupport::Deprecation.silence { builder.through_model } const_set join_model.name, join_model private_constant join_model.name @@ -1846,8 +1856,8 @@ module ActiveRecord hm_options[k] = options[k] if options.key? k end - has_many name, scope, hm_options, &extension - self._reflections[name.to_s].parent_reflection = habtm_reflection + ActiveSupport::Deprecation.silence { has_many name, scope, hm_options, &extension } + _reflections[name.to_s].parent_reflection = habtm_reflection end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 84d0493a60..1cb2b2d7c6 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -83,7 +83,7 @@ module ActiveRecord end def scope - target_scope.merge(association_scope) + target_scope.merge!(association_scope) end # The scope for this association. diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index c6d204d3c2..badde9973f 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -128,9 +128,9 @@ module ActiveRecord reflection = chain_head while reflection table = reflection.alias_name + next_reflection = reflection.next unless reflection == chain_tail - next_reflection = reflection.next foreign_table = next_reflection.alias_name scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) end @@ -138,7 +138,7 @@ module ActiveRecord # Exclude the scope of the association itself, because that # was already merged in the #scope method. reflection.constraints.each do |scope_chain_item| - item = eval_scope(reflection.klass, scope_chain_item, owner) + item = eval_scope(reflection.klass, table, scope_chain_item, owner) if scope_chain_item == refl.scope scope.merge! item.except(:where, :includes) @@ -153,14 +153,15 @@ module ActiveRecord scope.order_values |= item.order_values end - reflection = reflection.next + reflection = next_reflection end scope end - def eval_scope(klass, scope, owner) - klass.unscoped.instance_exec(owner, &scope) + def eval_scope(klass, table, scope, owner) + predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) + ActiveRecord::Relation.create(klass, table, predicate_builder).instance_exec(owner, &scope) end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 0437a79b84..77282e6463 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -30,13 +30,7 @@ module ActiveRecord reload end - if null_scope? - # Cache the proxy separately before the owner has an id - # or else a post-save proxy will still lack the id - @null_proxy ||= CollectionProxy.create(klass, self) - else - @proxy ||= CollectionProxy.create(klass, self) - end + CollectionProxy.create(klass, self) end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -315,9 +309,9 @@ module ActiveRecord record end - def scope(opts = {}) - scope = super() - scope.none! if opts.fetch(:nullify, true) && null_scope? + def scope + scope = super + scope.none! if null_scope? scope end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 0d84805b4d..55bf2e0ff0 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -28,12 +28,9 @@ module ActiveRecord # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. class CollectionProxy < Relation - delegate :exists?, :update_all, :arel, to: :scope - def initialize(klass, association) #:nodoc: @association = association super klass, klass.arel_table, klass.predicate_builder - merge! association.scope(nullify: false) end def target @@ -956,19 +953,10 @@ module ActiveRecord @association end - # We don't want this object to be put on the scoping stack, because - # that could create an infinite loop where we call an @association - # method, which gets the current scope, which is this object, which - # delegates to @association, and so on. - def scoping - @association.scope.scoping { yield } - end - # Returns a <tt>Relation</tt> object for the records in this association def scope - @association.scope + @scope ||= @association.scope end - alias spawn scope # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays # contain the same number of elements and if each element is equal @@ -1100,6 +1088,7 @@ module ActiveRecord # person.pets(true) # fetches pets from the database # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload + @scope = nil proxy_association.reload self end @@ -1121,11 +1110,21 @@ module ActiveRecord # person.pets # fetches pets from the database # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reset + @scope = nil proxy_association.reset proxy_association.reset_scope self end + delegate_methods = [ + QueryMethods, + SpawnMethods, + ].flat_map { |klass| + klass.public_instance_methods(false) + } - self.public_instance_methods(false) + [:scoping] + + delegate(*delegate_methods, to: :scope) + private def find_nth_with_limit(index, limit) @@ -1149,6 +1148,18 @@ module ActiveRecord def exec_queries load_target end + + def respond_to_missing?(method, _) + scope.respond_to?(method) || super + end + + def method_missing(method, *args, &block) + if scope.respond_to?(method) + scope.public_send(method, *args, &block) + else + super + end + end end end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 25613d2fa7..b413eb3f9c 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -100,7 +100,7 @@ module ActiveRecord end def delete_or_nullify_all_records(method) - count = delete_count(method, self.scope) + count = delete_count(method, scope) update_counter(-count) end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index b624154def..8458253ff8 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -85,7 +85,7 @@ module ActiveRecord if target.persisted? && owner.persisted? && !target.save set_owner_attributes(target) - raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + + raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " \ "The record failed to save after its foreign key was set to nil." end end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 604904abcc..1183bdf6f4 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -22,6 +22,10 @@ module ActiveRecord elsif record attributes = construct_join_attributes(record) + if through_record && through_record.destroyed? + through_record = through_proxy.tap(&:reload).target + end + if through_record through_record.update(attributes) elsif owner.new_record? diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 4cd1e64c3d..87e0847ec1 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -32,7 +32,7 @@ module ActiveRecord @alias_cache[node][column] end - class Table < Struct.new(:node, :columns) # :nodoc: + Table = Struct.new(:node, :columns) do # :nodoc: def table Arel::Nodes::TableAlias.new node.table, node.aliased_table_name end @@ -171,7 +171,7 @@ module ActiveRecord chain = child.reflection.chain foreign_table = parent.table foreign_klass = parent.base_klass - child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain) + child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, chain) end def make_outer_joins(parent, child) diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index a5705951f3..f5fcba1236 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -23,14 +23,11 @@ module ActiveRecord JoinInformation = Struct.new :joins, :binds - def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain) + def join_constraints(foreign_table, foreign_klass, node, join_type, tables, chain) joins = [] binds = [] tables = tables.reverse - scope_chain_index = 0 - scope_chain = scope_chain.reverse - # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse chain.reverse_each do |reflection| @@ -44,7 +41,7 @@ module ActiveRecord constraint = build_constraint(klass, table, key, foreign_table, foreign_key) predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) - scope_chain_items = scope_chain[scope_chain_index].map do |item| + scope_chain_items = reflection.scopes.map do |item| if item.is_a?(Relation) item else @@ -52,7 +49,6 @@ module ActiveRecord .instance_exec(node, &item) end end - scope_chain_index += 1 klass_scope = if klass.current_scope diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 1ed1deec55..ebe06566cc 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -394,28 +394,21 @@ module ActiveRecord protected - def clone_attribute_value(reader_method, attribute_name) # :nodoc: - value = send(reader_method, attribute_name) - value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError - value + def attribute_method?(attr_name) # :nodoc: + # We check defined? because Syck calls respond_to? before actually calling initialize. + defined?(@attributes) && @attributes.key?(attr_name) end - def arel_attributes_with_values_for_create(attribute_names) # :nodoc: + private + + def arel_attributes_with_values_for_create(attribute_names) arel_attributes_with_values(attributes_for_create(attribute_names)) end - def arel_attributes_with_values_for_update(attribute_names) # :nodoc: + def arel_attributes_with_values_for_update(attribute_names) arel_attributes_with_values(attributes_for_update(attribute_names)) end - def attribute_method?(attr_name) # :nodoc: - # We check defined? because Syck calls respond_to? before actually calling initialize. - defined?(@attributes) && @attributes.key?(attr_name) - end - - private - # Returns a Hash of the Arel::Attributes and attribute values that have been # typecasted for use in an Arel insert/update method. def arel_attributes_with_values(attribute_names) diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index b0e1391cb9..31c1e687dc 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -275,7 +275,17 @@ module ActiveRecord def attribute_will_change!(attr_name) super - mutations_from_database.force_change(attr_name) + if self.class.has_attribute?(attr_name) + mutations_from_database.force_change(attr_name) + else + ActiveSupport::Deprecation.warn(<<-EOW.squish) + #{attr_name} is not an attribute known to Active Record. + This behavior is deprecated and will be removed in the next + version of Rails. If you'd like #{attr_name} to be managed + by Active Record, add `attribute :#{attr_name} to your class. + EOW + mutations_from_database.deprecated_force_change(attr_name) + end end def _update_record(*) diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 8fcac82a0d..2f32caa257 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -9,7 +9,7 @@ module ActiveRecord # available. def to_key sync_with_transaction_state - key = self.id + key = id [key] if key end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 369a6e35aa..fdc4bf6621 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -54,7 +54,7 @@ module ActiveRecord attr_name.to_s end - name = self.class.primary_key if name == "id".freeze + name = self.class.primary_key if name == "id".freeze && self.class.primary_key _read_attribute(name, &block) end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 945192fe04..4d9aff76cc 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -54,7 +54,7 @@ module ActiveRecord elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } class_name_or_coder else - Coders::YAMLColumn.new(class_name_or_coder) + Coders::YAMLColumn.new(attr_name, class_name_or_coder) end decorate_attribute_type(attr_name, :serialize) do |type| diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index df1231ad47..321d039ed4 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/string/strip" - module ActiveRecord module AttributeMethods module TimeZoneConversion diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb index 3417090830..4de993e169 100644 --- a/activerecord/lib/active_record/attribute_mutation_tracker.rb +++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb @@ -5,6 +5,7 @@ module ActiveRecord def initialize(attributes) @attributes = attributes @forced_changes = Set.new + @deprecated_forced_changes = Set.new end def changed_values @@ -31,7 +32,7 @@ module ActiveRecord end def any_changes? - attr_names.any? { |attr| changed?(attr) } + attr_names.any? { |attr| changed?(attr) } || deprecated_forced_changes.any? end def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) @@ -60,11 +61,15 @@ module ActiveRecord forced_changes << attr_name.to_s end + def deprecated_force_change(attr_name) + deprecated_forced_changes << attr_name.to_s + end + # TODO Change this to private once we've dropped Ruby 2.2 support. # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :attributes, :forced_changes + attr_reader :attributes, :forced_changes, :deprecated_forced_changes private diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 9d0b501862..6bccbc06cd 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -328,9 +328,9 @@ module ActiveRecord def association_valid?(reflection, record, index = nil) return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) - validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) + context = validation_context unless [:create, :update].include?(validation_context) - unless valid = record.valid?(validation_context) + unless valid = record.valid?(context) if reflection.options[:autosave] indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors) diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index be6720ddf3..eb44887e18 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -225,6 +225,55 @@ module ActiveRecord # # This way, the +before_destroy+ gets executed before the <tt>dependent: :destroy</tt> is called, and the data is still available. # + # Also, there are cases when you want several callbacks of the same type to + # be executed in order. + # + # For example: + # + # class Topic + # has_many :children + # + # after_save :log_children + # after_save :do_something_else + # + # private + # + # def log_chidren + # # Child processing + # end + # + # def do_something_else + # # Something else + # end + # end + # + # In this case the +log_children+ gets executed before +do_something_else+. + # The same applies to all non-transactional callbacks. + # + # In case there are multiple transactional callbacks as seen below, the order + # is reversed. + # + # For example: + # + # class Topic + # has_many :children + # + # after_commit :log_children + # after_commit :do_something_else + # + # private + # + # def log_chidren + # # Child processing + # end + # + # def do_something_else + # # Something else + # end + # end + # + # In this case the +do_something_else+ gets executed before +log_children+. + # # == \Transactions # # The entire callback chain of a {#save}[rdoc-ref:Persistence#save], {#save!}[rdoc-ref:Persistence#save!], diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 3a04a10fc9..9c52a31b95 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -5,7 +5,8 @@ module ActiveRecord class YAMLColumn # :nodoc: attr_accessor :object_class - def initialize(object_class = Object) + def initialize(attr_name, object_class = Object) + @attr_name = attr_name @object_class = object_class check_arity_of_constructor end @@ -13,7 +14,7 @@ module ActiveRecord def dump(obj) return if obj.nil? - assert_valid_value(obj) + assert_valid_value(obj, action: "dump") YAML.dump obj end @@ -22,27 +23,25 @@ module ActiveRecord return yaml unless yaml.is_a?(String) && /^---/.match?(yaml) obj = YAML.load(yaml) - assert_valid_value(obj) + assert_valid_value(obj, action: "load") obj ||= object_class.new if object_class != Object obj end - def assert_valid_value(obj) + def assert_valid_value(obj, action:) unless obj.nil? || obj.is_a?(object_class) raise SerializationTypeMismatch, - "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" + "can't #{action} `#{@attr_name}`: was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" end end private def check_arity_of_constructor - begin - load(nil) - rescue ArgumentError - raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." - end + load(nil) + rescue ArgumentError + raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 5ec2fc073e..3f2e86a98d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -353,6 +353,16 @@ module ActiveRecord @threads_blocking_new_connections = 0 @available = ConnectionLeasingQueue.new self + + @lock_thread = false + end + + def lock_thread=(lock_thread) + if lock_thread + @lock_thread = Thread.current + else + @lock_thread = nil + end end # Retrieve the connection associated with the current thread, or call @@ -361,7 +371,7 @@ module ActiveRecord # #connection can be called any number of times; the connection is # held in a cache keyed by a thread. def connection - @thread_cached_conns[connection_cache_key(Thread.current)] ||= checkout + @thread_cached_conns[connection_cache_key(@lock_thread || Thread.current)] ||= checkout end # Returns true if there is an open connection being used for the current thread. @@ -967,7 +977,7 @@ module ActiveRecord end def pool_from_any_process_for(spec_name) - owner_to_pool = @owner_to_pool.values.find { |v| v[spec_name] } + owner_to_pool = @owner_to_pool.values.reverse.find { |v| v[spec_name] } owner_to_pool && owner_to_pool[spec_name] end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 2c352819fb..769f488469 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -51,8 +51,7 @@ module ActiveRecord # Returns a single value from a record def select_value(arel, name = nil, binds = []) - arel, binds = binds_from_relation arel, binds - if result = select_rows(to_sql(arel, binds), name, binds).first + if result = select_rows(arel, name, binds).first result.first end end @@ -60,14 +59,13 @@ module ActiveRecord # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] def select_values(arel, name = nil, binds = []) - arel, binds = binds_from_relation arel, binds - select_rows(to_sql(arel, binds), name, binds).map(&:first) + select_rows(arel, name, binds).map(&:first) end # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. - def select_rows(sql, name = nil, binds = []) - exec_query(sql, name, binds).rows + def select_rows(arel, name = nil, binds = []) + select_all(arel, name, binds).rows end # Executes the SQL statement in the context of this connection and returns diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 7eab7de5d3..e53ba4e666 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -83,7 +83,9 @@ module ActiveRecord # the same SQL query and repeatedly return the same result each time, silently # undermining the randomness you were expecting. def clear_query_cache - @query_cache.clear + @lock.synchronize do + @query_cache.clear + end end def select_all(arel, name = nil, binds = [], preparable: nil) @@ -99,21 +101,23 @@ module ActiveRecord private def cache_sql(sql, name, binds) - result = - if @query_cache[sql].key?(binds) - ActiveSupport::Notifications.instrument( - "sql.active_record", - sql: sql, - binds: binds, - name: name, - connection_id: object_id, - cached: true, - ) - @query_cache[sql][binds] - else - @query_cache[sql][binds] = yield - end - result.dup + @lock.synchronize do + result = + if @query_cache[sql].key?(binds) + ActiveSupport::Notifications.instrument( + "sql.active_record", + sql: sql, + binds: binds, + name: name, + connection_id: object_id, + cached: true, + ) + @query_cache[sql][binds] + else + @query_cache[sql][binds] = yield + end + result.dup + end end # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 0c6bc16e6f..7f4132accf 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -1,4 +1,5 @@ require "active_support/core_ext/big_decimal/conversions" +require "active_support/multibyte/chars" module ActiveRecord module ConnectionAdapters # :nodoc: @@ -140,6 +141,10 @@ module ActiveRecord quoted_date(value).sub(/\A2000-01-01 /, "") end + def quoted_binary(value) # :nodoc: + "'#{quote_string(value.to_s)}'" + end + private def type_casted_binds(binds) @@ -152,7 +157,7 @@ module ActiveRecord def _quote(value) case value - when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data + when String, ActiveSupport::Multibyte::Chars "'#{quote_string(value.to_s)}'" when true then quoted_true when false then quoted_false @@ -160,6 +165,7 @@ module ActiveRecord # BigDecimals need to be put in a non-normalized form and quoted. when BigDecimal then value.to_s("F") when Numeric, ActiveSupport::Duration then value.to_s + when Type::Binary::Data then quoted_binary(value) when Type::Time::Value then "'#{quoted_time(value)}'" when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 322684672f..c48a4acff8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -15,9 +15,9 @@ module ActiveRecord end delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options, to: :@conn + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options, to: :@conn private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options private @@ -29,7 +29,7 @@ module ActiveRecord end def visit_ColumnDefinition(o) - o.sql_type ||= type_to_sql(o.type, o.limit, o.precision, o.scale) + o.sql_type = type_to_sql(o.type, o.options) column_sql = "#{quote_column_name(o.name)} #{o.sql_type}" add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql @@ -49,7 +49,7 @@ module ActiveRecord statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) }) end - if supports_foreign_keys? + if supports_foreign_keys_in_create? statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) }) end @@ -96,17 +96,7 @@ module ActiveRecord end def column_options(o) - column_options = {} - column_options[:null] = o.null unless o.null.nil? - column_options[:default] = o.default unless o.default.nil? - column_options[:column] = o - column_options[:first] = o.first - column_options[:after] = o.after - column_options[:auto_increment] = o.auto_increment - column_options[:primary_key] = o.primary_key - column_options[:collation] = o.collation - column_options[:comment] = o.comment - column_options + o.options.merge(column: o) end def add_column_options!(sql, options) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 9b324c090b..5eb7787226 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -3,29 +3,37 @@ module ActiveRecord # Abstract representation of an index definition on a table. Instances of # this type are typically created and returned by methods in database # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc: - end + IndexDefinition = Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc: # Abstract representation of a column definition. Instances of this type # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment) #:nodoc: + ColumnDefinition = Struct.new(:name, :type, :options, :sql_type) do # :nodoc: def primary_key? - primary_key || type.to_sym == :primary_key + options[:primary_key] end - end - class AddColumnDefinition < Struct.new(:column) # :nodoc: - end + [:limit, :precision, :scale, :default, :null, :collation, :comment].each do |option_name| + module_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{option_name} + options[:#{option_name}] + end - class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc: + def #{option_name}=(value) + options[:#{option_name}] = value + end + CODE + end end - class PrimaryKeyDefinition < Struct.new(:name) # :nodoc: - end + AddColumnDefinition = Struct.new(:column) # :nodoc: + + ChangeColumnDefinition = Struct.new(:column, :name) #:nodoc: - class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: + PrimaryKeyDefinition = Struct.new(:name) # :nodoc: + + ForeignKeyDefinition = Struct.new(:from_table, :to_table, :options) do #:nodoc: def name options[:name] end @@ -177,6 +185,7 @@ module ActiveRecord :text, :time, :timestamp, + :virtual, ].each do |column_type| module_eval <<-CODE, __FILE__, __LINE__ + 1 def #{column_type}(*args, **options) @@ -357,33 +366,22 @@ module ActiveRecord # # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. def references(*args, **options) - args.each do |col| - ReferenceDefinition.new(col, **options).add_to(self) + args.each do |ref_name| + ReferenceDefinition.new(ref_name, options).add_to(self) end end alias :belongs_to :references - def new_column_definition(name, type, options) # :nodoc: + def new_column_definition(name, type, **options) # :nodoc: type = aliased_types(type.to_s, type) - column = create_column_definition name, type - - column.limit = options[:limit] - column.precision = options[:precision] - column.scale = options[:scale] - column.default = options[:default] - column.null = options[:null] - column.first = options[:first] - column.after = options[:after] - column.auto_increment = options[:auto_increment] - column.primary_key = type == :primary_key || options[:primary_key] - column.collation = options[:collation] - column.comment = options[:comment] - column + options[:primary_key] ||= type == :primary_key + options[:null] = false if options[:primary_key] + create_column_definition(name, type, options) end private - def create_column_definition(name, type) - ColumnDefinition.new name, type + def create_column_definition(name, type, options) + ColumnDefinition.new(name, type, options) end def aliased_types(name, fallback) @@ -591,8 +589,7 @@ module ActiveRecord # t.belongs_to(:supplier, foreign_key: true) # # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. - def references(*args) - options = args.extract_options! + def references(*args, **options) args.each do |ref_name| @base.add_reference(name, ref_name, options) end @@ -605,8 +602,7 @@ module ActiveRecord # t.remove_belongs_to(:supplier, polymorphic: true) # # See {connection.remove_reference}[rdoc-ref:SchemaStatements#remove_reference] - def remove_references(*args) - options = args.extract_options! + def remove_references(*args, **options) args.each do |ref_name| @base.remove_reference(name, ref_name, options) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index b912d24626..34036d8a1d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -7,13 +7,15 @@ module ActiveRecord # Adapter level by over-writing this code inside the database specific adapters module ColumnDumper def column_spec(column) - [schema_type(column), prepare_column_options(column)] + [schema_type_with_virtual(column), prepare_column_options(column)] end def column_spec_for_primary_key(column) return {} if default_primary_key?(column) spec = { id: schema_type(column).inspect } spec.merge!(prepare_column_options(column).except!(:null)) + spec[:default] ||= "nil" if explicit_primary_key_default?(column) + spec end # This can be overridden on an Adapter level basis to support other @@ -49,9 +51,10 @@ module ActiveRecord end # Lists the valid migration options - def migration_keys - [:limit, :precision, :scale, :default, :null, :collation, :comment] + def migration_keys # :nodoc: + column_options_keys end + deprecate :migration_keys private @@ -59,6 +62,18 @@ module ActiveRecord schema_type(column) == :bigint end + def explicit_primary_key_default?(column) + false + end + + def schema_type_with_virtual(column) + if supports_virtual_columns? && column.virtual? + :virtual + else + schema_type(column) + end + end + def schema_type(column) if column.bigint? :bigint 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 e5c0e1690c..3686ad8b54 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -69,7 +69,9 @@ module ActiveRecord end # Returns an array of indexes for the given table. - # def indexes(table_name, name = nil) end + def indexes(table_name, name = nil) + raise NotImplementedError, "#indexes is not implemented" + end # Checks to see if an index exists on a table for a given index definition. # @@ -120,7 +122,7 @@ module ActiveRecord checks = [] checks << lambda { |c| c.name == column_name } checks << lambda { |c| c.type == type } if type - migration_keys.each do |attr| + column_options_keys.each do |attr| checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr) end @@ -271,8 +273,8 @@ module ActiveRecord yield td if block_given? - if options[:force] && data_source_exists?(table_name) - drop_table(table_name, options) + if options[:force] + drop_table(table_name, **options, if_exists: true) end result = execute schema_creation.accept td @@ -766,16 +768,17 @@ module ActiveRecord raise ArgumentError, "You must specify the index name" end else - index_name(table_name, column: options) + index_name(table_name, index_name_options(options)) end end # Verifies the existence of an index with a given name. - # - # The default argument is returned if the underlying implementation does not define the indexes method, - # as there's no way to determine the correct answer in that case. - def index_name_exists?(table_name, index_name, default) - return default unless respond_to?(:indexes) + def index_name_exists?(table_name, index_name, default = nil) + unless default.nil? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing default to #index_name_exists? is deprecated without replacement. + MSG + end index_name = index_name.to_s indexes(table_name).detect { |i| i.name == index_name } end @@ -826,8 +829,8 @@ module ActiveRecord # # add_reference(:products, :supplier, foreign_key: {to_table: :firms}) # - def add_reference(table_name, *args) - ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self)) + def add_reference(table_name, ref_name, **options) + ReferenceDefinition.new(ref_name, options).add_to(update_table_definition(table_name, self)) end alias :add_belongs_to :add_reference @@ -1006,15 +1009,15 @@ module ActiveRecord end end - # Should not be called normally, but this operation is non-destructive. - # The migrations module handles this automatically. - def initialize_schema_migrations_table + def initialize_schema_migrations_table # :nodoc: ActiveRecord::SchemaMigration.create_table end + deprecate :initialize_schema_migrations_table - def initialize_internal_metadata_table + def initialize_internal_metadata_table # :nodoc: ActiveRecord::InternalMetadata.create_table end + deprecate :initialize_internal_metadata_table def internal_string_options_for_primary_key # :nodoc: { primary_key: true } @@ -1050,7 +1053,7 @@ module ActiveRecord end end - def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: + def type_to_sql(type, limit: nil, precision: nil, scale: nil, **) # :nodoc: type = type.to_sym if type if native = native_database_types[type] column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup @@ -1068,7 +1071,7 @@ module ActiveRecord raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified" end - elsif [:datetime, :time].include?(type) && precision ||= native[:precision] + elsif [:datetime, :time, :interval].include?(type) && precision ||= native[:precision] if (0..6) === precision column_type_sql << "(#{precision})" else @@ -1120,18 +1123,14 @@ module ActiveRecord end def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc: - if column_name.is_a?(String) && /\W/.match?(column_name) - column_names = column_name - else - column_names = Array(column_name) - end + column_names = index_column_names(column_name) options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) index_type = options[:type].to_s if options.key?(:type) index_type ||= options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) - index_name ||= index_name(table_name, index_name_options(column_names)) + index_name ||= index_name(table_name, column_names) if options.key?(:algorithm) algorithm = index_algorithms.fetch(options[:algorithm]) { @@ -1147,7 +1146,7 @@ module ActiveRecord validate_index_length!(table_name, index_name, options.fetch(:internal, false)) - if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false) + if data_source_exists?(table_name) && index_name_exists?(table_name, index_name) raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" end index_columns = quoted_columns_for_index(column_names, options).join(", ") @@ -1170,6 +1169,9 @@ module ActiveRecord end private + def column_options_keys + [:limit, :precision, :scale, :default, :null, :collation, :comment] + end def add_index_sort_order(quoted_columns, **options) if order = options[:order] @@ -1208,13 +1210,13 @@ module ActiveRecord if options.is_a?(Hash) checks << lambda { |i| i.name == options[:name].to_s } if options.key?(:name) - column_names = Array(options[:column]).map(&:to_s) + column_names = index_column_names(options[:column]) else - column_names = Array(options).map(&:to_s) + column_names = index_column_names(options) end - if column_names.any? - checks << lambda { |i| i.columns.join("_and_") == column_names.join("_and_") } + if column_names.present? + checks << lambda { |i| index_name(table_name, i.columns) == index_name(table_name, column_names) } end raise ArgumentError, "No name or columns specified" if checks.none? @@ -1261,8 +1263,16 @@ module ActiveRecord AlterTable.new create_table_definition(name) end + def index_column_names(column_names) + if column_names.is_a?(String) && /\W/.match?(column_names) + column_names + else + Array(column_names) + end + end + def index_name_options(column_names) - if column_names.is_a?(String) + if column_names.is_a?(String) && /\W/.match?(column_names) column_names = column_names.scan(/\w+/).join("_") end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 4046b3829d..b31ce0a181 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -107,6 +107,7 @@ module ActiveRecord @schema_cache = SchemaCache.new self @quoted_column_names, @quoted_table_names = {}, {} @visitor = arel_visitor + @lock = Monitor.new if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true @@ -176,7 +177,7 @@ module ActiveRecord if @owner == Thread.current msg << "it is already leased by the current thread." else - msg << "it is already in use by a different thread: #{@owner}. " << + msg << "it is already in use by a different thread: #{@owner}. " \ "Current thread: #{Thread.current}." end raise ActiveRecordError, msg @@ -194,8 +195,8 @@ module ActiveRecord def expire if in_use? if @owner != Thread.current - raise ActiveRecordError, "Cannot expire connection, " << - "it is owned by a different thread: #{@owner}. " << + raise ActiveRecordError, "Cannot expire connection, " \ + "it is owned by a different thread: #{@owner}. " \ "Current thread: #{Thread.current}." end @@ -236,11 +237,10 @@ module ActiveRecord false end - # Can this adapter determine the primary key for tables not attached - # to an Active Record class, such as join tables? - def supports_primary_key? - false + def supports_primary_key? # :nodoc: + true end + deprecate :supports_primary_key? # Does this adapter support DDL rollbacks in transactions? That is, would # CREATE TABLE or ALTER TABLE get rolled back by a transaction? @@ -310,6 +310,12 @@ module ActiveRecord false end + # Does this adapter support creating foreign key constraints + # in the same statement as creating the table? + def supports_foreign_keys_in_create? + supports_foreign_keys? + end + # Does this adapter support views? def supports_views? false @@ -340,6 +346,11 @@ module ActiveRecord true end + # Does this adapter support virtual columns? + def supports_virtual_columns? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -441,15 +452,15 @@ module ActiveRecord @connection end - def case_sensitive_comparison(table, attribute, column, value) - table[attribute].eq(Arel::Nodes::BindParam.new) + def case_sensitive_comparison(table, attribute, column, value) # :nodoc: + table[attribute].eq(value) end - def case_insensitive_comparison(table, attribute, column, value) + def case_insensitive_comparison(table, attribute, column, value) # :nodoc: if can_perform_case_insensitive_comparison_for?(column) - table[attribute].lower.eq(table.lower(Arel::Nodes::BindParam.new)) + table[attribute].lower.eq(table.lower(value)) else - table[attribute].eq(Arel::Nodes::BindParam.new) + table[attribute].eq(value) end end @@ -499,6 +510,10 @@ module ActiveRecord result end + def default_index_type?(index) # :nodoc: + index.using.nil? + end + private def initialize_type_map(m) @@ -591,7 +606,11 @@ module ActiveRecord binds: binds, type_casted_binds: type_casted_binds, statement_name: statement_name, - connection_id: object_id) { yield } + connection_id: object_id) do + @lock.synchronize do + yield + end + end rescue => e raise translate_exception_class(e, sql) end 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 01cd1e5446..14269b4570 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -93,10 +93,6 @@ module ActiveRecord true end - def supports_primary_key? - true - end - def supports_bulk_alter? #:nodoc: true end @@ -141,6 +137,14 @@ module ActiveRecord end end + def supports_virtual_columns? + if mariadb? + version >= "5.2.0" + else + version >= "5.7.5" + end + end + def supports_advisory_locks? true end @@ -367,6 +371,12 @@ module ActiveRecord # Returns an array of indexes for the given table. def indexes(table_name, name = nil) #:nodoc: + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + indexes = [] current_index = nil execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| @@ -521,6 +531,7 @@ module ActiveRecord WHERE fk.referenced_column_name IS NOT NULL AND fk.table_schema = #{quote(schema)} AND fk.table_name = #{quote(name)} + AND rc.table_name = #{quote(name)} SQL fk_info.map do |row| @@ -559,7 +570,7 @@ module ActiveRecord end # Maps logical Rails types to MySQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil) + def type_to_sql(type, limit: nil, precision: nil, scale: nil, unsigned: nil, **) # :nodoc: sql = \ case type.to_s when "integer" @@ -575,7 +586,7 @@ module ActiveRecord binary_to_sql(limit) end else - super(type, limit, precision, scale) + super end sql << " unsigned" if unsigned && type != :primary_key @@ -604,9 +615,9 @@ module ActiveRecord SQL end - def case_sensitive_comparison(table, attribute, column, value) + def case_sensitive_comparison(table, attribute, column, value) # :nodoc: if column.collation && !column.case_sensitive? - table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new)) + table[attribute].eq(Arel::Nodes::Bin.new(value)) else super end @@ -640,6 +651,10 @@ module ActiveRecord !native_database_types[type].nil? end + def default_index_type?(index) # :nodoc: + index.using == :btree || super + end + private def initialize_type_map(m) diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index dcf56997db..3e4ea28f63 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -149,9 +149,18 @@ module ActiveRecord # Expands each key in @configurations hash into fully resolved hash def resolve_all config = configurations.dup + + if env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url")) + end + + config.reject! { |k, v| v.is_a?(Hash) && !(v.key?("adapter") || v.key?("url")) } + config.merge! env_config if env_config + config.each do |key, value| config[key] = resolve(value) if value end + config end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb index 1499c1681f..c9ad47c035 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb @@ -15,6 +15,10 @@ module ActiveRecord def auto_increment? extra == "auto_increment" end + + def virtual? + /\b(?:VIRTUAL|STORED|PERSISTENT)\b/.match?(extra) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index 78e7181266..8c67a7a80b 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -3,7 +3,7 @@ module ActiveRecord module MySQL module DatabaseStatements # Returns an ActiveRecord::Result instance. - def select_all(arel, name = nil, binds = [], preparable: nil) + def select_all(arel, name = nil, binds = [], preparable: nil) # :nodoc: result = if ExplainRegistry.collect? && prepared_statements unprepared_statement { super } else @@ -15,8 +15,8 @@ module ActiveRecord # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. - def select_rows(sql, name = nil, binds = []) - select_result(sql, name, binds) do |result| + def select_rows(arel, name = nil, binds = []) # :nodoc: + select_result(arel, name, binds) do |result| @connection.next_result while @connection.more_results? result.to_a end @@ -58,7 +58,9 @@ module ActiveRecord @connection.last_id end - def select_result(sql, name = nil, binds = []) + def select_result(arel, name, binds) + arel, binds = binds_from_relation(arel, binds) + sql = to_sql(arel, binds) if without_prepared_statement?(binds) execute_and_free(sql, name) { |result| yield result } else diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb index 9d11ad28d4..d4f5906b33 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb @@ -36,15 +36,9 @@ module ActiveRecord end end - private - - def _quote(value) - if value.is_a?(Type::Binary::Data) - "x'#{value.hex}'" - else - super - end - end + def quoted_binary(value) + "x'#{value.hex}'" + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb index d808b50332..e8358271ab 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -1,9 +1,9 @@ module ActiveRecord module ConnectionAdapters module MySQL - class SchemaCreation < AbstractAdapter::SchemaCreation - delegate :add_sql_comment!, to: :@conn - private :add_sql_comment! + class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: + delegate :add_sql_comment!, :mariadb?, to: :@conn + private :add_sql_comment!, :mariadb? private @@ -11,11 +11,6 @@ module ActiveRecord "DROP FOREIGN KEY #{name}" end - def visit_ColumnDefinition(o) - o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned) - super - end - def visit_AddColumnDefinition(o) add_column_position!(super, column_options(o.column)) end @@ -29,12 +24,6 @@ module ActiveRecord add_sql_comment!(super, options[:comment]) end - def column_options(o) - column_options = super - column_options[:charset] = o.charset - column_options - end - def add_column_options!(sql, options) if charset = options[:charset] sql << " CHARACTER SET #{charset}" @@ -44,6 +33,13 @@ module ActiveRecord sql << " COLLATE #{collation}" end + if as = options[:as] + sql << " AS (#{as})" + if options[:stored] + sql << (mariadb? ? " PERSISTENT" : " STORED") + end + end + add_sql_comment!(super, options[:comment]) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb index 0cf40de70f..773bbcef4e 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -3,10 +3,7 @@ module ActiveRecord module MySQL module ColumnMethods def primary_key(name, type = :primary_key, **options) - if type == :primary_key && !options.key?(:default) - options[:auto_increment] = true - options[:limit] = 8 - end + options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default) super end @@ -59,33 +56,25 @@ module ActiveRecord end end - class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :charset, :unsigned - end - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods - def new_column_definition(name, type, options) # :nodoc: - column = super - case column.type + def new_column_definition(name, type, **options) # :nodoc: + case type + when :virtual + type = options[:type] when :primary_key - column.type = :integer - column.auto_increment = true + type = :integer + options[:limit] ||= 8 + options[:auto_increment] = true + options[:primary_key] = true when /\Aunsigned_(?<type>.+)\z/ - column.type = $~[:type].to_sym - column.unsigned = true + type = $~[:type].to_sym + options[:unsigned] = true end - column.unsigned ||= options[:unsigned] - column.charset = options[:charset] - column - end - - private - def create_column_definition(name, type) - MySQL::ColumnDefinition.new(name, type) - end + super + end end class Table < ActiveRecord::ConnectionAdapters::Table diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index 2065816501..ad4a069d73 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -1,19 +1,17 @@ module ActiveRecord module ConnectionAdapters module MySQL - module ColumnDumper - def column_spec_for_primary_key(column) - spec = super - if column.type == :integer && !column.auto_increment? - spec[:default] = schema_default(column) || "nil" - end - spec[:unsigned] = "true" if column.unsigned? - spec - end - + module ColumnDumper # :nodoc: def prepare_column_options(column) spec = super spec[:unsigned] = "true" if column.unsigned? + + if supports_virtual_columns? && column.virtual? + spec[:as] = extract_expression_for_virtual_column(column) + spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra) + spec = { type: schema_type(column).inspect }.merge!(spec) + end + spec end @@ -24,7 +22,11 @@ module ActiveRecord private def default_primary_key?(column) - super && column.auto_increment? + super && column.auto_increment? && !column.unsigned? + end + + def explicit_primary_key_default?(column) + column.type == :integer && !column.auto_increment? end def schema_type(column) @@ -46,6 +48,21 @@ module ActiveRecord column.collation.inspect if column.collation != @table_collation_cache[table_name] end end + + def extract_expression_for_virtual_column(column) + if mariadb? + create_table_info = create_table_info(column.table_name) + if %r/#{quote_column_name(column.name)} #{Regexp.quote(column.sql_type)} AS \((?<expression>.+?)\) #{column.extra}/m =~ create_table_info + $~[:expression].inspect + end + else + sql = "SELECT generation_expression FROM information_schema.columns" \ + " WHERE table_schema = #{quote(@config[:database])}" \ + " AND table_name = #{quote(column.table_name)}" \ + " AND column_name = #{quote(column.name)}" + select_value(sql, "SCHEMA").inspect + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 9a2017b443..705e6063dc 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -7,18 +7,14 @@ module ActiveRecord PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", binds)) end - def select_value(arel, name = nil, binds = []) - arel, binds = binds_from_relation arel, binds - sql = to_sql(arel, binds) - execute_and_clear(sql, name, binds) do |result| + def select_value(arel, name = nil, binds = []) # :nodoc: + select_result(arel, name, binds) do |result| result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0 end end - def select_values(arel, name = nil, binds = []) - arel, binds = binds_from_relation arel, binds - sql = to_sql(arel, binds) - execute_and_clear(sql, name, binds) do |result| + def select_values(arel, name = nil, binds = []) # :nodoc: + select_result(arel, name, binds) do |result| if result.nfields > 0 result.column_values(0) else @@ -29,8 +25,8 @@ module ActiveRecord # Executes a SELECT query and returns an array of rows. Each row is an # array of field values. - def select_rows(sql, name = nil, binds = []) - execute_and_clear(sql, name, binds) do |result| + def select_rows(arel, name = nil, binds = []) # :nodoc: + select_result(arel, name, binds) do |result| result.values end end @@ -179,6 +175,14 @@ module ActiveRecord def suppress_composite_primary_key(pk) pk unless pk.is_a?(Array) end + + def select_result(arel, name, binds) + arel, binds = binds_from_relation(arel, binds) + sql = to_sql(arel, binds) + execute_and_clear(sql, name, binds) do |result| + yield result + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 0e526f6201..4098250f3e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -11,6 +11,7 @@ require "active_record/connection_adapters/postgresql/oid/inet" require "active_record/connection_adapters/postgresql/oid/json" require "active_record/connection_adapters/postgresql/oid/jsonb" require "active_record/connection_adapters/postgresql/oid/money" +require "active_record/connection_adapters/postgresql/oid/oid" require "active_record/connection_adapters/postgresql/oid/point" require "active_record/connection_adapters/postgresql/oid/legacy_point" require "active_record/connection_adapters/postgresql/oid/range" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb new file mode 100644 index 0000000000..9c2ac08b30 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Oid < Type::Integer # :nodoc: + def type + :oid + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index 2c714f4018..54d5d0902e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/string/filters" - module ActiveRecord module ConnectionAdapters module PostgreSQL diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb index 2d2fede4e8..564e82a4ac 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -5,8 +5,9 @@ module ActiveRecord class SpecializedString < Type::String # :nodoc: attr_reader :type - def initialize(type) + def initialize(type, **options) @type = type + super(options) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 3783925954..6663448a99 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -55,6 +55,10 @@ module ActiveRecord end end + def quoted_binary(value) # :nodoc: + "'#{escape_bytea(value.to_s)}'" + end + def quote_default_expression(value, column) # :nodoc: if value.is_a?(Proc) value.call @@ -76,8 +80,6 @@ module ActiveRecord def _quote(value) case value - when Type::Binary::Data - "'#{escape_bytea(value.to_s)}'" when OID::Xml::Data "xml '#{quote_string(value.to_s)}'" when OID::Bit::Data diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb new file mode 100644 index 0000000000..e1d5089115 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: + private + def add_column_options!(sql, options) + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + super + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 4afb4733eb..11ea1e5144 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -42,6 +42,7 @@ module ActiveRecord # a record (as primary keys cannot be +nil+). This might be done via the # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. def primary_key(name, type = :primary_key, **options) + options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default) if type == :uuid options[:default] = options.fetch(:default, "gen_random_uuid()") elsif options.delete(:auto_increment) == true && %i(integer bigint).include?(type) @@ -87,6 +88,10 @@ module ActiveRecord args.each { |name| column(name, :inet, options) } end + def interval(*args, **options) + args.each { |name| column(name, :interval, options) } + end + def int4range(*args, **options) args.each { |name| column(name, :int4range, options) } end @@ -119,6 +124,10 @@ module ActiveRecord args.each { |name| column(name, :numrange, options) } end + def oid(*args, **options) + args.each { |name| column(name, :oid, options) } + end + def point(*args, **options) args.each { |name| column(name, :point, options) } end @@ -172,24 +181,8 @@ module ActiveRecord end end - class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :array - end - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods - - def new_column_definition(name, type, options) # :nodoc: - column = super - column.array = options[:array] - column - end - - private - - def create_column_definition(name, type) - PostgreSQL::ColumnDefinition.new name, type - end end class Table < ActiveRecord::ConnectionAdapters::Table diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb index 7808d37deb..5555a46b6b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -1,15 +1,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL - module ColumnDumper - def column_spec_for_primary_key(column) - spec = super - if schema_type(column) == :uuid - spec[:default] ||= "nil" - end - spec - end - + module ColumnDumper # :nodoc: # Adds +:array+ option to the default set def prepare_column_options(column) spec = super @@ -28,6 +20,10 @@ module ActiveRecord schema_type(column) == :bigserial end + def explicit_primary_key_default?(column) + column.type == :uuid || (column.type == :integer && !column.serial?) + end + def schema_type(column) return super unless column.serial? 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 85836fc575..a61d920a73 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -3,22 +3,6 @@ require "active_support/core_ext/string/strip" module ActiveRecord module ConnectionAdapters module PostgreSQL - class SchemaCreation < AbstractAdapter::SchemaCreation - private - - def visit_ColumnDefinition(o) - o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array) - super - end - - def add_column_options!(sql, options) - if options[:collation] - sql << " COLLATE \"#{options[:collation]}\"" - end - super - end - end - module SchemaStatements # Drops the database specified on the +name+ attribute # and creates it again using the provided +options+. @@ -148,7 +132,12 @@ module ActiveRecord end # Verifies existence of an index with a given name. - def index_name_exists?(table_name, index_name, default) + def index_name_exists?(table_name, index_name, default = nil) + unless default.nil? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing default to #index_name_exists? is deprecated without replacement. + MSG + end table = Utils.extract_schema_qualified_name(table_name.to_s) index = Utils.extract_schema_qualified_name(index_name.to_s) @@ -166,7 +155,13 @@ module ActiveRecord end # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) + def indexes(table_name, name = nil) # :nodoc: + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + table = Utils.extract_schema_qualified_name(table_name.to_s) result = query(<<-SQL, "SCHEMA") @@ -435,17 +430,18 @@ module ActiveRecord end def primary_keys(table_name) # :nodoc: + name = Utils.extract_schema_qualified_name(table_name.to_s) select_values(<<-SQL.strip_heredoc, "SCHEMA") - WITH pk_constraint AS ( - SELECT conrelid, unnest(conkey) AS connum FROM pg_constraint - WHERE contype = 'p' - AND conrelid = #{quote(quote_table_name(table_name))}::regclass - ), cons AS ( - SELECT conrelid, connum, row_number() OVER() AS rownum FROM pk_constraint - ) - SELECT attr.attname FROM pg_attribute attr - INNER JOIN cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.connum - ORDER BY cons.rownum + SELECT column_name + FROM information_schema.key_column_usage kcu + JOIN information_schema.table_constraints tc + ON kcu.table_name = tc.table_name + AND kcu.table_schema = tc.table_schema + AND kcu.constraint_name = tc.constraint_name + WHERE constraint_type = 'PRIMARY KEY' + AND kcu.table_name = #{quote(name.identifier)} + AND kcu.table_schema = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} + ORDER BY kcu.ordinal_position SQL end @@ -480,7 +476,7 @@ module ActiveRecord clear_cache! quoted_table_name = quote_table_name(table_name) quoted_column_name = quote_column_name(column_name) - sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale], options[:array]) + sql_type = type_to_sql(type, options) sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}" if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" @@ -488,7 +484,7 @@ module ActiveRecord if options[:using] sql << " USING #{options[:using]}" elsif options[:cast_as] - cast_as_type = type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale], options[:array]) + cast_as_type = type_to_sql(options[:cast_as], options) sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" end execute sql @@ -624,7 +620,7 @@ module ActiveRecord end # Maps logical Rails types to PostgreSQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil, array = nil) + def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc: sql = \ case type.to_s when "binary" @@ -649,7 +645,7 @@ module ActiveRecord else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead.") end else - super(type, limit, precision, scale) + super end sql << "[]" if array && type != :primary_key diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 0ebd907cc0..c89e29ba44 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -9,6 +9,7 @@ require "active_record/connection_adapters/postgresql/explain_pretty_printer" require "active_record/connection_adapters/postgresql/oid" require "active_record/connection_adapters/postgresql/quoting" require "active_record/connection_adapters/postgresql/referential_integrity" +require "active_record/connection_adapters/postgresql/schema_creation" require "active_record/connection_adapters/postgresql/schema_definitions" require "active_record/connection_adapters/postgresql/schema_dumper" require "active_record/connection_adapters/postgresql/schema_statements" @@ -108,6 +109,8 @@ module ActiveRecord bit: { name: "bit" }, bit_varying: { name: "bit varying" }, money: { name: "money" }, + interval: { name: "interval" }, + oid: { name: "oid" }, } OID = PostgreSQL::OID #:nodoc: @@ -233,7 +236,9 @@ module ActiveRecord # Clears the prepared statements cache. def clear_cache! - @statements.clear + @lock.synchronize do + @statements.clear + end end def truncate(table_name, name = nil) @@ -281,11 +286,6 @@ module ActiveRecord true end - # Does PostgreSQL support finding primary key on non-Active Record tables? - def supports_primary_key? #:nodoc: - true - end - def set_standard_conforming_strings execute("SET standard_conforming_strings = on", "SCHEMA") end @@ -404,6 +404,10 @@ module ActiveRecord @connection.server_version end + def default_index_type?(index) # :nodoc: + index.using == :btree || super + end + private # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html @@ -438,7 +442,7 @@ module ActiveRecord end end - def get_oid_type(oid, fmod, column_name, sql_type = "") + def get_oid_type(oid, fmod, column_name, sql_type = "".freeze) if !type_map.key?(oid) load_additional_types(type_map, [oid]) end @@ -455,7 +459,7 @@ module ActiveRecord register_class_with_limit m, "int2", Type::Integer register_class_with_limit m, "int4", Type::Integer register_class_with_limit m, "int8", Type::Integer - m.alias_type "oid", "int2" + m.register_type "oid", OID::Oid.new m.register_type "float4", Type::Float.new m.alias_type "float8", "float4" m.register_type "text", Type::Text.new @@ -490,8 +494,10 @@ module ActiveRecord m.register_type "polygon", OID::SpecializedString.new(:polygon) m.register_type "circle", OID::SpecializedString.new(:circle) - # FIXME: why are we keeping these types as strings? - m.alias_type "interval", "varchar" + m.register_type "interval" do |_, _, sql_type| + precision = extract_precision(sql_type) + OID::SpecializedString.new(:interval, precision: precision) + end register_class_with_precision m, "time", Type::Time register_class_with_precision m, "timestamp", OID::DateTime @@ -633,8 +639,10 @@ module ActiveRecord if in_transaction? raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message) else - # outside of transactions we can simply flush this query and retry - @statements.delete sql_key(sql) + @lock.synchronize do + # outside of transactions we can simply flush this query and retry + @statements.delete sql_key(sql) + end retry end end @@ -670,19 +678,21 @@ module ActiveRecord # Prepare the statement if it hasn't been prepared, return # the statement key. def prepare_statement(sql) - sql_key = sql_key(sql) - unless @statements.key? sql_key - nextkey = @statements.next_key - begin - @connection.prepare nextkey, sql - rescue => e - raise translate_exception_class(e, sql) + @lock.synchronize do + sql_key = sql_key(sql) + unless @statements.key? sql_key + nextkey = @statements.next_key + begin + @connection.prepare nextkey, sql + rescue => e + raise translate_exception_class(e, sql) + end + # Clear the queue + @connection.get_last_result + @statements[sql_key] = nextkey end - # Clear the queue - @connection.get_last_result - @statements[sql_key] = nextkey + @statements[sql_key] end - @statements[sql_key] end # Connects to a PostgreSQL server and sets up the adapter depending on the @@ -759,11 +769,11 @@ module ActiveRecord query(<<-end_sql, "SCHEMA") SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, - (SELECT c.collname FROM pg_collation c, pg_type t - WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation), - col_description(a.attrelid, a.attnum) AS comment - FROM pg_attribute a LEFT JOIN pg_attrdef d - ON a.attrelid = d.adrelid AND a.attnum = d.adnum + c.collname, col_description(a.attrelid, a.attnum) AS comment + FROM pg_attribute a + LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum + LEFT JOIN pg_type t ON a.atttypid = t.oid + LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index f01ed67b0f..7276a65098 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -18,15 +18,11 @@ module ActiveRecord quoted_date(value) end - private + def quoted_binary(value) + "x'#{value.hex}'" + end - def _quote(value) - if value.is_a?(Type::Binary::Data) - "x'#{value.hex}'" - else - super - end - end + private def _type_cast(value) case value diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb index 70c0d28830..bc798d1dbb 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb @@ -1,15 +1,8 @@ module ActiveRecord module ConnectionAdapters module SQLite3 - class SchemaCreation < AbstractAdapter::SchemaCreation + class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: private - - def column_options(o) - options = super - options[:null] = false if o.primary_key - options - end - def add_column_options!(sql, options) if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb index d0b38dff4c..e157e4b218 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb @@ -3,7 +3,7 @@ module ActiveRecord module SQLite3 module ColumnMethods def primary_key(name, type = :primary_key, **options) - if options.delete(:auto_increment) == true && %i(integer bigint).include?(type) + if %i(integer bigint).include?(type) && (options.delete(:auto_increment) == true || !options.key?(:default)) type = :primary_key end @@ -13,6 +13,11 @@ module ActiveRecord class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods + + def references(*args, **options) + super(*args, type: :integer, **options) + end + alias :belongs_to :references end class Table < ActiveRecord::ConnectionAdapters::Table diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb index c027fef83c..eec018eda3 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb @@ -1,12 +1,16 @@ module ActiveRecord module ConnectionAdapters module SQLite3 - module ColumnDumper + module ColumnDumper # :nodoc: private def default_primary_key?(column) schema_type(column) == :integer end + + def explicit_primary_key_default?(column) + column.bigint? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 297e2997a9..16ef195bfc 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -95,6 +95,8 @@ module ActiveRecord @active = nil @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) + + configure_connection end def supports_ddl_transactions? @@ -120,12 +122,12 @@ module ActiveRecord true end - def supports_primary_key? #:nodoc: + def requires_reloading? true end - def requires_reloading? - true + def supports_foreign_keys_in_create? + sqlite_version >= "3.6.19" end def supports_views? @@ -185,6 +187,19 @@ module ActiveRecord true end + # REFERENTIAL INTEGRITY ==================================== + + def disable_referential_integrity # :nodoc: + old = select_value("PRAGMA foreign_keys") + + begin + execute("PRAGMA foreign_keys = OFF") + yield + ensure + execute("PRAGMA foreign_keys = #{old}") + end + end + #-- # DATABASE STATEMENTS ====================================== #++ @@ -316,6 +331,12 @@ module ActiveRecord # Returns an array of indexes for the given table. def indexes(table_name, name = nil) #:nodoc: + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| sql = <<-SQL SELECT sql @@ -418,6 +439,24 @@ module ActiveRecord rename_column_indexes(table_name, column.name, new_column_name) end + def add_reference(table_name, ref_name, **options) # :nodoc: + super(table_name, ref_name, type: :integer, **options) + end + alias :add_belongs_to :add_reference + + def foreign_keys(table_name) + fk_info = select_all("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA") + fk_info.map do |row| + options = { + column: row["from"], + primary_key: row["to"], + on_delete: extract_foreign_key_action(row["on_delete"]), + on_update: extract_foreign_key_action(row["on_update"]) + } + ForeignKeyDefinition.new(table_name, row["table"], options) + end + end + private def table_structure(table_name) @@ -519,6 +558,8 @@ module ActiveRecord RecordNotUnique.new(message) when /.* may not be NULL/, /NOT NULL constraint failed: .*/ NotNullViolation.new(message) + when /FOREIGN KEY constraint failed/i + InvalidForeignKey.new(message) else super end @@ -568,6 +609,18 @@ module ActiveRecord def create_table_definition(*args) SQLite3::TableDefinition.new(*args) end + + def extract_foreign_key_action(specifier) + case specifier + when "CASCADE"; :cascade + when "SET NULL"; :nullify + when "RESTRICT"; :restrict + end + end + + def configure_connection + execute("PRAGMA foreign_keys = ON", "SCHEMA") + end end end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 5d0f82130d..0028dc0edb 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -196,12 +196,12 @@ module ActiveRecord end def find_by(*args) # :nodoc: - return super if scope_attributes? || !(Hash === args.first) || reflect_on_all_aggregations.any? + return super if scope_attributes? || reflect_on_all_aggregations.any? hash = args.first - return super if hash.values.any? { |v| - v.nil? || Array === v || Hash === v || Relation === v + return super if !(Hash === hash) || hash.values.any? { |v| + v.nil? || Array === v || Hash === v || Relation === v || Base === v } # We can't cache Post.find_by(author: david) ...yet @@ -308,7 +308,7 @@ module ActiveRecord relation = Relation.create(self, arel_table, predicate_builder) if finder_needs_type_condition? && !ignore_default_scope? - relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + relation.where(type_condition).create_with(inheritance_column.to_s => sti_name) else relation end @@ -450,7 +450,7 @@ module ActiveRecord # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] def hash if id - self.class.hash ^ self.id.hash + self.class.hash ^ id.hash else super end @@ -472,7 +472,7 @@ module ActiveRecord # Allows sort on objects def <=>(other_object) if other_object.is_a?(self.class) - self.to_key <=> other_object.to_key + to_key <=> other_object.to_key else super end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 93b9371206..cbd71a3779 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -105,7 +105,8 @@ module ActiveRecord end if touch - updates << sanitize_sql_for_assignment(touch_updates(touch)) + touch_updates = touch_updates(touch) + updates << sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty? end unscoped.where(primary_key => id).update_all updates.join(", ") diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index de1b0d63bc..e79167d568 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -885,7 +885,7 @@ module ActiveRecord # # The keys must be the fixture names, that coincide with the short paths to the fixture files. def set_fixture_class(class_names = {}) - self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys) + self.fixture_class_names = fixture_class_names.merge(class_names.stringify_keys) end def fixtures(*fixture_set_names) @@ -970,6 +970,7 @@ module ActiveRecord @fixture_connections = enlist_fixture_connections @fixture_connections.each do |connection| connection.begin_transaction joinable: false + connection.pool.lock_thread = true end # When connections are established in the future, begin a transaction too @@ -985,6 +986,7 @@ module ActiveRecord if connection && !@fixture_connections.include?(connection) connection.begin_transaction joinable: false + connection.pool.lock_thread = true @fixture_connections << connection end end @@ -1007,6 +1009,7 @@ module ActiveRecord ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber @fixture_connections.each do |connection| connection.rollback_transaction if connection.transaction_open? + connection.pool.lock_thread = false end @fixture_connections.clear else diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index e73cb4fc12..263e2a5f7f 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -59,7 +59,16 @@ module ActiveRecord # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns # the locked record. def lock!(lock = true) - reload(lock: lock) if persisted? + if persisted? + if changed? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Locking a record with unpersisted changes is deprecated and will raise an + exception in Rails 5.2. Use `save` to persist the changes, or `reload` to + discard them explicitly. + MSG + end + reload(lock: lock) + end self end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 4b8d8d9105..ea101946f4 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -81,7 +81,7 @@ module ActiveRecord RED when /transaction\s*\Z/i CYAN - else + else MAGENTA end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index ed0c81b639..40f6226315 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -525,7 +525,7 @@ module ActiveRecord raise StandardError, "Directly inheriting from ActiveRecord::Migration is not supported. " \ "Please specify the Rails release the migration was written for:\n" \ "\n" \ - " class #{self.class.name} < ActiveRecord::Migration[4.2]" + " class #{subclass} < ActiveRecord::Migration[4.2]" end end @@ -692,7 +692,7 @@ module ActiveRecord connection.respond_to?(:reverting) && connection.reverting end - class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc: + ReversibleBlockHelper = Struct.new(:reverting) do # :nodoc: def up yield unless reverting end @@ -938,7 +938,7 @@ module ActiveRecord # MigrationProxy is used to defer loading of the actual migration classes # until they are needed - class MigrationProxy < Struct.new(:name, :version, :filename, :scope) + MigrationProxy = Struct.new(:name, :version, :filename, :scope) do def initialize(name, version, filename, scope) super @migration = nil @@ -1107,8 +1107,8 @@ module ActiveRecord validate(@migrations) - Base.connection.initialize_schema_migrations_table - Base.connection.initialize_internal_metadata_table + ActiveRecord::SchemaMigration.create_table + ActiveRecord::InternalMetadata.create_table end def current_version @@ -1170,9 +1170,10 @@ module ActiveRecord def run_without_lock migration = migrations.detect { |m| m.version == @target_version } raise UnknownMigrationVersionError.new(@target_version) if migration.nil? - execute_migration_in_transaction(migration, @direction) + result = execute_migration_in_transaction(migration, @direction) record_environment + result end # Used for running multiple migrations up to or down to a certain value. @@ -1181,11 +1182,12 @@ module ActiveRecord raise UnknownMigrationVersionError.new(@target_version) end - runnable.each do |migration| + result = runnable.each do |migration| execute_migration_in_transaction(migration, @direction) end record_environment + result end # Stores the current environment in the database. diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index 2904634eb7..85032ce470 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -14,23 +14,62 @@ module ActiveRecord V5_1 = Current class V5_0 < V5_1 + module TableDefinition + def references(*args, **options) + super(*args, type: :integer, **options) + end + alias :belongs_to :references + end + def create_table(table_name, options = {}) if adapter_name == "PostgreSQL" - if options[:id] == :uuid && !options[:default] + if options[:id] == :uuid && !options.key?(:default) options[:default] = "uuid_generate_v4()" end end + unless adapter_name == "Mysql2" && options[:id] == :bigint + if [:integer, :bigint].include?(options[:id]) && !options.key?(:default) + options[:default] = nil + end + end + # Since 5.1 Postgres adapter uses bigserial type for primary # keys by default and MySQL uses bigint. This compat layer makes old migrations utilize # serial/int type instead -- the way it used to work before 5.1. - if options[:id].blank? + unless options.key?(:id) options[:id] = :integer - options[:auto_increment] = true end - super + if block_given? + super(table_name, options) do |t| + class << t + prepend TableDefinition + end + yield t + end + else + super + end end + + def change_table(table_name, options = {}) + if block_given? + super(table_name, options) do |t| + class << t + prepend TableDefinition + end + yield t + end + else + super + end + end + + def add_reference(table_name, ref_name, **options) + super(table_name, ref_name, type: :integer, **options) + end + alias :add_belongs_to :add_reference end class V4_2 < V5_0 @@ -106,13 +145,13 @@ module ActiveRecord def index_name_for_remove(table_name, options = {}) index_name = index_name(table_name, options) - unless index_name_exists?(table_name, index_name, true) + unless index_name_exists?(table_name, index_name) if options.is_a?(Hash) && options.has_key?(:name) options_without_column = options.dup options_without_column.delete :column index_name_without_column = index_name(table_name, options_without_column) - return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) + return index_name_without_column if index_name_exists?(table_name, index_name_without_column) end raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 2a28c6bf6d..54216caaaf 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -432,6 +432,7 @@ module ActiveRecord connection.schema_cache.clear_data_source_cache!(table_name) reload_schema_from_cache + initialize_find_by_cache end private diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index e983026961..01ecd79b8f 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -393,7 +393,7 @@ module ActiveRecord # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, # then the existing record will be marked for destruction. def assign_nested_attributes_for_one_to_one_association(association_name, attributes) - options = self.nested_attributes_options[association_name] + options = nested_attributes_options[association_name] if attributes.respond_to?(:permitted?) attributes = attributes.to_h end @@ -452,7 +452,7 @@ module ActiveRecord # { id: '2', _destroy: true } # ]) def assign_nested_attributes_for_collection_association(association_name, attributes_collection) - options = self.nested_attributes_options[association_name] + options = nested_attributes_options[association_name] if attributes_collection.respond_to?(:permitted?) attributes_collection = attributes_collection.to_h end @@ -562,7 +562,7 @@ module ActiveRecord def call_reject_if(association_name, attributes) return false if will_be_destroyed?(association_name, attributes) - case callback = self.nested_attributes_options[association_name][:reject_if] + case callback = nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) when Proc diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 63d8a201f0..7ceb7d1a55 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -148,6 +148,8 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. + # + # Unless an error is raised, returns true. def save!(*args) create_or_update(*args) || raise(RecordNotSaved.new("Failed to save the record", self)) end @@ -338,14 +340,16 @@ module ActiveRecord self end - # Wrapper around #increment that saves the record. This method differs from - # its non-bang version in that it passes through the attribute setter. - # Saving is not subjected to validation checks. Returns +true+ if the - # record could be saved. - def increment!(attribute, by = 1) + # Wrapper around #increment that writes the update to the database. + # Only +attribute+ is updated; the record itself is not saved. + # This means that any other modified attributes will still be dirty. + # Validations and callbacks are skipped. Supports the `touch` option from + # +update_counters+, see that for more. + # Returns +self+. + def increment!(attribute, by = 1, touch: nil) increment(attribute, by) change = public_send(attribute) - (attribute_in_database(attribute.to_s) || 0) - self.class.update_counters(id, attribute => change) + self.class.update_counters(id, attribute => change, touch: touch) clear_attribute_change(attribute) # eww self end @@ -357,12 +361,14 @@ module ActiveRecord increment(attribute, -by) end - # Wrapper around #decrement that saves the record. This method differs from - # its non-bang version in the sense that it passes through the attribute setter. - # Saving is not subjected to validation checks. Returns +true+ if the - # record could be saved. - def decrement!(attribute, by = 1) - increment!(attribute, -by) + # Wrapper around #decrement that writes the update to the database. + # Only +attribute+ is updated; the record itself is not saved. + # This means that any other modified attributes will still be dirty. + # Validations and callbacks are skipped. Supports the `touch` option from + # +update_counters+, see that for more. + # Returns +self+. + def decrement!(attribute, by = 1, touch: nil) + increment!(attribute, -by, touch: touch) end # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 2701c5bca9..0276d41494 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -87,8 +87,8 @@ module ActiveRecord if File.file?(filename) cache = YAML.load(File.read(filename)) if cache.version == ActiveRecord::Migrator.current_version - self.connection.schema_cache = cache - self.connection_pool.schema_cache = cache.dup + connection.schema_cache = cache + connection_pool.schema_cache = cache.dup else warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}." end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index cf1c0fb423..246d330b76 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -309,14 +309,6 @@ db_namespace = namespace :db do end namespace :test do - - task :deprecated do - Rake.application.top_level_tasks.grep(/^db:test:/).each do |task| - $stderr.puts "WARNING: #{task} is deprecated. The Rails test helper now maintains " \ - "your test schema automatically, see the release notes for details." - end - end - # desc "Recreate the test database from the current schema" task load: %w(db:test:purge) do case ActiveRecord::Base.schema_format @@ -345,22 +337,6 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"] end - # desc "Recreate the test database from a fresh schema" - task clone: %w(db:test:deprecated environment) do - case ActiveRecord::Base.schema_format - when :ruby - db_namespace["test:clone_schema"].invoke - when :sql - db_namespace["test:clone_structure"].invoke - end - end - - # desc "Recreate the test database from a fresh schema.rb file" - task clone_schema: %w(db:test:deprecated db:schema:dump db:test:load_schema) - - # desc "Recreate the test database from a fresh structure.sql file" - task clone_structure: %w(db:test:deprecated db:structure:dump db:test:load_structure) - # desc "Empty the test database" task purge: %w(environment load_config check_protected_environments) do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"] diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 8ff265bdfa..6274996ab8 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -11,7 +11,7 @@ module ActiveRecord # Attributes listed as readonly will be used to create a new record but update operations will # ignore these fields. def attr_readonly(*attributes) - self._attr_readonly = Set.new(attributes.map(&:to_s)) + (self._attr_readonly || []) + self._attr_readonly = Set.new(attributes.map(&:to_s)) + (_attr_readonly || []) end # Returns an array of all the attributes that have been specified as readonly. diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index f3e81ee1e2..61a2279292 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,5 +1,6 @@ require "thread" require "active_support/core_ext/string/filters" +require "active_support/deprecation" module ActiveRecord # = Active Record Reflection @@ -175,8 +176,19 @@ module ActiveRecord JoinKeys.new(foreign_key, active_record_primary_key) end + # Returns a list of scopes that should be applied for this Reflection + # object when querying the database. + def scopes + scope ? [scope] : [] + end + + def scope_chain + chain.map(&:scopes) + end + deprecate :scope_chain + def constraints - scope_chain.flatten + chain.map(&:scopes).flatten end def counter_cache_column @@ -321,7 +333,7 @@ module ActiveRecord end end - # Holds all the meta-data about an aggregation as it was specified in the + # Holds all the metadata about an aggregation as it was specified in the # Active Record class. class AggregateReflection < MacroReflection #:nodoc: def mapping @@ -330,7 +342,7 @@ module ActiveRecord end end - # Holds all the meta-data about an association as it was specified in the + # Holds all the metadata about an association as it was specified in the # Active Record class. class AssociationReflection < MacroReflection #:nodoc: # Returns the target association's class. @@ -364,6 +376,17 @@ module ActiveRecord @constructable = calculate_constructable(macro, options) @association_scope_cache = {} @scope_lock = Mutex.new + + if options[:class_name] && options[:class_name].class == Class + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing a class to the `class_name` is deprecated and will raise + an ArgumentError in Rails 5.2. It eagerloads more classes than + necessary and potentially creates circular dependencies. + + Please pass the class name as a string: + `#{macro} :#{name}, class_name: '#{options[:class_name]}'` + MSG + end end def association_scope_cache(conn, owner) @@ -398,7 +421,7 @@ module ActiveRecord end def association_primary_key_type - klass.type_for_attribute(association_primary_key) + klass.type_for_attribute(association_primary_key.to_s) end def active_record_primary_key @@ -450,12 +473,6 @@ module ActiveRecord false end - # An array of arrays of scopes. Each item in the outside array corresponds to a reflection - # in the #chain. - def scope_chain - scope ? [[scope]] : [[]] - end - def has_scope? scope end @@ -698,7 +715,7 @@ module ActiveRecord end end - # Holds all the meta-data about a :through association as it was specified + # Holds all the metadata about a :through association as it was specified # in the Active Record class. class ThroughReflection < AbstractReflection #:nodoc: attr_reader :delegate_reflection @@ -785,45 +802,12 @@ module ActiveRecord through_reflection.clear_association_scope_cache end - # Consider the following example: - # - # class Person - # has_many :articles - # has_many :comment_tags, through: :articles - # end - # - # class Article - # has_many :comments - # has_many :comment_tags, through: :comments, source: :tags - # end - # - # class Comment - # has_many :tags - # end - # - # There may be scopes on Person.comment_tags, Article.comment_tags and/or Comment.tags, - # but only Comment.tags will be represented in the #chain. So this method creates an array - # of scopes corresponding to the chain. - def scope_chain - @scope_chain ||= begin - scope_chain = source_reflection.scope_chain.map(&:dup) - - # Add to it the scope from this reflection (if any) - scope_chain.first << scope if scope - - through_scope_chain = through_reflection.scope_chain.map(&:dup) - - if options[:source_type] - type = foreign_type - source_type = options[:source_type] - through_scope_chain.first << lambda { |object| - where(type => source_type) - } - end + def scopes + source_reflection.scopes + super + end - # Recursively fill out the rest of the array from the through reflection - scope_chain + through_scope_chain - end + def source_type_scope + through_reflection.klass.where(foreign_type => options[:source_type]) end def has_scope? @@ -851,7 +835,7 @@ module ActiveRecord end def association_primary_key_type - klass.type_for_attribute(association_primary_key) + klass.type_for_attribute(association_primary_key.to_s) end # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form. @@ -931,6 +915,14 @@ module ActiveRecord raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection) end + if parent_reflection.nil? + reflections = active_record.reflections.keys.map(&:to_sym) + + if reflections.index(through_reflection.name) > reflections.index(name) + raise HasManyThroughOrderError.new(active_record.name, self, through_reflection) + end + end + check_validity_of_inverse! end @@ -961,8 +953,7 @@ module ActiveRecord end end - protected - + private def actual_source_reflection # FIXME: this is a horrible name source_reflection.send(:actual_source_reflection) end @@ -973,7 +964,6 @@ module ActiveRecord def inverse_name; delegate_reflection.send(:inverse_name); end - private def derive_class_name # get the class_name of the belongs_to association of the through reflection options[:source_type] || source_reflection.class_name @@ -991,6 +981,15 @@ module ActiveRecord @previous_reflection = previous_reflection end + def scopes + scopes = @previous_reflection.scopes + if @previous_reflection.options[:source_type] + scopes + [@previous_reflection.source_type_scope] + else + scopes + end + end + def klass @reflection.klass end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ccd75ec5d2..61ee09bcc8 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -418,8 +418,7 @@ module ActiveRecord records.each { |record| record.update(attributes) } else if ActiveRecord::Base === id - id = id.id - ActiveSupport::Deprecation.warn(<<-MSG.squish) + raise ArgumentError, <<-MSG.squish You are passing an instance of ActiveRecord::Base to `update`. Please pass the id of the object by calling `.id`. MSG diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 4b2987ac6d..76031515fd 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -260,7 +260,7 @@ module ActiveRecord end def act_on_ignored_order(error_on_ignore) - raise_error = (error_on_ignore.nil? ? self.klass.error_on_ignored_order : error_on_ignore) + raise_error = (error_on_ignore.nil? ? klass.error_on_ignored_order : error_on_ignore) if raise_error raise ArgumentError.new(ORDER_IGNORE_MESSAGE) diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 827688a663..35c670f1a1 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -193,7 +193,7 @@ module ActiveRecord # If #count is used with #distinct (i.e. `relation.distinct.count`) it is # considered distinct. - distinct = self.distinct_value + distinct = distinct_value if operation == "count" column_name ||= select_for_count diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 3c1dea8c6c..d3ba724507 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -38,6 +38,7 @@ module ActiveRecord delegate :to_xml, :encode_with, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of, + :to_sentence, :to_formatted_s, :shuffle, :split, :index, to: :records delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, @@ -113,7 +114,7 @@ module ActiveRecord arel.respond_to?(method, include_private) end - protected + private def method_missing(method, *args, &block) if @klass.respond_to?(method) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 6663bdb244..4548944fe6 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -17,8 +17,8 @@ module ActiveRecord # Person.where("administrator = 1").order("created_on DESC").find(1) # # NOTE: The returned records may not be in the same order as the ids you - # provide since database rows are unordered. You'd need to provide an explicit QueryMethods#order - # option if you want the results are sorted. + # provide since database rows are unordered. You will need to provide an explicit QueryMethods#order + # option if you want the results to be sorted. # # ==== Find with lock # diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 78c046b07f..4ee413c805 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -3,7 +3,6 @@ require "active_record/relation/query_attribute" require "active_record/relation/where_clause" require "active_record/relation/where_clause_factory" require "active_model/forbidden_attributes_protection" -require "active_support/core_ext/string/filters" module ActiveRecord module QueryMethods @@ -657,7 +656,7 @@ module ActiveRecord end self.where_clause = self.where_clause.or(other.where_clause) - self.having_clause = self.having_clause.or(other.having_clause) + self.having_clause = having_clause.or(other.having_clause) self end diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index ef0d059d1c..417b24c7bb 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -25,10 +25,7 @@ module ActiveRecord end def except(*columns) - WhereClause.new( - predicates_except(columns), - binds_except(columns), - ) + WhereClause.new(*except_predicates_and_binds(columns)) end def or(other) @@ -134,20 +131,35 @@ module ActiveRecord end end - def predicates_except(columns) - predicates.reject do |node| - case node - when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual - subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) - columns.include?(subrelation.name.to_s) + def except_predicates_and_binds(columns) + except_binds = [] + binds_index = 0 + + predicates = self.predicates.reject do |node| + except = \ + case node + when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual + binds_contains = node.grep(Arel::Nodes::BindParam).size + subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) + columns.include?(subrelation.name.to_s) + end + + if except && binds_contains > 0 + (binds_index...(binds_index + binds_contains)).each do |i| + except_binds[i] = true + end + + binds_index += binds_contains end + + except end - end - def binds_except(columns) - binds.reject do |attr| - columns.include?(attr.name) + binds = self.binds.reject.with_index do |_, i| + except_binds[i] end + + [predicates, binds] end def predicates_with_wrapped_sql_literals diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 737bc278bd..04bee73e8f 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -15,9 +15,12 @@ module ActiveRecord attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) attributes.stringify_keys! - attributes, binds = predicate_builder.create_binds(attributes) - - parts = predicate_builder.build_from_hash(attributes) + if perform_case_sensitive?(options = other.last) + parts, binds = build_for_case_sensitive(attributes, options) + else + attributes, binds = predicate_builder.create_binds(attributes) + parts = predicate_builder.build_from_hash(attributes) + end when Arel::Nodes::Node parts = [opts] else @@ -32,6 +35,43 @@ module ActiveRecord protected attr_reader :klass, :predicate_builder + + private + + def perform_case_sensitive?(options) + options && options.key?(:case_sensitive) + end + + def build_for_case_sensitive(attributes, options) + parts, binds = [], [] + table = klass.arel_table + + attributes.each do |attribute, value| + if reflection = klass._reflect_on_association(attribute) + attribute = reflection.foreign_key.to_s + value = value[reflection.klass.primary_key] unless value.nil? + end + + if value.nil? + parts << table[attribute].eq(value) + else + column = klass.column_for_attribute(attribute) + + binds << predicate_builder.send(:build_bind_param, attribute, value) + value = Arel::Nodes::BindParam.new + + predicate = if options[:case_sensitive] + klass.connection.case_sensitive_comparison(table, attribute, column, value) + else + klass.connection.case_insensitive_comparison(table, attribute, column, value) + end + + parts << predicate + end + end + + [parts, binds] + end end end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 647834b12e..427c0019c6 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -46,7 +46,7 @@ module ActiveRecord # # sanitize_sql_for_assignment("name=NULL and group_id='4'") # # => "name=NULL and group_id='4'" - def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name) # :doc: + def sanitize_sql_for_assignment(assignments, default_table_name = table_name) # :doc: case assignments when Array; sanitize_sql_array(assignments) when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name) diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 7a2bc9c8af..5104427339 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -48,7 +48,7 @@ module ActiveRecord instance_eval(&block) if info[:version].present? - initialize_schema_migrations_table + ActiveRecord::SchemaMigration.create_table connection.assume_migrated_upto_version(info[:version], migrations_paths) end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 12289511b7..15533f0151 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -188,7 +188,7 @@ HEADER index_parts << "length: { #{format_options(index.lengths)} }" if index.lengths.present? index_parts << "order: { #{format_options(index.orders)} }" if index.orders.present? index_parts << "where: #{index.where.inspect}" if index.where - index_parts << "using: #{index.using.inspect}" if index.using + index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index) index_parts << "type: #{index.type.inspect}" if index.type index_parts << "comment: #{index.comment.inspect}" if index.comment index_parts diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb index 7606961e2e..115799cc20 100644 --- a/activerecord/lib/active_record/secure_token.rb +++ b/activerecord/lib/active_record/secure_token.rb @@ -27,7 +27,7 @@ module ActiveRecord # Load securerandom only when has_secure_token is used. require "active_support/core_ext/securerandom" define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token } - before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token) unless self.send("#{attribute}?") } + before_create { send("#{attribute}=", self.class.generate_unique_secure_token) unless send("#{attribute}?") } end def generate_unique_secure_token diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 5a408e7b8e..db2bd0b55e 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -9,7 +9,7 @@ module ActiveRecord #:nodoc: end def serializable_hash(options = nil) - options = options.try(:clone) || {} + options = options.try(:dup) || {} options[:except] = Array(options[:except]).map(&:to_s) options[:except] |= Array(self.class.inheritance_column) diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index d4be20d999..006afe7495 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -78,7 +78,7 @@ module ActiveRecord module ClassMethods def store(store_attribute, options = {}) - serialize store_attribute, IndifferentCoder.new(options[:coder]) + serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder]) store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors end @@ -177,12 +177,12 @@ module ActiveRecord end class IndifferentCoder # :nodoc: - def initialize(coder_or_class_name) + def initialize(attr_name, coder_or_class_name) @coder = if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump) coder_or_class_name else - ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object) + ActiveRecord::Coders::YAMLColumn.new(attr_name, coder_or_class_name || Object) end end diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index b618e5cfcd..71efc1829a 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -44,7 +44,7 @@ module ActiveRecord end def associated_table(table_name) - association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.singularize) + association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.to_s.singularize) if !association && table_name == arel_table.name return self diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 1423e6008f..82604a915f 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/string/filters" - module ActiveRecord module Tasks # :nodoc: class DatabaseAlreadyExists < StandardError; end # :nodoc: @@ -269,8 +267,8 @@ module ActiveRecord if seed_loader seed_loader.load_seed else - raise "You tried to load seed data, but no seed loader is specified. Please specify seed " + - "loader with ActiveRecord::Tasks::DatabaseTasks.seed_loader = your_seed_loader\n" + + raise "You tried to load seed data, but no seed loader is specified. Please specify seed " \ + "loader with ActiveRecord::Tasks::DatabaseTasks.seed_loader = your_seed_loader\n" \ "Seed loader should respond to load_seed method" end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 56b75540e3..08417aaa0f 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -11,7 +11,6 @@ module ActiveRecord :before_commit_without_transaction_enrollment, :commit_without_transaction_enrollment, :rollback_without_transaction_enrollment, - terminator: deprecated_false_terminator, scope: [:kind, :name] end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index ac9134bfcb..6af05c1860 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -43,7 +43,7 @@ module ActiveRecord def assert_valid_value(value) if coder.respond_to?(:assert_valid_value) - coder.assert_valid_value(value) + coder.assert_valid_value(value, action: "serialize") end end diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index ca5eda2f84..7cfd55f516 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -57,7 +57,7 @@ module ActiveRecord # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The method, # proc or string should return or evaluate to a +true+ or +false+ value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See ActiveModel::Validation#validates! for more information. + # See ActiveModel::Validations#validates! for more information. def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 9e8edfbfaf..154cf5f1a4 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -50,37 +50,7 @@ module ActiveRecord end def build_relation(klass, attribute, value) - if reflection = klass._reflect_on_association(attribute) - attribute = reflection.foreign_key - value = value.attributes[reflection.klass.primary_key] unless value.nil? - end - - if value.nil? - return klass.unscoped.where!(attribute => value) - end - - # the attribute may be an aliased attribute - if klass.attribute_alias?(attribute) - attribute = klass.attribute_alias(attribute) - end - - attribute_name = attribute.to_s - - table = klass.arel_table - column = klass.columns_hash[attribute_name] - cast_type = klass.type_for_attribute(attribute_name) - - comparison = if !options[:case_sensitive] - # will use SQL LOWER function before comparison, unless it detects a case insensitive collation - klass.connection.case_insensitive_comparison(table, attribute, column, value) - else - klass.connection.case_sensitive_comparison(table, attribute, column, value) - end - klass.unscoped.tap do |scope| - parts = [comparison] - binds = [Relation::QueryAttribute.new(attribute_name, value, cast_type)] - scope.where_clause += Relation::WhereClause.new(parts, binds) - end + klass.unscoped.where!({ attribute => value }, options) end def scope_relation(record, relation) diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb index 4263c11ffc..43075077b9 100644 --- a/activerecord/lib/rails/generators/active_record/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration.rb @@ -20,6 +20,14 @@ module ActiveRecord key_type = options[:primary_key_type] ", id: :#{key_type}" if key_type end + + def db_migrate_path + if defined?(Rails) && Rails.application + Rails.application.config.paths["db/migrate"].to_ary.first + else + "db/migrate" + end + end end end end diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index 8511531af7..1f1c47499b 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -10,7 +10,7 @@ module ActiveRecord def create_migration_file set_local_assigns! validate_file_name! - migration_template @migration_template, "db/migrate/#{file_name}.rb" + migration_template @migration_template, File.join(db_migrate_path, "#{file_name}.rb") end # TODO Change this to private once we've dropped Ruby 2.2 support. diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index 61a8d3c100..5cec07d2e3 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -17,7 +17,7 @@ module ActiveRecord def create_migration_file return unless options[:migration] && options[:parent].nil? attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false - migration_template "../../migration/templates/create_table_migration.rb", "db/migrate/create_#{table_name}.rb" + migration_template "../../migration/templates/create_table_migration.rb", File.join(db_migrate_path, "create_#{table_name}.rb") end def create_model_file @@ -41,7 +41,7 @@ module ActiveRecord # FIXME: Change this file to a symlink once RubyGems 2.5.0 is required. def generate_application_record - if self.behavior == :invoke && !application_record_exist? + if behavior == :invoke && !application_record_exist? template "application_record.rb", application_record_file_name end end diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 9828e682ef..0c9d1dff9d 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -31,7 +31,6 @@ module ActiveRecord end def test_tables - tables = nil tables = @connection.tables assert_includes tables, "accounts" assert_includes tables, "authors" @@ -185,34 +184,6 @@ module ActiveRecord end unless current_adapter?(:SQLite3Adapter) - def test_foreign_key_violations_are_translated_to_specific_exception - error = assert_raises(ActiveRecord::InvalidForeignKey) do - # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method - if @connection.prefetch_primary_key? - id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) - @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" - else - @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" - end - end - - assert_not_nil error.cause - end - - def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false - klass_has_fk = Class.new(ActiveRecord::Base) do - self.table_name = "fk_test_has_fk" - end - - error = assert_raises(ActiveRecord::InvalidForeignKey) do - has_fk = klass_has_fk.new - has_fk.fk_id = 1231231231 - has_fk.save(validate: false) - end - - assert_not_nil error.cause - end - def test_value_limit_violations_are_translated_to_specific_exception error = assert_raises(ActiveRecord::ValueTooLong) do Event.create(title: "abcdefgh") @@ -223,30 +194,13 @@ module ActiveRecord def test_numeric_value_out_of_ranges_are_translated_to_specific_exception error = assert_raises(ActiveRecord::RangeError) do - Book.connection.create("INSERT INTO books(author_id) VALUES (2147483648)") + Book.connection.create("INSERT INTO books(author_id) VALUES (9223372036854775808)") end assert_not_nil error.cause end end - def test_disable_referential_integrity - assert_nothing_raised do - @connection.disable_referential_integrity do - # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method - if @connection.prefetch_primary_key? - id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) - @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" - else - @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" - end - # should delete created record as otherwise disable_referential_integrity will try to enable constraints after executed block - # and will fail (at least on Oracle) - @connection.execute "DELETE FROM fk_test_has_fk" - end - end - end - def test_select_all_always_return_activerecord_result result = @connection.select_all "SELECT * FROM posts" assert result.is_a?(ActiveRecord::Result) @@ -290,6 +244,59 @@ module ActiveRecord end end + class AdapterForeignKeyTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + def setup + @connection = ActiveRecord::Base.connection + end + + def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false + klass_has_fk = Class.new(ActiveRecord::Base) do + self.table_name = "fk_test_has_fk" + end + + error = assert_raises(ActiveRecord::InvalidForeignKey) do + has_fk = klass_has_fk.new + has_fk.fk_id = 1231231231 + has_fk.save(validate: false) + end + + assert_not_nil error.cause + end + + def test_foreign_key_violations_are_translated_to_specific_exception + error = assert_raises(ActiveRecord::InvalidForeignKey) do + insert_into_fk_test_has_fk + end + + assert_not_nil error.cause + end + + def test_disable_referential_integrity + assert_nothing_raised do + @connection.disable_referential_integrity do + insert_into_fk_test_has_fk + # should delete created record as otherwise disable_referential_integrity will try to enable constraints + # after executed block and will fail (at least on Oracle) + @connection.execute "DELETE FROM fk_test_has_fk" + end + end + end + + private + + def insert_into_fk_test_has_fk + # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method + if @connection.prefetch_primary_key? + id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) + @connection.execute "INSERT INTO fk_test_has_fk (id,fk_id) VALUES (#{id_value},0)" + else + @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + end + end + end + class AdapterTestWithoutTransaction < ActiveRecord::TestCase self.use_transactional_tests = false diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 2a528b2cb1..67e1efde27 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -92,8 +92,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase assert_equal expected, actual end - expected = "ALTER TABLE `peaple` ADD INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" - actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t| + expected = "ALTER TABLE `people` ADD INDEX `index_people_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" + actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| t.index :last_name, length: 10, using: :btree, algorithm: :copy end assert_equal expected, actual diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index c1de2218e2..1f94472390 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -66,9 +66,10 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase def test_execute_after_disconnect @connection.disconnect! - assert_raise(ActiveRecord::StatementInvalid) do + error = assert_raise(ActiveRecord::StatementInvalid) do @connection.execute("SELECT 1") end + assert_kind_of Mysql2::Error, error.cause end def test_quote_after_disconnect @@ -119,7 +120,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end end - def test_passing_arbitary_flags_to_adapter + def test_passing_arbitrary_flags_to_adapter run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.merge(flags: Mysql2::Client::COMPRESS)) assert_equal (Mysql2::Client::COMPRESS | Mysql2::Client::FOUND_ROWS), ActiveRecord::Base.connection.raw_connection.query_options[:flags] diff --git a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb index 135789a57d..c131a5169c 100644 --- a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb @@ -6,23 +6,27 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase end test "microsecond precision for MySQL gte 5.6.4" do - stub_version "5.6.4" - assert_microsecond_precision + stub_version "5.6.4" do + assert_microsecond_precision + end end test "no microsecond precision for MySQL lt 5.6.4" do - stub_version "5.6.3" - assert_no_microsecond_precision + stub_version "5.6.3" do + assert_no_microsecond_precision + end end test "microsecond precision for MariaDB gte 5.3.0" do - stub_version "5.5.5-10.1.8-MariaDB-log" - assert_microsecond_precision + stub_version "5.5.5-10.1.8-MariaDB-log" do + assert_microsecond_precision + end end test "no microsecond precision for MariaDB lt 5.3.0" do - stub_version "5.2.9-MariaDB" - assert_no_microsecond_precision + stub_version "5.2.9-MariaDB" do + assert_no_microsecond_precision + end end private @@ -41,5 +45,8 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase def stub_version(full_version_string) @connection.stubs(:full_version).returns(full_version_string) @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) + yield + ensure + @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) end end diff --git a/activerecord/test/cases/adapters/mysql2/legacy_migration_test.rb b/activerecord/test/cases/adapters/mysql2/legacy_migration_test.rb deleted file mode 100644 index 5d3125c2be..0000000000 --- a/activerecord/test/cases/adapters/mysql2/legacy_migration_test.rb +++ /dev/null @@ -1,60 +0,0 @@ -require "cases/helper" - -class MysqlLegacyMigrationTest < ActiveRecord::Mysql2TestCase - self.use_transactional_tests = false - - class GenerateTableWithoutBigint < ActiveRecord::Migration[5.0] - def change - create_table :legacy_integer_pk do |table| - table.string :foo - end - - create_table :override_pk, id: :bigint do |table| - table.string :bar - end - end - end - - def setup - super - @connection = ActiveRecord::Base.connection - - @migration_verbose_old = ActiveRecord::Migration.verbose - ActiveRecord::Migration.verbose = false - - migrations = [GenerateTableWithoutBigint.new(nil, 1)] - - ActiveRecord::Migrator.new(:up, migrations).migrate - end - - def teardown - ActiveRecord::Migration.verbose = @migration_verbose_old - @connection.drop_table("legacy_integer_pk") - @connection.drop_table("override_pk") - ActiveRecord::SchemaMigration.delete_all rescue nil - super - end - - def test_create_table_uses_integer_as_pkey_by_default - col = column(:legacy_integer_pk, :id) - assert_equal "int(11)", sql_type_for(col) - assert col.auto_increment? - end - - def test_create_tables_respects_pk_column_type_override - col = column(:override_pk, :id) - assert_equal "bigint(20)", sql_type_for(col) - end - - private - - def column(table_name, column_name) - ActiveRecord::Base.connection - .columns(table_name.to_s) - .detect { |c| c.name == column_name.to_s } - end - - def sql_type_for(col) - col && col.sql_type - end -end diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index fa54aac6b3..605baa9905 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -16,7 +16,7 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase table_name = ActiveRecord::SchemaMigration.table_name connection.drop_table table_name, if_exists: true - connection.initialize_schema_migrations_table + ActiveRecord::SchemaMigration.create_table assert connection.column_exists?(table_name, :version, :string, collation: "utf8_general_ci") end @@ -27,7 +27,7 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase table_name = ActiveRecord::InternalMetadata.table_name connection.drop_table table_name, if_exists: true - connection.initialize_internal_metadata_table + ActiveRecord::InternalMetadata.create_table assert connection.column_exists?(table_name, :key, :string, collation: "utf8_general_ci") end diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb index bee42d48f1..d6e7f29a5c 100644 --- a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb +++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb @@ -8,7 +8,7 @@ class Mysql2SqlTypesTest < ActiveRecord::Mysql2TestCase assert_equal "blob", type_to_sql(:binary) end - def type_to_sql(*args) - ActiveRecord::Base.connection.type_to_sql(*args) + def type_to_sql(type, limit = nil) + ActiveRecord::Base.connection.type_to_sql(type, limit: limit) end end diff --git a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb new file mode 100644 index 0000000000..442a4fb7b5 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb @@ -0,0 +1,59 @@ +require "cases/helper" +require "support/schema_dumping_helper" + +if ActiveRecord::Base.connection.supports_virtual_columns? + class Mysql2VirtualColumnTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + + self.use_transactional_tests = false + + class VirtualColumn < ActiveRecord::Base + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :virtual_columns, force: true do |t| + t.string :name + t.virtual :upper_name, type: :string, as: "UPPER(`name`)" + t.virtual :name_length, type: :integer, as: "LENGTH(`name`)", stored: true + end + VirtualColumn.create(name: "Rails") + end + + def teardown + @connection.drop_table :virtual_columns, if_exists: true + VirtualColumn.reset_column_information + end + + def test_virtual_column + column = VirtualColumn.columns_hash["upper_name"] + assert_predicate column, :virtual? + assert_match %r{\bVIRTUAL\b}, column.extra + assert_equal "RAILS", VirtualColumn.take.upper_name + end + + def test_stored_column + column = VirtualColumn.columns_hash["name_length"] + assert_predicate column, :virtual? + assert_match %r{\b(?:STORED|PERSISTENT)\b}, column.extra + assert_equal 5, VirtualColumn.take.name_length + end + + def test_change_table + @connection.change_table :virtual_columns do |t| + t.virtual :lower_name, type: :string, as: "LOWER(name)" + end + VirtualColumn.reset_column_information + column = VirtualColumn.columns_hash["lower_name"] + assert_predicate column, :virtual? + assert_match %r{\bVIRTUAL\b}, column.extra + assert_equal "rails", VirtualColumn.take.lower_name + end + + def test_schema_dumping + output = dump_table_schema("virtual_columns") + assert_match(/t\.virtual\s+"upper_name",\s+type: :string,\s+as: "UPPER\(`name`\)"$/i, output) + assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "LENGTH\(`name`\)",\s+stored: true$/i, output) + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index 5c207116c4..505c297cd4 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -32,9 +32,9 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase end def test_binary_columns_are_limitless_the_upper_limit_is_one_GB - assert_equal "bytea", @connection.type_to_sql(:binary, 100_000) + assert_equal "bytea", @connection.type_to_sql(:binary, limit: 100_000) assert_raise ActiveRecord::ActiveRecordError do - @connection.type_to_sql :binary, 4294967295 + @connection.type_to_sql(:binary, limit: 4294967295) end end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index e916d15f7f..3cbd4ca212 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -95,7 +95,7 @@ module ActiveRecord end def test_indexes_logs_name - @connection.indexes("items", "hello") + assert_deprecated { @connection.indexes("items", "hello") } assert_equal "SCHEMA", @subscriber.logged[0][1] end @@ -156,7 +156,7 @@ module ActiveRecord secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})") ActiveRecord::Base.connection_pool.checkin(secondary_connection) elsif ARTest.config["with_manual_interventions"] - puts "Kill the connection now (e.g. by restarting the PostgreSQL " + + puts "Kill the connection now (e.g. by restarting the PostgreSQL " \ 'server with the "-m fast" option) and then press enter.' $stdin.gets else @@ -175,7 +175,7 @@ module ActiveRecord new_connection_pid = @connection.query("select pg_backend_pid()") assert_not_equal original_connection_pid, new_connection_pid, - "umm -- looks like you didn't break the connection, because we're still " + + "umm -- looks like you didn't break the connection, because we're still " \ "successfully querying with the same connection pid." # Repair all fixture connections so other tests won't break. diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 0ac8b7339b..0725fde5ae 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -28,12 +28,12 @@ class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase end def test_data_type_of_time_types - assert_equal :string, @first_time.column_for_attribute(:time_interval).type - assert_equal :string, @first_time.column_for_attribute(:scaled_time_interval).type + assert_equal :interval, @first_time.column_for_attribute(:time_interval).type + assert_equal :interval, @first_time.column_for_attribute(:scaled_time_interval).type end def test_data_type_of_oid_types - assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type + assert_equal :oid, @first_oid.column_for_attribute(:obj_id).type end def test_time_values @@ -61,9 +61,9 @@ class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase end def test_text_columns_are_limitless_the_upper_limit_is_one_GB - assert_equal "text", @connection.type_to_sql(:text, 100_000) + assert_equal "text", @connection.type_to_sql(:text, limit: 100_000) assert_raise ActiveRecord::ActiveRecordError do - @connection.type_to_sql :text, 4294967295 + @connection.type_to_sql(:text, limit: 4294967295) end end end diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index 19b00258b6..b9e177e6ec 100644 --- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -30,7 +30,7 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase record = PostgresqlInfinity.new(float: "-Infinity") assert_equal(-Float::INFINITY, record.float) record = PostgresqlInfinity.new(float: "NaN") - assert_send [record.float, :nan?] + assert record.float.nan?, "Expected #{record.float} to be NaN" end test "update_all with infinity on a float column" do diff --git a/activerecord/test/cases/adapters/postgresql/legacy_migration_test.rb b/activerecord/test/cases/adapters/postgresql/legacy_migration_test.rb deleted file mode 100644 index 082fe95053..0000000000 --- a/activerecord/test/cases/adapters/postgresql/legacy_migration_test.rb +++ /dev/null @@ -1,54 +0,0 @@ -require "cases/helper" - -class PostgresqlLegacyMigrationTest < ActiveRecord::PostgreSQLTestCase - class GenerateTableWithoutBigserial < ActiveRecord::Migration[5.0] - def change - create_table :legacy_integer_pk do |table| - table.string :foo - end - - create_table :override_pk, id: :bigint do |table| - table.string :bar - end - end - end - - def setup - super - - @migration_verbose_old = ActiveRecord::Migration.verbose - ActiveRecord::Migration.verbose = false - - migrations = [GenerateTableWithoutBigserial.new(nil, 1)] - ActiveRecord::Migrator.new(:up, migrations).migrate - end - - def teardown - ActiveRecord::Migration.verbose = @migration_verbose_old - - super - end - - def test_create_table_uses_serial_as_pkey_by_default - col = column(:legacy_integer_pk, :id) - assert_equal "integer", sql_type_for(col) - assert col.serial? - end - - def test_create_tables_respects_pk_column_type_override - col = column(:override_pk, :id) - assert_equal "bigint", sql_type_for(col) - end - - private - - def column(table_name, column_name) - ActiveRecord::Base.connection. - columns(table_name.to_s). - detect { |c| c.name == column_name.to_s } - end - - def sql_type_for(col) - col && col.sql_type - end -end diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb index 834354dcc9..bfb2b7c27a 100644 --- a/activerecord/test/cases/adapters/postgresql/numbers_test.rb +++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb @@ -31,7 +31,7 @@ class PostgresqlNumberTest < ActiveRecord::PostgreSQLTestCase assert_equal 123456.789, first.double assert_equal(-::Float::INFINITY, second.single) assert_equal ::Float::INFINITY, second.double - assert_send [third.double, :nan?] + assert third.double.nan?, "Expected #{third.double} to be NaN" end def test_update diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index e6af93a53e..3054f0271f 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -54,12 +54,6 @@ module ActiveRecord end end - def test_primary_key_raises_error_if_table_not_found - assert_raises(ActiveRecord::StatementInvalid) do - @connection.primary_key("unobtainium") - end - end - def test_exec_insert_with_returning_disabled connection = connection_without_insert_returning result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], "id", "postgresql_partitioned_table_parent_id_seq") @@ -263,9 +257,12 @@ module ActiveRecord def test_index_with_opclass with_example_table do - @connection.add_index "ex", "data varchar_pattern_ops", name: "with_opclass" - index = @connection.indexes("ex").find { |idx| idx.name == "with_opclass" } + @connection.add_index "ex", "data varchar_pattern_ops" + index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" } assert_equal "data varchar_pattern_ops", index.columns + + @connection.remove_index "ex", "data varchar_pattern_ops" + assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" } end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 237e9ff6a5..7b065ff320 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -301,13 +301,13 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase def test_index_name_exists with_schema_search_path(SCHEMA_NAME) do - assert @connection.index_name_exists?(TABLE_NAME, INDEX_A_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_B_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_C_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_D_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true) - assert_not @connection.index_name_exists?(TABLE_NAME, "missing_index", true) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_A_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_B_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_C_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_D_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME) + assert_not @connection.index_name_exists?(TABLE_NAME, "missing_index") end end @@ -366,14 +366,6 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase end end - def test_primary_key_raises_error_if_table_not_found_on_schema_search_path - with_schema_search_path(SCHEMA2_NAME) do - assert_raises(ActiveRecord::StatementInvalid) do - @connection.primary_key(PK_TABLE_NAME) - end - end - end - def test_pk_and_sequence_for_with_schema_specified pg_name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name [ diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index f34d50e25c..6aa6a79705 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -234,25 +234,23 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase end end - if ActiveRecord::Base.connection.supports_pgcrypto_uuid? - def test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration - @verbose_was = ActiveRecord::Migration.verbose - ActiveRecord::Migration.verbose = false - - migration = Class.new(ActiveRecord::Migration[4.2]) do - def version; 101 end - def migrate(x) - create_table("pg_uuids_4", id: :uuid) - end - end.new - ActiveRecord::Migrator.new(:up, [migration]).migrate - - schema = dump_table_schema "pg_uuids_4" - assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema) - ensure - drop_table "pg_uuids_4" - ActiveRecord::Migration.verbose = @verbose_was - end + def test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + migration = Class.new(ActiveRecord::Migration[5.0]) do + def version; 101 end + def migrate(x) + create_table("pg_uuids_4", id: :uuid) + end + end.new + ActiveRecord::Migrator.new(:up, [migration]).migrate + + schema = dump_table_schema "pg_uuids_4" + assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema) + ensure + drop_table "pg_uuids_4" + ActiveRecord::Migration.verbose = @verbose_was end end end @@ -285,6 +283,25 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase schema = dump_table_schema "pg_uuids" assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema) end + + def test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + migration = Class.new(ActiveRecord::Migration[5.0]) do + def version; 101 end + def migrate(x) + create_table("pg_uuids_4", id: :uuid, default: nil) + end + end.new + ActiveRecord::Migrator.new(:up, [migration]).migrate + + schema = dump_table_schema "pg_uuids_4" + assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema) + ensure + drop_table "pg_uuids_4" + ActiveRecord::Migration.verbose = @verbose_was + end end end @@ -330,7 +347,6 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase assert_raise ActiveRecord::RecordNotFound do UuidPost.find(123456) end - end def test_find_by_with_uuid diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 91967c1e33..e1cfd703e8 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -41,8 +41,8 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase test_copy_table("comments", "comments_with_index") do @connection.add_index("comments_with_index", ["post_id", "type"]) test_copy_table("comments_with_index", "comments_with_index2") do - assert_equal table_indexes_without_name("comments_with_index"), - table_indexes_without_name("comments_with_index2") + assert_nil table_indexes_without_name("comments_with_index") + assert_nil table_indexes_without_name("comments_with_index2") end end end @@ -59,7 +59,8 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase copied_id = @connection.columns("goofy_string_id2").detect { |col| col.name == "id" } assert_equal original_id.type, copied_id.type assert_equal original_id.sql_type, copied_id.sql_type - assert_equal original_id.limit, copied_id.limit + assert_nil original_id.limit + assert_nil copied_id.limit end end diff --git a/activerecord/test/cases/adapters/sqlite3/legacy_migration_test.rb b/activerecord/test/cases/adapters/sqlite3/legacy_migration_test.rb deleted file mode 100644 index fcca8d66b5..0000000000 --- a/activerecord/test/cases/adapters/sqlite3/legacy_migration_test.rb +++ /dev/null @@ -1,59 +0,0 @@ -require "cases/helper" - -class SqliteLegacyMigrationTest < ActiveRecord::SQLite3TestCase - self.use_transactional_tests = false - - class GenerateTableWithoutBigint < ActiveRecord::Migration[5.0] - def change - create_table :legacy_integer_pk do |table| - table.string :foo - end - - create_table :override_pk, id: :bigint do |table| - table.string :bar - end - end - end - - def setup - super - @connection = ActiveRecord::Base.connection - - @migration_verbose_old = ActiveRecord::Migration.verbose - ActiveRecord::Migration.verbose = false - - migrations = [GenerateTableWithoutBigint.new(nil, 1)] - - ActiveRecord::Migrator.new(:up, migrations).migrate - end - - def teardown - ActiveRecord::Migration.verbose = @migration_verbose_old - @connection.drop_table("legacy_integer_pk") - @connection.drop_table("override_pk") - ActiveRecord::SchemaMigration.delete_all rescue nil - super - end - - def test_create_table_uses_integer_as_pkey_by_default - col = column(:legacy_integer_pk, :id) - assert_equal "INTEGER", sql_type_for(col) - assert primary_key?(:legacy_integer_pk, "id"), "id is not primary key" - end - - private - - def column(table_name, column_name) - ActiveRecord::Base.connection - .columns(table_name.to_s) - .detect { |c| c.name == column_name.to_s } - end - - def sql_type_for(col) - col && col.sql_type - end - - def primary_key?(table_name, column) - ActiveRecord::Base.connection.execute("PRAGMA table_info(#{table_name})").find { |col| col["name"] == column }["pk"] == 1 - end -end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index a6109348cc..a6afb7816b 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -287,7 +287,7 @@ module ActiveRecord def test_indexes_logs_name with_example_table do assert_logged [["PRAGMA index_list(\"ex\")", "SCHEMA", []]] do - @conn.indexes("ex", "hello") + assert_deprecated { @conn.indexes("ex", "hello") } end end end diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index aebcce3691..37ff973397 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -3,7 +3,6 @@ require "cases/helper" class SQLite3StatementPoolTest < ActiveRecord::SQLite3TestCase if Process.respond_to?(:fork) def test_cache_is_per_pid - cache = ActiveRecord::ConnectionAdapters::SQLite3Adapter::StatementPool.new(10) cache["foo"] = "bar" assert_equal "bar", cache["foo"] diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index ff1bf8acd4..11f4aae5b3 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -739,18 +739,25 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_invalid_association_reference - assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + e = assert_raise(ActiveRecord::AssociationNotFoundError) { Post.all.merge!(includes: :monkeys).find(6) } - assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message) + + e = assert_raise(ActiveRecord::AssociationNotFoundError) { Post.all.merge!(includes: [ :monkeys ]).find(6) } - assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message) + + e = assert_raise(ActiveRecord::AssociationNotFoundError) { Post.all.merge!(includes: [ "monkeys" ]).find(6) } - assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") { + assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message) + + e = assert_raise(ActiveRecord::AssociationNotFoundError) { Post.all.merge!(includes: [ :monkeys, :elephants ]).find(6) } + assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message) end def test_eager_has_many_through_with_order @@ -933,7 +940,11 @@ class EagerAssociationTest < ActiveRecord::TestCase d2 = find_all_ordered(Firm, :account) d1.each_index do |i| assert_equal(d1[i], d2[i]) - assert_equal(d1[i].account, d2[i].account) + if d1[i].account.nil? + assert_nil(d2[i].account) + else + assert_equal(d1[i].account, d2[i].account) + end end end @@ -943,7 +954,13 @@ class EagerAssociationTest < ActiveRecord::TestCase d2 = find_all_ordered(Client, firm_types) d1.each_index do |i| assert_equal(d1[i], d2[i]) - firm_types.each { |type| assert_equal(d1[i].send(type), d2[i].send(type)) } + firm_types.each do |type| + if (expected = d1[i].send(type)).nil? + assert_nil(d2[i].send(type)) + else + assert_equal(expected, d2[i].send(type)) + end + end end end def test_eager_with_valid_association_as_string_not_symbol diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 54fb61d6a5..d6b595d7e7 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -86,8 +86,10 @@ class DeveloperWithSymbolClassName < Developer has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys end -class DeveloperWithConstantClassName < Developer - has_and_belongs_to_many :projects, class_name: ProjectWithSymbolsForKeys +ActiveSupport::Deprecation.silence do + class DeveloperWithConstantClassName < Developer + has_and_belongs_to_many :projects, class_name: ProjectWithSymbolsForKeys + end end class DeveloperWithExtendOption < Developer @@ -743,8 +745,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_find_scoped_grouped_having - assert_equal 2, projects(:active_record).well_payed_salary_groups.to_a.size - assert projects(:active_record).well_payed_salary_groups.all? { |g| g.salary > 10000 } + assert_equal 2, projects(:active_record).well_paid_salary_groups.to_a.size + assert projects(:active_record).well_paid_salary_groups.all? { |g| g.salary > 10000 } end def test_get_ids diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index cbecfa84ff..ede3a44090 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -611,21 +611,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_update_all_on_association_accessed_before_save firm = Firm.new(name: "Firm") - clients_proxy_id = firm.clients.object_id firm.clients << Client.first firm.save! assert_equal firm.clients.count, firm.clients.update_all(description: "Great!") - assert_not_equal clients_proxy_id, firm.clients.object_id end def test_update_all_on_association_accessed_before_save_with_explicit_foreign_key - # We can use the same cached proxy object because the id is available for the scope firm = Firm.new(name: "Firm", id: 100) - clients_proxy_id = firm.clients.object_id firm.clients << Client.first firm.save! assert_equal firm.clients.count, firm.clients.update_all(description: "Great!") - assert_equal clients_proxy_id, firm.clients.object_id end def test_belongs_to_sanity diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 47c6480a8e..ea52fb5a67 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -28,6 +28,9 @@ require "models/member" require "models/membership" require "models/club" require "models/organization" +require "models/user" +require "models/family" +require "models/family_tree" class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags, @@ -880,7 +883,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase book.subscriber_ids = [] assert_equal [], book.subscribers.reload end - end def test_collection_singular_ids_setter_with_changed_primary_key @@ -1231,4 +1233,23 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase ensure TenantMembership.current_member = nil end + + def test_has_many_through_with_scope_should_respect_table_alias + family = Family.create! + users = 3.times.map { User.create! } + FamilyTree.create!(member: users[0], family: family) + FamilyTree.create!(member: users[1], family: family) + FamilyTree.create!(member: users[2], family: family, token: "wat") + + assert_equal 2, users[0].family_members.to_a.size + assert_equal 0, users[2].family_members.to_a.size + end + + def test_incorrectly_ordered_through_associations + assert_raises(ActiveRecord::HasManyThroughOrderError) do + DeveloperWithIncorrectlyOrderedHasManyThrough.create( + companies: [Company.create] + ) + end + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index ed22a9802f..7c11d2e7fc 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -476,7 +476,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal ships(:black_pearl), pirate.ship assert_equal pirate.id, pirate.ship.pirate_id - assert_equal "Failed to remove the existing associated ship. " + + assert_equal "Failed to remove the existing associated ship. " \ "The record failed to save after its foreign key was set to nil.", error.message end @@ -648,6 +648,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase class SpecialBook < ActiveRecord::Base self.table_name = "books" belongs_to :author, class_name: "SpecialAuthor" + has_one :subscription, class_name: "SpecialSupscription", foreign_key: "subscriber_id" end class SpecialAuthor < ActiveRecord::Base @@ -655,11 +656,27 @@ class HasOneAssociationsTest < ActiveRecord::TestCase has_one :book, class_name: "SpecialBook", foreign_key: "author_id" end - def test_assocation_enum_works_properly + class SpecialSupscription < ActiveRecord::Base + self.table_name = "subscriptions" + belongs_to :book, class_name: "SpecialBook" + end + + def test_association_enum_works_properly author = SpecialAuthor.create!(name: "Test") book = SpecialBook.create!(status: "published") author.book = book refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count end + + def test_association_enum_works_properly_with_nested_join + author = SpecialAuthor.create!(name: "Test") + book = SpecialBook.create!(status: "published") + author.book = book + + where_clause = { books: { subscriptions: { subscriber_id: nil } } } + assert_nothing_raised do + SpecialAuthor.joins(book: :subscription).where.not(where_clause) + end + end end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 432c3526a5..38a729d2d4 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -86,6 +86,13 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_nil @member.club end + def test_set_record_after_delete_association + @member.club = nil + @member.club = clubs(:moustache_club) + @member.reload + assert_equal clubs(:moustache_club), @member.club + end + def test_has_one_through_polymorphic assert_equal clubs(:moustache_club), @member.sponsor_club end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 6fe6ee6783..287b3e9ebc 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -443,7 +443,7 @@ class InverseHasManyTests < ActiveRecord::TestCase assert man.equal?(man.interests.first.man), "Two inverses should lead back to the same object that was originally held" assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held" - assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed" + assert_nil man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed" man.name = "Ben Bitdiddle" assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the parent name is changed" man.interests.find(interest.id).man.name = "Alyssa P. Hacker" diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index a223b4338f..26056f6f63 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -220,11 +220,6 @@ class AssociationProxyTest < ActiveRecord::TestCase assert_equal david.projects, david.projects.scope end - test "proxy object is cached" do - david = developers(:david) - assert david.projects.equal?(david.projects) - end - test "inverses get set of subsets of the association" do man = Man.create man.interests.create diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 978dd93fa4..1fc63a49d4 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -3,7 +3,7 @@ require "cases/helper" module ActiveRecord module AttributeMethods class ReadTest < ActiveRecord::TestCase - class FakeColumn < Struct.new(:name) + FakeColumn = Struct.new(:name) do def type; :integer; end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 3dc0c0ce53..4d24a980dc 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -866,6 +866,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert subklass.method_defined?(:id), "subklass is missing id method" end + test "define_attribute_method works with both symbol and string" do + klass = Class.new(ActiveRecord::Base) + + assert_nothing_raised { klass.define_attribute_method(:foo) } + assert_nothing_raised { klass.define_attribute_method("bar") } + end + test "read_attribute with nil should not asplode" do assert_nil Topic.new.read_attribute(nil) end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 6d31b7a091..2203aa1788 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -37,7 +37,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase private def should_be_cool - unless self.first_name == "cool" + unless first_name == "cool" errors.add :first_name, "not cool" end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index a611cc208c..979a59f566 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -98,6 +98,13 @@ class BasicsTest < ActiveRecord::TestCase assert_nil Edge.primary_key end + def test_primary_key_and_references_columns_should_be_identical_type + pk = Author.columns_hash["id"] + ref = Post.columns_hash["author_id"] + + assert_equal pk.bigint?, ref.bigint? + end + def test_many_mutations car = Car.new name: "<3<3<3" car.engines_count = 0 diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index ffd5c1395d..1813534b62 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -13,9 +13,9 @@ require "models/speedometer" require "models/ship_part" require "models/treasure" require "models/developer" +require "models/post" require "models/comment" require "models/rating" -require "models/post" class NumericData < ActiveRecord::Base self.table_name = "numeric_data" @@ -166,14 +166,14 @@ class CalculationsTest < ActiveRecord::TestCase end def test_limit_should_apply_before_count - accounts = Account.limit(3).where("firm_id IS NOT NULL") + accounts = Account.limit(4) assert_equal 3, accounts.count(:firm_id) assert_equal 3, accounts.select(:firm_id).count end def test_limit_should_apply_before_count_arel_attribute - accounts = Account.limit(3).where("firm_id IS NOT NULL") + accounts = Account.limit(4) firm_id_attribute = Account.arel_table[:firm_id] assert_equal 3, accounts.count(firm_id_attribute) diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 11ec6fb2c5..b3c86586d0 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -6,10 +6,6 @@ class CallbackDeveloper < ActiveRecord::Base self.table_name = "developers" class << self - def callback_string(callback_method) - "history << [#{callback_method.to_sym.inspect}, :string]" - end - def callback_proc(callback_method) Proc.new { |model| model.history << [callback_method, :proc] } end @@ -33,7 +29,6 @@ class CallbackDeveloper < ActiveRecord::Base ActiveRecord::Callbacks::CALLBACKS.each do |callback_method| next if callback_method.to_s.start_with?("around_") define_callback_method(callback_method) - ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method)) } send(callback_method, callback_proc(callback_method)) send(callback_method, callback_object(callback_method)) send(callback_method) { |model| model.history << [callback_method, :block] } @@ -44,11 +39,6 @@ class CallbackDeveloper < ActiveRecord::Base end end -class CallbackDeveloperWithFalseValidation < CallbackDeveloper - before_validation proc { |model| model.history << [:before_validation, :returning_false]; false } - before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] } -end - class CallbackDeveloperWithHaltedValidation < CallbackDeveloper before_validation proc { |model| model.history << [:before_validation, :throwing_abort]; throw(:abort) } before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] } @@ -125,11 +115,11 @@ class ContextualCallbacksDeveloper < ActiveRecord::Base after_validation :after_validation_on_create_and_update, on: [ :create, :update ] def before_validation_on_create_and_update - history << "before_validation_on_#{self.validation_context}".to_sym + history << "before_validation_on_#{validation_context}".to_sym end def after_validation_on_create_and_update - history << "after_validation_on_#{self.validation_context}".to_sym + history << "after_validation_on_#{validation_context}".to_sym end def history @@ -137,23 +127,6 @@ class ContextualCallbacksDeveloper < ActiveRecord::Base end end -class CallbackCancellationDeveloper < ActiveRecord::Base - self.table_name = "developers" - - attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called - attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy - - before_save { defined?(@cancel_before_save) ? !@cancel_before_save : false } - before_create { !@cancel_before_create } - before_update { !@cancel_before_update } - before_destroy { !@cancel_before_destroy } - - after_save { @after_save_called = true } - after_update { @after_update_called = true } - after_create { @after_create_called = true } - after_destroy { @after_destroy_called = true } -end - class CallbackHaltedDeveloper < ActiveRecord::Base self.table_name = "developers" @@ -178,7 +151,6 @@ class CallbacksTest < ActiveRecord::TestCase david = CallbackDeveloper.new assert_equal [ [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], @@ -189,12 +161,10 @@ class CallbacksTest < ActiveRecord::TestCase david = CallbackDeveloper.find(1) assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], @@ -206,17 +176,14 @@ class CallbacksTest < ActiveRecord::TestCase david.valid? assert_equal [ [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], - [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], @@ -228,22 +195,18 @@ class CallbacksTest < ActiveRecord::TestCase david.valid? assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], - [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], @@ -254,44 +217,36 @@ class CallbacksTest < ActiveRecord::TestCase david = CallbackDeveloper.create("name" => "David", "salary" => 1000000) assert_equal [ [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], - [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], [ :before_save, :method ], - [ :before_save, :string ], [ :before_save, :proc ], [ :before_save, :object ], [ :before_save, :block ], [ :before_create, :method ], - [ :before_create, :string ], [ :before_create, :proc ], [ :before_create, :object ], [ :before_create, :block ], [ :after_create, :method ], - [ :after_create, :string ], [ :after_create, :proc ], [ :after_create, :object ], [ :after_create, :block ], [ :after_save, :method ], - [ :after_save, :string ], [ :after_save, :proc ], [ :after_save, :object ], [ :after_save, :block ], [ :after_commit, :block ], [ :after_commit, :object ], [ :after_commit, :proc ], - [ :after_commit, :string ], [ :after_commit, :method ] ], david.history end @@ -323,49 +278,40 @@ class CallbacksTest < ActiveRecord::TestCase david.save assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], - [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], [ :before_save, :method ], - [ :before_save, :string ], [ :before_save, :proc ], [ :before_save, :object ], [ :before_save, :block ], [ :before_update, :method ], - [ :before_update, :string ], [ :before_update, :proc ], [ :before_update, :object ], [ :before_update, :block ], [ :after_update, :method ], - [ :after_update, :string ], [ :after_update, :proc ], [ :after_update, :object ], [ :after_update, :block ], [ :after_save, :method ], - [ :after_save, :string ], [ :after_save, :proc ], [ :after_save, :object ], [ :after_save, :block ], [ :after_commit, :block ], [ :after_commit, :object ], [ :after_commit, :proc ], - [ :after_commit, :string ], [ :after_commit, :method ] ], david.history end @@ -399,29 +345,24 @@ class CallbacksTest < ActiveRecord::TestCase david.destroy assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_destroy, :method ], - [ :before_destroy, :string ], [ :before_destroy, :proc ], [ :before_destroy, :object ], [ :before_destroy, :block ], [ :after_destroy, :method ], - [ :after_destroy, :string ], [ :after_destroy, :proc ], [ :after_destroy, :object ], [ :after_destroy, :block ], [ :after_commit, :block ], [ :after_commit, :object ], [ :after_commit, :proc ], - [ :after_commit, :string ], [ :after_commit, :method ] ], david.history end @@ -431,82 +372,16 @@ class CallbacksTest < ActiveRecord::TestCase CallbackDeveloper.delete(david.id) assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], ], david.history end - def test_deprecated_before_save_returning_false - david = ImmutableDeveloper.find(1) - assert_deprecated do - assert david.valid? - assert !david.save - exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } - assert_equal david, exc.record - assert_equal "Failed to save the record", exc.message - end - - david = ImmutableDeveloper.find(1) - david.salary = 10_000_000 - assert !david.valid? - assert !david.save - assert_raise(ActiveRecord::RecordInvalid) { david.save! } - - someone = CallbackCancellationDeveloper.find(1) - someone.cancel_before_save = true - assert_deprecated do - assert someone.valid? - assert !someone.save - end - assert_save_callbacks_not_called(someone) - end - - def test_deprecated_before_create_returning_false - someone = CallbackCancellationDeveloper.new - someone.cancel_before_create = true - assert_deprecated do - assert someone.valid? - assert !someone.save - end - assert_save_callbacks_not_called(someone) - end - - def test_deprecated_before_update_returning_false - someone = CallbackCancellationDeveloper.find(1) - someone.cancel_before_update = true - assert_deprecated do - assert someone.valid? - assert !someone.save - end - assert_save_callbacks_not_called(someone) - end - - def test_deprecated_before_destroy_returning_false - david = ImmutableDeveloper.find(1) - assert_deprecated do - assert !david.destroy - exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } - assert_equal david, exc.record - assert_equal "Failed to destroy the record", exc.message - end - assert_not_nil ImmutableDeveloper.find_by_id(1) - - someone = CallbackCancellationDeveloper.find(1) - someone.cancel_before_destroy = true - assert_deprecated do - assert !someone.destroy - assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } - end - assert !someone.after_destroy_called - end - def assert_save_callbacks_not_called(someone) assert !someone.after_save_called assert !someone.after_create_called @@ -564,50 +439,19 @@ class CallbacksTest < ActiveRecord::TestCase assert !someone.after_destroy_called end - def test_callback_returning_false - david = CallbackDeveloperWithFalseValidation.find(1) - assert_deprecated { david.save } - assert_equal [ - [ :after_find, :method ], - [ :after_find, :string ], - [ :after_find, :proc ], - [ :after_find, :object ], - [ :after_find, :block ], - [ :after_initialize, :method ], - [ :after_initialize, :string ], - [ :after_initialize, :proc ], - [ :after_initialize, :object ], - [ :after_initialize, :block ], - [ :before_validation, :method ], - [ :before_validation, :string ], - [ :before_validation, :proc ], - [ :before_validation, :object ], - [ :before_validation, :block ], - [ :before_validation, :returning_false ], - [ :after_rollback, :block ], - [ :after_rollback, :object ], - [ :after_rollback, :proc ], - [ :after_rollback, :string ], - [ :after_rollback, :method ], - ], david.history - end - def test_callback_throwing_abort david = CallbackDeveloperWithHaltedValidation.find(1) david.save assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], @@ -615,7 +459,6 @@ class CallbacksTest < ActiveRecord::TestCase [ :after_rollback, :block ], [ :after_rollback, :object ], [ :after_rollback, :proc ], - [ :after_rollback, :string ], [ :after_rollback, :method ], ], david.history end diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb index b9c6224425..59ef389326 100644 --- a/activerecord/test/cases/coders/yaml_column_test.rb +++ b/activerecord/test/cases/coders/yaml_column_test.rb @@ -5,46 +5,48 @@ module ActiveRecord module Coders class YAMLColumnTest < ActiveRecord::TestCase def test_initialize_takes_class - coder = YAMLColumn.new(Object) + coder = YAMLColumn.new("attr_name", Object) assert_equal Object, coder.object_class end def test_type_mismatch_on_different_classes_on_dump - coder = YAMLColumn.new(Array) - assert_raises(SerializationTypeMismatch) do + coder = YAMLColumn.new("tags", Array) + error = assert_raises(SerializationTypeMismatch) do coder.dump("a") end + assert_equal %{can't dump `tags`: was supposed to be a Array, but was a String. -- "a"}, error.to_s end def test_type_mismatch_on_different_classes - coder = YAMLColumn.new(Array) - assert_raises(SerializationTypeMismatch) do + coder = YAMLColumn.new("tags", Array) + error = assert_raises(SerializationTypeMismatch) do coder.load "--- foo" end + assert_equal %{can't load `tags`: was supposed to be a Array, but was a String. -- "foo"}, error.to_s end def test_nil_is_ok - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") assert_nil coder.load "--- " end def test_returns_new_with_different_class - coder = YAMLColumn.new SerializationTypeMismatch + coder = YAMLColumn.new("attr_name", SerializationTypeMismatch) assert_equal SerializationTypeMismatch, coder.load("--- ").class end def test_returns_string_unless_starts_with_dash - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") assert_equal "foo", coder.load("foo") end def test_load_handles_other_classes - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") assert_equal [], coder.load([]) end def test_load_doesnt_swallow_yaml_exceptions - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") bad_yaml = "--- {" assert_raises(Psych::SyntaxError) do coder.load(bad_yaml) @@ -52,7 +54,7 @@ module ActiveRecord end def test_load_doesnt_handle_undefined_class_or_module - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") missing_class_yaml = '--- !ruby/object:DoesNotExistAndShouldntEver {}\n' assert_raises(ArgumentError) do coder.load(missing_class_yaml) diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index a65bb89052..d230700119 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -14,71 +14,19 @@ module ActiveRecord # Avoid column definitions in create table statements like: # `title` varchar(255) DEFAULT NULL def test_should_not_include_default_clause_when_default_is_null - column = Column.new("title", nil, SqlTypeMetadata.new(limit: 20)) - column_def = ColumnDefinition.new( - column.name, "string", - column.limit, column.precision, column.scale, column.default, column.null) + column_def = ColumnDefinition.new("title", "string", limit: 20) assert_equal "title varchar(20)", @viz.accept(column_def) end def test_should_include_default_clause_when_default_is_present - column = Column.new("title", "Hello", SqlTypeMetadata.new(limit: 20)) - column_def = ColumnDefinition.new( - column.name, "string", - column.limit, column.precision, column.scale, column.default, column.null) + column_def = ColumnDefinition.new("title", "string", limit: 20, default: "Hello") assert_equal "title varchar(20) DEFAULT 'Hello'", @viz.accept(column_def) end def test_should_specify_not_null_if_null_option_is_false - type_metadata = SqlTypeMetadata.new(limit: 20) - column = Column.new("title", "Hello", type_metadata, false) - column_def = ColumnDefinition.new( - column.name, "string", - column.limit, column.precision, column.scale, column.default, column.null) + column_def = ColumnDefinition.new("title", "string", limit: 20, default: "Hello", null: false) assert_equal "title varchar(20) DEFAULT 'Hello' NOT NULL", @viz.accept(column_def) end - - if current_adapter?(:Mysql2Adapter) - def test_should_set_default_for_mysql_binary_data_types - type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") - binary_column = MySQL::Column.new("title", "a", type) - assert_equal "a", binary_column.default - - type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") - varbinary_column = MySQL::Column.new("title", "a", type) - assert_equal "a", varbinary_column.default - end - - def test_should_be_empty_string_default_for_mysql_binary_data_types - type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") - binary_column = MySQL::Column.new("title", "", type, false) - assert_equal "", binary_column.default - - type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") - varbinary_column = MySQL::Column.new("title", "", type, false) - assert_equal "", varbinary_column.default - end - - def test_should_not_set_default_for_blob_and_text_data_types - text_type = MySQL::TypeMetadata.new(SqlTypeMetadata.new(type: :text)) - - text_column = MySQL::Column.new("title", nil, text_type) - assert_nil text_column.default - - not_null_text_column = MySQL::Column.new("title", nil, text_type, false) - assert_nil not_null_text_column.default - end - - def test_has_default_should_return_false_for_blob_and_text_data_types - binary_type = SqlTypeMetadata.new(sql_type: "blob") - blob_column = MySQL::Column.new("title", nil, binary_type) - assert !blob_column.has_default? - - text_type = SqlTypeMetadata.new(type: :text) - text_column = MySQL::Column.new("title", nil, text_type) - assert !text_column.has_default? - end - end end end end diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb index a625299e8d..63f67a9a16 100644 --- a/activerecord/test/cases/comment_test.rb +++ b/activerecord/test/cases/comment_test.rb @@ -113,7 +113,7 @@ if ActiveRecord::Base.connection.supports_comments? assert_match %r[t\.string\s+"content",\s+comment: "Whoa, content describes itself!"], output assert_match %r[t\.integer\s+"rating",\s+comment: "I am running out of imagination"], output assert_match %r[t\.index\s+.+\s+comment: "\\\"Very important\\\" index that powers all the performance.\\nAnd it's fun!"], output - assert_match %r[t\.index\s+.+\s+name: "idx_obvious",.+\s+comment: "We need to see obvious comments"], output + assert_match %r[t\.index\s+.+\s+name: "idx_obvious",\s+comment: "We need to see obvious comments"], output end def test_schema_dump_omits_blank_comments diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 2c33bf22ab..681399c8bb 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -20,6 +20,66 @@ module ActiveRecord @handler.remove_connection("readonly") end + def test_establish_connection_using_3_levels_config + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } + }, + "another_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/bad-readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/bad-primary.sqlite3" } + }, + "common" => { "adapter" => "sqlite3", "database" => "db/common.sqlite3" } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + @handler.establish_connection(:common) + @handler.establish_connection(:primary) + @handler.establish_connection(:readonly) + + assert_not_nil pool = @handler.retrieve_connection_pool("readonly") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + + assert_not_nil pool = @handler.retrieve_connection_pool("primary") + assert_equal "db/primary.sqlite3", pool.spec.config[:database] + + assert_not_nil pool = @handler.retrieve_connection_pool("common") + assert_equal "db/common.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ENV["RAILS_ENV"] = previous_env + end + + def test_establish_connection_using_two_level_configurations + config = { "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + @handler.establish_connection(:development) + + assert_not_nil pool = @handler.retrieve_connection_pool("development") + assert_equal "db/primary.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + end + + def test_establish_connection_using_top_level_key_in_two_level_config + config = { + "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + "development_readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + @handler.establish_connection(:development_readonly) + + assert_not_nil pool = @handler.retrieve_connection_pool("development_readonly") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + end + def test_retrieve_connection assert @handler.retrieve_connection(@spec_name) end @@ -89,6 +149,41 @@ module ActiveRecord rd.close end + def test_pool_from_any_process_for_uses_most_recent_spec + skip unless current_adapter?(:SQLite3Adapter) + + file = Tempfile.new "lol.sqlite3" + + rd, wr = IO.pipe + rd.binmode + wr.binmode + + pid = fork do + ActiveRecord::Base.configurations["arunit"]["database"] = file.path + ActiveRecord::Base.establish_connection(:arunit) + + pid2 = fork do + wr.write ActiveRecord::Base.connection_config[:database] + wr.close + end + + Process.waitpid pid2 + end + + Process.waitpid pid + + wr.close + + assert_equal file.path, rd.read + + rd.close + ensure + if file + file.close + file.unlink + end + end + def test_a_class_using_custom_pool_and_switching_back_to_primary klass2 = Class.new(Base) { def self.name; "klass2"; end } diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index e2e5445a4e..a348c2d783 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -89,12 +89,20 @@ unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strin end def test_decimal_without_scale - types = %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)} - types.each do |type| - cast_type = @connection.type_map.lookup(type) - - assert_equal :decimal, cast_type.type - assert_equal 2, cast_type.cast(2.1) + if current_adapter?(:OracleAdapter) + { + decimal: %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0)}, + integer: %w{number(2) number(2,0)} + } + else + { decimal: %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)} } + end.each do |expected_type, types| + types.each do |type| + cast_type = @connection.type_map.lookup(type) + + assert_equal expected_type, cast_type.type + assert_equal 2, cast_type.cast(2.1) + end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 42600e53fd..afd0ac2dd4 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -341,14 +341,18 @@ module ActiveRecord end end + class ConnectionTestModel < ActiveRecord::Base + end + def test_connection_notification_is_called payloads = [] subscription = ActiveSupport::Notifications.subscribe("!connection.active_record") do |name, started, finished, unique_id, payload| payloads << payload end - ActiveRecord::Base.establish_connection :arunit + ConnectionTestModel.establish_connection :arunit + assert_equal [:config, :connection_id, :spec_name], payloads[0].keys.sort - assert_equal "primary", payloads[0][:spec_name] + assert_equal "ActiveRecord::ConnectionAdapters::ConnectionPoolTest::ConnectionTestModel", payloads[0][:spec_name] ensure ActiveSupport::Notifications.unsubscribe(subscription) if subscription end @@ -395,7 +399,7 @@ module ActiveRecord all_threads_in_new_connection.wait end rescue Timeout::Error - flunk "pool unable to establish connections concurrently or implementation has " << + flunk "pool unable to establish connections concurrently or implementation has " \ "changed, this test then needs to patch a different :new_connection method" ensure # clean up the threads @@ -437,7 +441,7 @@ module ActiveRecord end end - def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_aquire_all_connections_proceed_anyway + def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_acquire_all_connections_proceed_anyway @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout [:disconnect!, :clear_reloadable_connections!].each do |group_action_method| @pool.with_connection do |connection| diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index c7d0ba32b4..46d7526cc0 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -221,6 +221,15 @@ class CounterCacheTest < ActiveRecord::TestCase assert_equal previously_updated_at, @topic.updated_at end + test "update counters doesn't touch timestamps with touch: []" do + @topic.update_column :updated_at, 5.minutes.ago + previously_updated_at = @topic.updated_at + + Topic.update_counters(@topic.id, replies_count: -1, touch: []) + + assert_equal previously_updated_at, @topic.updated_at + end + test "update counters with touch: true" do assert_touching @topic, :updated_at do Topic.update_counters(@topic.id, replies_count: -1, touch: true) diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index a1c3c5af9c..e4a2f9ee17 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -73,7 +73,7 @@ if subsecond_precision_supported? assert_match %r{t\.datetime\s+"updated_at",\s+precision: 6,\s+null: false$}, output end - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter, :SQLServerAdapter) def test_datetime_precision_with_zero_should_be_dumped @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 0 diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 0e58e65a07..a43c06cd6e 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -301,6 +301,14 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["arr", "arr matey!"], pirate.catchphrase_change end + def test_virtual_attribute_will_change + assert_deprecated do + parrot = Parrot.create!(name: "Ruby") + parrot.send(:attribute_will_change!, :cancel_save_from_callback) + assert parrot.has_changes_to_save? + end + end + def test_association_assignment_changes_foreign_key pirate = Pirate.create!(catchphrase: "jarl") pirate.parrot = Parrot.create!(name: "Lorre") @@ -569,6 +577,7 @@ class DirtyTest < ActiveRecord::TestCase end ensure ActiveRecord::Base.connection.drop_table :testings rescue nil + ActiveRecord::Base.clear_cache! end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index ca22fe969c..deec669935 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -117,7 +117,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal "The Fourth Topic of the day", records[2].title end - def test_find_passing_active_record_object_is_deprecated + def test_find_passing_active_record_object_is_not_permitted assert_raises(ArgumentError) do Topic.find(Topic.last) end @@ -167,7 +167,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal false, relation.exists?(false) end - def test_exists_passing_active_record_object_is_not_permited + def test_exists_passing_active_record_object_is_not_permitted assert_raises(ArgumentError) do Topic.exists?(Topic.new) end @@ -339,6 +339,11 @@ class FinderTest < ActiveRecord::TestCase assert_equal author.post, Post.find_by(author_id: Author.where(id: author)) end + def test_find_by_and_where_consistency_with_active_record_instance + author = authors(:david) + assert_equal Post.where(author_id: author).take, Post.find_by(author_id: author) + end + def test_take assert_equal topics(:first), Topic.take end @@ -858,13 +863,13 @@ class FinderTest < ActiveRecord::TestCase end def test_bind_variables_with_quotes - Company.create("name" => "37signals' go'es agains") - assert Company.where(["name = ?", "37signals' go'es agains"]).first + Company.create("name" => "37signals' go'es against") + assert Company.where(["name = ?", "37signals' go'es against"]).first end def test_named_bind_variables_with_quotes - Company.create("name" => "37signals' go'es agains") - assert Company.where(["name = :name", { name: "37signals' go'es agains" }]).first + Company.create("name" => "37signals' go'es against") + assert Company.where(["name = :name", { name: "37signals' go'es against" }]).first end def test_named_bind_variables diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index dd48053823..afe761cb55 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -7,6 +7,7 @@ require "models/binary" require "models/book" require "models/bulb" require "models/category" +require "models/post" require "models/comment" require "models/company" require "models/computer" @@ -19,7 +20,6 @@ require "models/matey" require "models/other_dog" require "models/parrot" require "models/pirate" -require "models/post" require "models/randomly_named_c1" require "models/reply" require "models/ship" @@ -640,6 +640,8 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase def test_transaction_created_on_connection_notification connection = stub(transaction_open?: false) connection.expects(:begin_transaction).with(joinable: false) + pool = connection.stubs(:pool).returns(ActiveRecord::ConnectionAdapters::ConnectionPool.new(ActiveRecord::Base.connection_pool.spec)) + pool.stubs(:lock_thread=).with(false) fire_connection_notification(connection) end @@ -647,12 +649,16 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase # Mocha is not thread-safe so define our own stub to test connection = Class.new do attr_accessor :rollback_transaction_called + attr_accessor :pool def transaction_open?; true; end def begin_transaction(*args); end def rollback_transaction(*args) @rollback_transaction_called = true end end.new + connection.pool = Class.new do + def lock_thread=(lock_thread); false; end + end.new fire_connection_notification(connection) teardown_fixtures assert(connection.rollback_transaction_called, "Expected <mock connection>#rollback_transaction to be called but was not") @@ -1012,7 +1018,7 @@ end class FixtureClassNamesTest < ActiveRecord::TestCase def setup - @saved_cache = self.fixture_class_names.dup + @saved_cache = fixture_class_names.dup end def teardown diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 1ddcbf0e4f..5a3b8e3fb5 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -6,7 +6,6 @@ require "active_record" require "cases/test_case" require "active_support/dependencies" require "active_support/logger" -require "active_support/core_ext/string/strip" require "support/config" require "support/connection" diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index 155e858822..5a1d066aef 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -101,6 +101,17 @@ class JsonSerializationTest < ActiveRecord::TestCase assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json end + def test_uses_serializable_hash_with_frozen_hash + def @contact.serializable_hash(options = nil) + super({ only: %w(name) }.freeze) + end + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{awesome}, json + assert_no_match %r{age}, json + end + def test_uses_serializable_hash_with_only_option def @contact.serializable_hash(options = nil) super(only: %w(name)) diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 95fb670dac..23095618a4 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -18,7 +18,7 @@ class LockWithoutDefault < ActiveRecord::Base; end class LockWithCustomColumnWithoutDefault < ActiveRecord::Base self.table_name = :lock_without_defaults_cust - self.column_defaults # to test @column_defaults caching. + column_defaults # to test @column_defaults caching. self.locking_column = :custom_lock_version end @@ -536,7 +536,10 @@ unless in_memory_db? Person.transaction do person = Person.find 1 old, person.first_name = person.first_name, "fooman" - person.lock! + # Locking a dirty record is deprecated + assert_deprecated do + person.lock! + end assert_equal old, person.first_name end end diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 8a4242cf1d..ec817a579b 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -101,12 +101,7 @@ module ActiveRecord def test_primary_key_creates_primary_key_column with_change_table do |t| - if current_adapter?(:Mysql2Adapter) - @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, { first: true, auto_increment: true, limit: 8, primary_key: true }] - else - @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, primary_key: true, first: true] - end - + @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, primary_key: true, first: true] t.primary_key :id, first: true end end diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index e5a7412bc3..7a80bfb899 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require "support/schema_dumping_helper" module ActiveRecord class Migration @@ -103,10 +104,118 @@ module ActiveRecord end def test_legacy_migrations_raises_exception_when_inherited - assert_raises(StandardError) do - Class.new(ActiveRecord::Migration) + e = assert_raises(StandardError) do + class_eval("class LegacyMigration < ActiveRecord::Migration; end") end + assert_match(/LegacyMigration < ActiveRecord::Migration\[4\.2\]/, e.message) end end end end + +class LegacyPrimaryKeyTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + self.use_transactional_tests = false + + class LegacyPrimaryKey < ActiveRecord::Base + end + + def setup + @migration = nil + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + end + + def teardown + @migration.migrate(:down) if @migration + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::SchemaMigration.delete_all rescue nil + LegacyPrimaryKey.reset_column_information + end + + def test_legacy_primary_key_should_be_auto_incremented + @migration = Class.new(ActiveRecord::Migration[5.0]) { + def change + create_table :legacy_primary_keys do |t| + t.references :legacy_ref + end + end + }.new + + @migration.migrate(:up) + + legacy_pk = LegacyPrimaryKey.columns_hash["id"] + assert_not legacy_pk.bigint? + assert_not legacy_pk.null + + legacy_ref = LegacyPrimaryKey.columns_hash["legacy_ref_id"] + assert_not legacy_ref.bigint? + + record1 = LegacyPrimaryKey.create! + assert_not_nil record1.id + + record1.destroy + + record2 = LegacyPrimaryKey.create! + assert_not_nil record2.id + assert_operator record2.id, :>, record1.id + end + + def test_legacy_integer_primary_key_should_not_be_auto_incremented + skip if current_adapter?(:SQLite3Adapter) + + @migration = Class.new(ActiveRecord::Migration[5.0]) { + def change + create_table :legacy_primary_keys, id: :integer do |t| + end + end + }.new + + @migration.migrate(:up) + + assert_raises(ActiveRecord::NotNullViolation) do + LegacyPrimaryKey.create! + end + + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", id: :integer, default: nil}, schema + end + + if current_adapter?(:Mysql2Adapter) + def test_legacy_bigint_primary_key_should_be_auto_incremented + @migration = Class.new(ActiveRecord::Migration[5.0]) { + def change + create_table :legacy_primary_keys, id: :bigint + end + }.new + + @migration.migrate(:up) + + legacy_pk = LegacyPrimaryKey.columns_hash["id"] + assert legacy_pk.bigint? + assert legacy_pk.auto_increment? + + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", (?!id: :bigint, default: nil)}, schema + end + else + def test_legacy_bigint_primary_key_should_not_be_auto_incremented + @migration = Class.new(ActiveRecord::Migration[5.0]) { + def change + create_table :legacy_primary_keys, id: :bigint do |t| + end + end + }.new + + @migration.migrate(:up) + + assert_raises(ActiveRecord::NotNullViolation) do + LegacyPrimaryKey.create! + end + + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", id: :bigint, default: nil}, schema + end + end +end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 9be6667aa1..7762d37915 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -1,12 +1,30 @@ require "cases/helper" -require "support/ddl_helper" require "support/schema_dumping_helper" +if ActiveRecord::Base.connection.supports_foreign_keys_in_create? + module ActiveRecord + class Migration + class ForeignKeyInCreateTest < ActiveRecord::TestCase + def test_foreign_keys + foreign_keys = ActiveRecord::Base.connection.foreign_keys("fk_test_has_fk") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "fk_test_has_fk", fk.from_table + assert_equal "fk_test_has_pk", fk.to_table + assert_equal "fk_id", fk.column + assert_equal "pk_id", fk.primary_key + assert_equal "fk_name", fk.name unless current_adapter?(:SQLite3Adapter) + end + end + end + end +end + if ActiveRecord::Base.connection.supports_foreign_keys? module ActiveRecord class Migration class ForeignKeyTest < ActiveRecord::TestCase - include DdlHelper include SchemaDumpingHelper include ActiveSupport::Testing::Stream @@ -29,10 +47,8 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end teardown do - if defined?(@connection) - @connection.drop_table "astronauts", if_exists: true - @connection.drop_table "rockets", if_exists: true - end + @connection.drop_table "astronauts", if_exists: true + @connection.drop_table "rockets", if_exists: true end def test_foreign_keys @@ -76,20 +92,23 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end def test_add_foreign_key_with_non_standard_primary_key - with_example_table @connection, "space_shuttles", "pk BIGINT PRIMARY KEY" do - @connection.add_foreign_key(:astronauts, :space_shuttles, - column: "rocket_id", primary_key: "pk", name: "custom_pk") + @connection.create_table :space_shuttles, id: false, force: true do |t| + t.bigint :pk, primary_key: true + end - foreign_keys = @connection.foreign_keys("astronauts") - assert_equal 1, foreign_keys.size + @connection.add_foreign_key(:astronauts, :space_shuttles, + column: "rocket_id", primary_key: "pk", name: "custom_pk") - fk = foreign_keys.first - assert_equal "astronauts", fk.from_table - assert_equal "space_shuttles", fk.to_table - assert_equal "pk", fk.primary_key + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size - @connection.remove_foreign_key :astronauts, name: "custom_pk" - end + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "space_shuttles", fk.to_table + assert_equal "pk", fk.primary_key + ensure + @connection.remove_foreign_key :astronauts, name: "custom_pk" + @connection.drop_table :space_shuttles end def test_add_on_delete_restrict_foreign_key @@ -229,7 +248,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? create_table("cities") { |t| } create_table("houses") do |t| - t.column :city_id, :bigint + t.references :city end add_foreign_key :houses, :cities, column: "city_id" @@ -261,7 +280,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? create_table(:schools) create_table(:classes) do |t| - t.column :school_id, :bigint + t.references :school end add_foreign_key :classes, :schools end @@ -305,9 +324,11 @@ else @connection.remove_foreign_key :clubs, :categories end - def test_foreign_keys_should_raise_not_implemented - assert_raises NotImplementedError do - @connection.foreign_keys("clubs") + unless current_adapter?(:SQLite3Adapter) + def test_foreign_keys_should_raise_not_implemented + assert_raises NotImplementedError do + @connection.foreign_keys("clubs") + end end end end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 0f975026b8..f10fcf1398 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -31,9 +31,10 @@ module ActiveRecord connection.add_index(table_name, [:foo], name: "old_idx") connection.rename_index(table_name, "old_idx", "new_idx") - # if the adapter doesn't support the indexes call, pick defaults that let the test pass - assert_not connection.index_name_exists?(table_name, "old_idx", false) - assert connection.index_name_exists?(table_name, "new_idx", true) + assert_deprecated do + assert_not connection.index_name_exists?(table_name, "old_idx", false) + assert connection.index_name_exists?(table_name, "new_idx", true) + end end def test_rename_index_too_long @@ -45,8 +46,7 @@ module ActiveRecord } assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message) - # if the adapter doesn't support the indexes call, pick defaults that let the test pass - assert connection.index_name_exists?(table_name, "old_idx", false) + assert connection.index_name_exists?(table_name, "old_idx") end def test_double_add_index @@ -63,7 +63,7 @@ module ActiveRecord def test_add_index_works_with_long_index_names connection.add_index(table_name, "foo", name: good_index_name) - assert connection.index_name_exists?(table_name, good_index_name, false) + assert connection.index_name_exists?(table_name, good_index_name) connection.remove_index(table_name, name: good_index_name) end @@ -75,7 +75,7 @@ module ActiveRecord } assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message) - assert_not connection.index_name_exists?(table_name, too_long_index_name, false) + assert_not connection.index_name_exists?(table_name, too_long_index_name) connection.add_index(table_name, "foo", name: good_index_name) end @@ -83,7 +83,7 @@ module ActiveRecord good_index_name = "x" * connection.index_name_length connection.add_index(table_name, "foo", name: good_index_name, internal: true) - assert connection.index_name_exists?(table_name, good_index_name, false) + assert connection.index_name_exists?(table_name, good_index_name) connection.remove_index(table_name, name: good_index_name) end diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 4957ab8b3d..9418995ea0 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -1,9 +1,9 @@ require "cases/helper" -if ActiveRecord::Base.connection.supports_foreign_keys? +if ActiveRecord::Base.connection.supports_foreign_keys_in_create? module ActiveRecord class Migration - class ReferencesForeignKeyTest < ActiveRecord::TestCase + class ReferencesForeignKeyInCreateTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection @connection.create_table(:testing_parents, force: true) @@ -42,8 +42,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? test "options hash can be passed" do @connection.change_table :testing_parents do |t| - t.bigint :other_id - t.index :other_id, unique: true + t.references :other, index: { unique: true } end @connection.create_table :testings do |t| t.references :testing_parent, foreign_key: { primary_key: :other_id } @@ -61,6 +60,24 @@ if ActiveRecord::Base.connection.supports_foreign_keys? assert_equal([["testings", "testing_parents", "parent_id"]], fks.map { |fk| [fk.from_table, fk.to_table, fk.column] }) end + end + end + end +end + +if ActiveRecord::Base.connection.supports_foreign_keys? + module ActiveRecord + class Migration + class ReferencesForeignKeyTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.drop_table "testings", if_exists: true + @connection.drop_table "testing_parents", if_exists: true + end test "foreign keys cannot be added to polymorphic relations when creating the table" do @connection.create_table :testings do |t| @@ -92,8 +109,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? test "foreign keys accept options when changing the table" do @connection.change_table :testing_parents do |t| - t.bigint :other_id - t.index :other_id, unique: true + t.references :other, index: { unique: true } end @connection.create_table :testings @connection.change_table :testings do |t| @@ -177,18 +193,15 @@ if ActiveRecord::Base.connection.supports_foreign_keys? test "multiple foreign keys can be added to the same table" do @connection.create_table :testings do |t| - t.bigint :col_1 - t.bigint :col_2 - - t.foreign_key :testing_parents, column: :col_1 - t.foreign_key :testing_parents, column: :col_2 + t.references :parent1, foreign_key: { to_table: :testing_parents } + t.references :parent2, foreign_key: { to_table: :testing_parents } end - fks = @connection.foreign_keys("testings") + fks = @connection.foreign_keys("testings").sort_by(&:column) fk_definitions = fks.map { |fk| [fk.from_table, fk.to_table, fk.column] } - assert_equal([["testings", "testing_parents", "col_1"], - ["testings", "testing_parents", "col_2"]], fk_definitions) + assert_equal([["testings", "testing_parents", "parent1_id"], + ["testings", "testing_parents", "parent2_id"]], fk_definitions) end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 082cfd3242..de16ecf442 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -43,10 +43,10 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_prefix = "" ActiveRecord::Base.table_name_suffix = "" - ActiveRecord::Base.connection.initialize_schema_migrations_table - ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}" + ActiveRecord::SchemaMigration.create_table + ActiveRecord::SchemaMigration.delete_all - %w(things awesome_things prefix_things_suffix p_awesome_things_s ).each do |table| + %w(things awesome_things prefix_things_suffix p_awesome_things_s).each do |table| Thing.connection.drop_table(table) rescue nil end Thing.reset_column_information @@ -399,6 +399,7 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.migrations_paths = old_path ENV["RAILS_ENV"] = original_rails_env ENV["RACK_ENV"] = original_rack_env + ActiveRecord::Migrator.up(migrations_path) end def test_migration_sets_internal_metadata_even_when_fully_migrated @@ -425,6 +426,7 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.migrations_paths = old_path ENV["RAILS_ENV"] = original_rails_env ENV["RACK_ENV"] = original_rack_env + ActiveRecord::Migrator.up(migrations_path) end def test_internal_metadata_stores_environment_when_other_data_exists @@ -887,7 +889,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert_equal :datetime, column(:birthdate).type end - protected + private def with_bulk_change_table # Reset columns/indexes cache as we're changing the table @@ -914,7 +916,6 @@ if ActiveRecord::Base.connection.supports_bulk_alter? @indexes ||= Person.connection.indexes("delete_me") end end # AlterTableMigrationsTest - end class CopyMigrationsTest < ActiveRecord::TestCase @@ -1132,4 +1133,13 @@ class CopyMigrationsTest < ActiveRecord::TestCase def test_unknown_migration_version_should_raise_an_argument_error assert_raise(ArgumentError) { ActiveRecord::Migration[1.0] } end + + def test_deprecate_initialize_internal_tables + assert_deprecated { ActiveRecord::Base.connection.initialize_schema_migrations_table } + assert_deprecated { ActiveRecord::Base.connection.initialize_internal_metadata_table } + end + + def test_deprecate_migration_keys + assert_deprecated { ActiveRecord::Base.connection.migration_keys } + end end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index bb9835394b..20d70b75ac 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -45,10 +45,11 @@ class MigratorTest < ActiveRecord::TestCase end def test_migrator_with_duplicate_names - assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do + e = assert_raises(ActiveRecord::DuplicateMigrationNameError) do list = [ActiveRecord::Migration.new("Chunky"), ActiveRecord::Migration.new("Chunky")] ActiveRecord::Migrator.new(:up, list) end + assert_match(/Multiple migrations have the name Chunky/, e.message) end def test_migrator_with_duplicate_versions @@ -290,6 +291,27 @@ class MigratorTest < ActiveRecord::TestCase assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls end + def test_migrator_output_when_running_multiple_migrations + _, migrator = migrator_class(3) + + result = migrator.migrate("valid") + assert_equal(3, result.count) + + # Nothing migrated from duplicate run + result = migrator.migrate("valid") + assert_equal(0, result.count) + + result = migrator.rollback("valid") + assert_equal(1, result.count) + end + + def test_migrator_output_when_running_single_migration + _, migrator = migrator_class(1) + result = migrator.run(:up, "valid", 1) + + assert_equal(1, result.version) + end + def test_migrator_rollback _, migrator = migrator_class(3) diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb index 350a966d40..b9d2acbed2 100644 --- a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb +++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb @@ -120,14 +120,14 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase assert_assignment_affects_records_in_target(:birds_with_add) end - test("Assignment updates records in target when not loaded" + + test("Assignment updates records in target when not loaded" \ " and callback loads target") do assert_not @pirate.birds_with_add_load.loaded? @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes assert_assignment_affects_records_in_target(:birds_with_add_load) end - test("Assignment updates records in target when loaded" + + test("Assignment updates records in target when loaded" \ " and callback loads target") do @pirate.birds_with_add_load.load_target @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 3f1da82cb4..5b7e2fd008 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -139,6 +139,14 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal initial_credit + 2, a1.reload.credit_limit end + def test_increment_updates_timestamps + topic = topics(:first) + topic.update_columns(updated_at: 5.minutes.ago) + previous_updated_at = topic.updated_at + topic.increment!(:replies_count, touch: true) + assert_operator previous_updated_at, :<, topic.reload.updated_at + end + def test_destroy_all conditions = "author_name = 'Mary'" topics_by_mary = Topic.all.merge!(where: conditions, order: "id").to_a @@ -230,6 +238,14 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal 41, accounts(:signals37, :reload).credit_limit end + def test_decrement_updates_timestamps + topic = topics(:first) + topic.update_columns(updated_at: 5.minutes.ago) + previous_updated_at = topic.updated_at + topic.decrement!(:replies_count, touch: true) + assert_operator previous_updated_at, :<, topic.reload.updated_at + end + def test_create topic = Topic.new topic.title = "New Topic" diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 1d72899102..03c8644229 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -7,6 +7,7 @@ require "models/movie" require "models/keyboard" require "models/mixed_case_monkey" require "models/dashboard" +require "models/non_primary_key" class PrimaryKeysTest < ActiveRecord::TestCase fixtures :topics, :subscribers, :movies, :mixed_case_monkeys @@ -89,6 +90,12 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal("John Doe", subscriberReloaded.name) end + def test_id_column_that_is_not_primary_key + NonPrimaryKey.create!(id: 100) + actual = NonPrimaryKey.find_by(id: 100) + assert_match %r{<NonPrimaryKey id: 100}, actual.inspect + end + def test_find_with_more_than_one_string_key assert_equal 2, Subscriber.find(subscribers(:first).nick, subscribers(:second).nick).length end @@ -113,38 +120,45 @@ class PrimaryKeysTest < ActiveRecord::TestCase def test_delete_should_quote_pkey assert_nothing_raised { MixedCaseMonkey.delete(1) } end + def test_update_counters_should_quote_pkey_and_quote_counter_columns assert_nothing_raised { MixedCaseMonkey.update_counters(1, fleaCount: 99) } end + def test_find_with_one_id_should_quote_pkey assert_nothing_raised { MixedCaseMonkey.find(1) } end + def test_find_with_multiple_ids_should_quote_pkey assert_nothing_raised { MixedCaseMonkey.find([1, 2]) } end + def test_instance_update_should_quote_pkey assert_nothing_raised { MixedCaseMonkey.find(1).save } end + def test_instance_destroy_should_quote_pkey assert_nothing_raised { MixedCaseMonkey.find(1).destroy } end - if ActiveRecord::Base.connection.supports_primary_key? - def test_primary_key_returns_value_if_it_exists - klass = Class.new(ActiveRecord::Base) do - self.table_name = "developers" - end + def test_deprecate_supports_primary_key + assert_deprecated { ActiveRecord::Base.connection.supports_primary_key? } + end - assert_equal "id", klass.primary_key + def test_primary_key_returns_value_if_it_exists + klass = Class.new(ActiveRecord::Base) do + self.table_name = "developers" end - def test_primary_key_returns_nil_if_it_does_not_exist - klass = Class.new(ActiveRecord::Base) do - self.table_name = "developers_projects" - end + assert_equal "id", klass.primary_key + end - assert_nil klass.primary_key + def test_primary_key_returns_nil_if_it_does_not_exist + klass = Class.new(ActiveRecord::Base) do + self.table_name = "developers_projects" end + + assert_nil klass.primary_key end def test_quoted_primary_key_after_set_primary_key @@ -224,13 +238,13 @@ class PrimaryKeyWithAutoIncrementTest < ActiveRecord::TestCase @connection.drop_table(:auto_increments, if_exists: true) end - def test_primary_key_with_auto_increment - @connection.create_table(:auto_increments, id: :integer, auto_increment: true, force: true) + def test_primary_key_with_integer + @connection.create_table(:auto_increments, id: :integer, force: true) assert_auto_incremented end - def test_primary_key_with_auto_increment_and_bigint - @connection.create_table(:auto_increments, id: :bigint, auto_increment: true, force: true) + def test_primary_key_with_bigint + @connection.create_table(:auto_increments, id: :bigint, force: true) assert_auto_incremented end @@ -291,6 +305,10 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase t.string :region t.integer :code end + @connection.create_table(:barcodes_reverse, primary_key: ["code", "region"], force: true) do |t| + t.string :region + t.integer :code + end end def teardown @@ -301,6 +319,11 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase assert_equal ["region", "code"], @connection.primary_keys("barcodes") end + def test_composite_primary_key_out_of_order + skip if current_adapter?(:SQLite3Adapter) + assert_equal ["code", "region"], @connection.primary_keys("barcodes_reverse") + end + def test_primary_key_issues_warning model = Class.new(ActiveRecord::Base) do def self.table_name @@ -313,76 +336,106 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase assert_match(/WARNING: Active Record does not support composite primary key\./, warning) end - def test_collectly_dump_composite_primary_key + def test_dumping_composite_primary_key schema = dump_table_schema "barcodes" assert_match %r{create_table "barcodes", primary_key: \["region", "code"\]}, schema end + + def test_dumping_composite_primary_key_out_of_order + skip if current_adapter?(:SQLite3Adapter) + schema = dump_table_schema "barcodes_reverse" + assert_match %r{create_table "barcodes_reverse", primary_key: \["code", "region"\]}, schema + end end -if current_adapter?(:Mysql2Adapter) - class PrimaryKeyIntegerNilDefaultTest < ActiveRecord::TestCase - include SchemaDumpingHelper +class PrimaryKeyIntegerNilDefaultTest < ActiveRecord::TestCase + include SchemaDumpingHelper - self.use_transactional_tests = false + self.use_transactional_tests = false - def setup - @connection = ActiveRecord::Base.connection - @connection.create_table(:int_defaults, id: :integer, default: nil, force: true) - end + def setup + @connection = ActiveRecord::Base.connection + end - def teardown - @connection.drop_table :int_defaults, if_exists: true - end + def teardown + @connection.drop_table :int_defaults, if_exists: true + end - test "primary key with integer allows default override via nil" do - column = @connection.columns(:int_defaults).find { |c| c.name == "id" } - assert_equal :integer, column.type - assert_not column.auto_increment? - end + def test_schema_dump_primary_key_integer_with_default_nil + skip if current_adapter?(:SQLite3Adapter) + @connection.create_table(:int_defaults, id: :integer, default: nil, force: true) + schema = dump_table_schema "int_defaults" + assert_match %r{create_table "int_defaults", id: :integer, default: nil}, schema + end - test "schema dump primary key with int default nil" do - schema = dump_table_schema "int_defaults" - assert_match %r{create_table "int_defaults", id: :integer, default: nil}, schema - end + def test_schema_dump_primary_key_bigint_with_default_nil + @connection.create_table(:int_defaults, id: :bigint, default: nil, force: true) + schema = dump_table_schema "int_defaults" + assert_match %r{create_table "int_defaults", id: :bigint, default: nil}, schema end end -class PrimaryKeyIntegerTest < ActiveRecord::TestCase - include SchemaDumpingHelper +if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) + class PrimaryKeyIntegerTest < ActiveRecord::TestCase + include SchemaDumpingHelper - self.use_transactional_tests = false + self.use_transactional_tests = false - class Widget < ActiveRecord::Base - end + class Widget < ActiveRecord::Base + end - setup do - @connection = ActiveRecord::Base.connection - @connection.create_table(:widgets, force: true) - end + setup do + @connection = ActiveRecord::Base.connection + @pk_type = current_adapter?(:PostgreSQLAdapter) ? :serial : :integer + end - teardown do - @connection.drop_table :widgets, if_exists: true - Widget.reset_column_information - end + teardown do + @connection.drop_table :widgets, if_exists: true + end - if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) - test "schema dump primary key with bigserial" do - schema = dump_table_schema "widgets" - assert_match %r{create_table "widgets", force: :cascade}, schema + test "primary key column type with serial/integer" do + @connection.create_table(:widgets, id: @pk_type, force: true) + column = @connection.columns(:widgets).find { |c| c.name == "id" } + assert_equal :integer, column.type + assert_not column.bigint? end - end - test "primary key column type" do - column_type = Widget.type_for_attribute(Widget.primary_key) - assert_equal :integer, column_type.type + test "primary key with serial/integer are automatically numbered" do + @connection.create_table(:widgets, id: @pk_type, force: true) + widget = Widget.create! + assert_not_nil widget.id + end - if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) - assert_equal 8, column_type.limit + test "schema dump primary key with serial/integer" do + @connection.create_table(:widgets, id: @pk_type, force: true) + schema = dump_table_schema "widgets" + assert_match %r{create_table "widgets", id: :#{@pk_type}, force: :cascade}, schema end if current_adapter?(:Mysql2Adapter) - column = @connection.columns(:widgets).find { |c| c.name == "id" } - assert column.auto_increment? + test "primary key column type with options" do + @connection.create_table(:widgets, id: :primary_key, limit: 4, unsigned: true, force: true) + column = @connection.columns(:widgets).find { |c| c.name == "id" } + assert column.auto_increment? + assert_equal :integer, column.type + assert_not column.bigint? + assert column.unsigned? + + schema = dump_table_schema "widgets" + assert_match %r{create_table "widgets", id: :integer, unsigned: true, force: :cascade}, schema + end + + test "bigint primary key with unsigned" do + @connection.create_table(:widgets, id: :bigint, unsigned: true, force: true) + column = @connection.columns(:widgets).find { |c| c.name == "id" } + assert column.auto_increment? + assert_equal :integer, column.type + assert column.bigint? + assert column.unsigned? + + schema = dump_table_schema "widgets" + assert_match %r{create_table "widgets", id: :bigint, unsigned: true, force: :cascade}, schema + end end end end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 4a49bfe9b1..494663eb04 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -50,32 +50,36 @@ class QueryCacheTest < ActiveRecord::TestCase assert_cache :off end - def test_query_cache_across_threads - ActiveRecord::Base.connection_pool.connections.each do |conn| - assert_cache :off, conn - end - - assert !ActiveRecord::Base.connection.nil? - assert_cache :off - - middleware { - assert_cache :clean - - Task.find 1 - assert_cache :dirty + private def with_temporary_connection_pool + old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) + new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = new_pool - thread_1_connection = ActiveRecord::Base.connection - ActiveRecord::Base.clear_active_connections! - assert_cache :off, thread_1_connection + yield + ensure + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = old_pool + end - started = Concurrent::Event.new - checked = Concurrent::Event.new + def test_query_cache_across_threads + with_temporary_connection_pool do + begin + if in_memory_db? + # Separate connections to an in-memory database create an entirely new database, + # with an empty schema etc, so we just stub out this schema on the fly. + ActiveRecord::Base.connection_pool.with_connection do |connection| + connection.create_table :tasks do |t| + t.datetime :starting + t.datetime :ending + end + end + ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base) + end - thread_2_connection = nil - thread = Thread.new { - thread_2_connection = ActiveRecord::Base.connection + ActiveRecord::Base.connection_pool.connections.each do |conn| + assert_cache :off, conn + end - assert_equal thread_2_connection, thread_1_connection + assert !ActiveRecord::Base.connection.nil? assert_cache :off middleware { @@ -84,29 +88,51 @@ class QueryCacheTest < ActiveRecord::TestCase Task.find 1 assert_cache :dirty - started.set - checked.wait - + thread_1_connection = ActiveRecord::Base.connection ActiveRecord::Base.clear_active_connections! - }.call({}) - } + assert_cache :off, thread_1_connection + + started = Concurrent::Event.new + checked = Concurrent::Event.new + + thread_2_connection = nil + thread = Thread.new { + thread_2_connection = ActiveRecord::Base.connection + + assert_equal thread_2_connection, thread_1_connection + assert_cache :off - started.wait + middleware { + assert_cache :clean - thread_1_connection = ActiveRecord::Base.connection - assert_not_equal thread_1_connection, thread_2_connection - assert_cache :dirty, thread_2_connection - checked.set - thread.join + Task.find 1 + assert_cache :dirty - assert_cache :off, thread_2_connection - }.call({}) + started.set + checked.wait - ActiveRecord::Base.connection_pool.connections.each do |conn| - assert_cache :off, conn + ActiveRecord::Base.clear_active_connections! + }.call({}) + } + + started.wait + + thread_1_connection = ActiveRecord::Base.connection + assert_not_equal thread_1_connection, thread_2_connection + assert_cache :dirty, thread_2_connection + checked.set + thread.join + + assert_cache :off, thread_2_connection + }.call({}) + + ActiveRecord::Base.connection_pool.connections.each do |conn| + assert_cache :off, conn + end + ensure + ActiveRecord::Base.clear_all_connections! + end end - ensure - ActiveRecord::Base.clear_all_connections! end def test_middleware_delegates @@ -260,19 +286,51 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_cache_is_not_available_when_using_a_not_connected_connection - spec_name = Task.connection_specification_name - conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2") - ActiveRecord::Base.connection_handler.establish_connection(conf) - Task.connection_specification_name = "test2" - refute Task.connected? + with_temporary_connection_pool do + spec_name = Task.connection_specification_name + conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2") + ActiveRecord::Base.connection_handler.establish_connection(conf) + Task.connection_specification_name = "test2" + refute Task.connected? - Task.cache do - Task.connection # warmup postgresql connection setup queries - assert_queries(2) { Task.find(1); Task.find(1) } + Task.cache do + begin + if in_memory_db? + Task.connection.create_table :tasks do |t| + t.datetime :starting + t.datetime :ending + end + ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base) + end + Task.connection # warmup postgresql connection setup queries + assert_queries(2) { Task.find(1); Task.find(1) } + ensure + ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name) + Task.connection_specification_name = spec_name + end + end + end + end + + def test_query_cache_executes_new_queries_within_block + ActiveRecord::Base.connection.enable_query_cache! + + # Warm up the cache by running the query + assert_queries(1) do + assert_equal 0, Post.where(title: "test").to_a.count + end + + # Check that if the same query is run again, no queries are executed + assert_queries(0) do + assert_equal 0, Post.where(title: "test").to_a.count + end + + ActiveRecord::Base.connection.uncached do + # Check that new query is executed, avoiding the cache + assert_queries(1) do + assert_equal 0, Post.where(title: "test").to_a.count + end end - ensure - ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name) - Task.connection_specification_name = spec_name end def test_query_cache_doesnt_leak_cached_results_of_rolled_back_queries @@ -328,37 +386,44 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_query_cache_does_not_establish_connection_if_unconnected - ActiveRecord::Base.clear_active_connections! - refute ActiveRecord::Base.connection_handler.active_connections? # sanity check + with_temporary_connection_pool do + ActiveRecord::Base.clear_active_connections! + refute ActiveRecord::Base.connection_handler.active_connections? # sanity check - middleware { - refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup" - }.call({}) + middleware { + refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup" + }.call({}) - refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup" + refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup" + end end def test_query_cache_is_enabled_on_connections_established_after_middleware_runs - ActiveRecord::Base.clear_active_connections! - refute ActiveRecord::Base.connection_handler.active_connections? # sanity check + with_temporary_connection_pool do + ActiveRecord::Base.clear_active_connections! + refute ActiveRecord::Base.connection_handler.active_connections? # sanity check - middleware { - assert ActiveRecord::Base.connection.query_cache_enabled, "QueryCache did not get lazily enabled" - }.call({}) + middleware { + assert ActiveRecord::Base.connection.query_cache_enabled, "QueryCache did not get lazily enabled" + }.call({}) + end end def test_query_caching_is_local_to_the_current_thread - ActiveRecord::Base.clear_active_connections! + with_temporary_connection_pool do + ActiveRecord::Base.clear_active_connections! + + middleware { + assert ActiveRecord::Base.connection_pool.query_cache_enabled + assert ActiveRecord::Base.connection.query_cache_enabled - middleware { - assert ActiveRecord::Base.connection_pool.query_cache_enabled - assert ActiveRecord::Base.connection.query_cache_enabled + Thread.new { + refute ActiveRecord::Base.connection_pool.query_cache_enabled + refute ActiveRecord::Base.connection.query_cache_enabled + }.join + }.call({}) - Thread.new { - refute ActiveRecord::Base.connection_pool.query_cache_enabled - refute ActiveRecord::Base.connection.query_cache_enabled - }.join - }.call({}) + end end private @@ -388,6 +453,10 @@ end class QueryCacheExpiryTest < ActiveRecord::TestCase fixtures :tasks, :posts, :categories, :categories_posts + def teardown + Task.connection.clear_query_cache + end + def test_cache_gets_cleared_after_migration # warm the cache Post.find(1) @@ -463,4 +532,16 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase end end end + + test "threads use the same connection" do + @connection_1 = ActiveRecord::Base.connection.object_id + + thread_a = Thread.new do + @connection_2 = ActiveRecord::Base.connection.object_id + end + + thread_a.join + + assert_equal @connection_1, @connection_2 + end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 0ef51272b9..c1c2efb9c8 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -258,7 +258,9 @@ class ReflectionTest < ActiveRecord::TestCase [Post.reflect_on_association(:first_taggings).scope], [Author.reflect_on_association(:misc_posts).scope] ] - actual = Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain + actual = assert_deprecated do + Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain + end assert_equal expected, actual expected = [ @@ -270,7 +272,9 @@ class ReflectionTest < ActiveRecord::TestCase [], [] ] - actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain + actual = assert_deprecated do + Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain + end assert_equal expected, actual end @@ -331,6 +335,15 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "custom_primary_key", Author.reflect_on_association(:tags_with_primary_key).association_primary_key.to_s # nested end + def test_association_primary_key_type + # Normal Association + assert_equal :integer, Author.reflect_on_association(:posts).association_primary_key_type.type + assert_equal :string, Author.reflect_on_association(:essay).association_primary_key_type.type + + # Through Association + assert_equal :string, Author.reflect_on_association(:essay_category).association_primary_key_type.type + end + def test_association_primary_key_raises_when_missing_primary_key reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } @@ -395,15 +408,27 @@ class ReflectionTest < ActiveRecord::TestCase end def test_through_reflection_scope_chain_does_not_modify_other_reflections - orig_conds = Post.reflect_on_association(:first_blue_tags_2).scope_chain.inspect - Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain - assert_equal orig_conds, Post.reflect_on_association(:first_blue_tags_2).scope_chain.inspect + orig_conds = assert_deprecated do + Post.reflect_on_association(:first_blue_tags_2).scope_chain + end.inspect + assert_deprecated do + Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain + end + assert_equal orig_conds, assert_deprecated { + Post.reflect_on_association(:first_blue_tags_2).scope_chain + }.inspect end def test_symbol_for_class_name assert_equal Client, Firm.reflect_on_association(:unsorted_clients_with_symbol).klass end + def test_class_for_class_name + assert_deprecated do + assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { class_name: Client }, Firm), :validate? + end + end + def test_join_table category = Struct.new(:table_name, :pluralize_table_names).new("categories", true) product = Struct.new(:table_name, :pluralize_table_names).new("products", true) diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index d2382b9bb2..49d4aeafc9 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -32,7 +32,8 @@ module ActiveRecord :exclude?, :find_all, :flat_map, :group_by, :include?, :length, :map, :none?, :one?, :partition, :reject, :reverse, :sample, :second, :sort, :sort_by, :third, - :to_ary, :to_set, :to_xml, :to_yaml, :join + :to_ary, :to_set, :to_xml, :to_yaml, :join, + :in_groups, :in_groups_of, :to_sentence, :to_formatted_s ] ARRAY_DELEGATES.each do |method| diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 2cbbc775ce..4f92f71a09 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -3,7 +3,7 @@ require "models/post" module ActiveRecord class RelationMutationTest < ActiveRecord::TestCase - class FakeKlass < Struct.new(:table_name, :name) + FakeKlass = Struct.new(:table_name, :name) do extend ActiveRecord::Delegation::DelegateCache inherited self @@ -108,7 +108,7 @@ module ActiveRecord end test "#reorder!" do - @relation = self.relation.order("foo") + @relation = relation.order("foo") assert relation.reorder!("bar").equal?(relation) assert_equal ["bar"], relation.order_values diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index d5af0cc9a5..1241bb54a4 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -8,7 +8,7 @@ module ActiveRecord class RelationTest < ActiveRecord::TestCase fixtures :posts, :comments, :authors - class FakeKlass < Struct.new(:table_name, :name) + FakeKlass = Struct.new(:table_name, :name) do extend ActiveRecord::Delegation::DelegateCache inherited self diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 9519fec0c4..0c94e891eb 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1617,9 +1617,9 @@ class RelationTest < ActiveRecord::TestCase assert_equal "David", topic2.reload.author_name end - def test_update_on_relation_passing_active_record_object_is_deprecated + def test_update_on_relation_passing_active_record_object_is_not_permitted topic = Topic.create!(title: "Foo", author_name: nil) - assert_deprecated(/update/) do + assert_raises(ArgumentError) do Topic.where(id: topic.id).update(topic, title: "Bar") end end @@ -1935,6 +1935,18 @@ class RelationTest < ActiveRecord::TestCase assert !Post.all.respond_to?(:by_lifo) end + def test_unscope_with_subquery + p1 = Post.where(id: 1) + p2 = Post.where(id: 2) + + assert_not_equal p1, p2 + + comments = Comment.where(post: p1).unscope(where: :post_id).where(post: p2) + + assert_not_equal p1.first.comments, comments + assert_equal p2.first.comments, comments + end + def test_unscope_removes_binds left = Post.where(id: Arel::Nodes::BindParam.new) column = Post.columns_hash["id"] diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index bea78d2a95..9584318e86 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -178,24 +178,20 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_schema_dumps_index_columns_in_right_order - index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_index/).first.strip + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_index/).first.strip if current_adapter?(:PostgreSQLAdapter) - assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }, using: :btree', index_definition + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition elsif current_adapter?(:Mysql2Adapter) - assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }, using: :btree', index_definition + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }', index_definition else assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition end end def test_schema_dumps_partial_indices - index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_partial_index/).first.strip - if current_adapter?(:PostgreSQLAdapter) - assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition - elsif current_adapter?(:Mysql2Adapter) - assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition - elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? - assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_partial_index/).first.strip + if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)"', index_definition else assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition end @@ -248,9 +244,9 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_schema_dumps_index_type - output = standard_dump - assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output - assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output + output = dump_table_schema "key_tests" + assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext$}, output + assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza"$}, output end end @@ -261,23 +257,34 @@ class SchemaDumperTest < ActiveRecord::TestCase if current_adapter?(:PostgreSQLAdapter) def test_schema_dump_includes_bigint_default - output = standard_dump + output = dump_table_schema "defaults" assert_match %r{t\.bigint\s+"bigint_default",\s+default: 0}, output end def test_schema_dump_includes_limit_on_array_type - output = standard_dump + output = dump_table_schema "bigint_array" assert_match %r{t\.bigint\s+"big_int_data_points\",\s+array: true}, output end def test_schema_dump_allows_array_of_decimal_defaults - output = standard_dump + output = dump_table_schema "bigint_array" assert_match %r{t\.decimal\s+"decimal_array_default",\s+default: \["1.23", "3.45"\],\s+array: true}, output end def test_schema_dump_expression_indices - index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_expression_index/).first.strip - assert_equal 't.index "lower((name)::text)", name: "company_expression_index", using: :btree', index_definition + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip + assert_equal 't.index "lower((name)::text)", name: "company_expression_index"', index_definition + end + + def test_schema_dump_interval_type + output = dump_table_schema "postgresql_times" + assert_match %r{t\.interval\s+"time_interval"$}, output + assert_match %r{t\.interval\s+"scaled_time_interval",\s+precision: 6$}, output + end + + def test_schema_dump_oid_type + output = dump_table_schema "postgresql_oids" + assert_match %r{t\.oid\s+"obj_id"$}, output end if ActiveRecord::Base.connection.supports_extensions? @@ -341,9 +348,9 @@ class SchemaDumperTest < ActiveRecord::TestCase create_table("dogs") do |t| t.column :name, :string - t.column :owner_id, :bigint + t.references :owner t.index [:name] - t.foreign_key :dog_owners, column: "owner_id" if supports_foreign_keys? + t.foreign_key :dog_owners, column: "owner_id" end end def down diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 3a04f4bf7d..14fb2fbbfa 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -10,6 +10,8 @@ require "concurrent/atomic/cyclic_barrier" class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts, :comments + self.use_transactional_tests = false + def test_default_scope expected = Developer.all.merge!(order: "salary DESC").to_a.collect(&:salary) received = DeveloperOrderedBySalary.all.collect(&:salary) diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index a469da0a5b..673392b4c4 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -240,6 +240,20 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal [], light.long_state end + def test_unexpected_serialized_type + Topic.serialize :content, Hash + topic = Topic.create!(content: { zomg: true }) + + Topic.serialize :content, Array + + topic.reload + error = assert_raise(ActiveRecord::SerializationTypeMismatch) do + topic.content + end + expected = "can't load `content`: was supposed to be a Array, but was a Hash. -- {:zomg=>true}" + assert_equal expected, error.to_s + end + def test_serialized_column_should_unserialize_after_update_column t = Topic.create(content: "first") assert_equal("first", t.content) diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index f45f63c68e..fab3648564 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -105,5 +105,31 @@ module ActiveRecord refute_equal book, other_book end + + def test_find_by_does_not_use_statement_cache_if_table_name_is_changed + book = Book.create(name: "my book") + + Book.find_by(name: book.name) # warming the statement cache. + + # changing the table name should change the query that is not cached. + Book.table_name = :birds + assert_nil Book.find_by(name: book.name) + ensure + Book.table_name = :books + end + + def test_find_does_not_use_statement_cache_if_table_name_is_changed + book = Book.create(name: "my book") + + Book.find(book.id) # warming the statement cache. + + # changing the table name should change the query that is not cached. + Book.table_name = :birds + assert_raise ActiveRecord::RecordNotFound do + Book.find(book.id) + end + ensure + Book.table_name = :books + end end end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index f7c53b5801..5653fd83fd 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -100,6 +100,8 @@ module ActiveRecord @configurations = { "development" => { "database" => "my-db" } } ActiveRecord::Base.stubs(:configurations).returns(@configurations) + # To refrain from connecting to a newly created empty DB in sqlite3_mem tests + ActiveRecord::Base.connection_handler.stubs(:establish_connection) end def test_ignores_configurations_without_databases diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index 03f6c234e8..09c585167e 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -68,7 +68,7 @@ if subsecond_precision_supported? assert_match %r{t\.time\s+"finish",\s+precision: 6$}, output end - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter, :SQLServerAdapter) def test_time_precision_with_zero_should_be_dumped @connection.create_table(:foos, force: true) do |t| t.time :start, precision: 0 diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 7766a74612..39b40e3411 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -422,7 +422,7 @@ class TimestampTest < ActiveRecord::TestCase self.table_name = "people" before_create do - self.born_at = self.created_at + self.born_at = created_at end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 9b1cca8583..111495c481 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -206,16 +206,6 @@ class TransactionTest < ActiveRecord::TestCase assert_equal posts_count, author.posts.reload.size end - def test_cancellation_from_returning_false_in_before_filter - def @first.before_save_for_transaction - false - end - - assert_deprecated do - @first.save - end - end - def test_cancellation_from_before_destroy_rollbacks_in_destroy add_cancelling_before_destroy_with_db_side_effect_to_topic @first nbooks_before_destroy = Book.count @@ -279,7 +269,11 @@ class TransactionTest < ActiveRecord::TestCase e = assert_raises(RuntimeError) { new_topic.save } assert_equal "Make the transaction rollback", e.message assert_equal new_record_snapshot, !new_topic.persisted?, "The topic should have its old persisted value" - assert_equal id_snapshot, new_topic.id, "The topic should have its old id" + if id_snapshot.nil? + assert_nil new_topic.id, "The topic should have its old id" + else + assert_equal id_snapshot, new_topic.id, "The topic should have its old id" + end assert_equal id_present, new_topic.has_attribute?(Topic.primary_key) end end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 6d22638592..277280b42e 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -372,7 +372,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase e2 = Event.create(title: "abcdefgh") assert_not e2.valid?, "Created an event whose title is not unique" - elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter) + elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter) assert_raise(ActiveRecord::ValueTooLong) do Event.create(title: "abcdefgh") end @@ -391,7 +391,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase e2 = Event.create(title: "一二三四五六七八") assert_not e2.valid?, "Created an event whose title is not unique" - elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter) + elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter) assert_raise(ActiveRecord::ValueTooLong) do Event.create(title: "一二三四五六七八") end diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index d055968a56..07288568e8 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -154,7 +154,7 @@ if ActiveRecord::Base.connection.supports_views? end # sqlite dose not support CREATE, INSERT, and DELETE for VIEW - if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter) class UpdateableViewTest < ActiveRecord::TestCase self.use_transactional_tests = false fixtures :books @@ -200,7 +200,7 @@ if ActiveRecord::Base.connection.supports_views? end end end - end # end of `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)` + end # end of `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)` end # end of `if ActiveRecord::Base.connection.supports_views?` if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && diff --git a/activerecord/test/fixtures/subscribers.yml b/activerecord/test/fixtures/subscribers.yml index c6a8c2fa24..0f6e0cd48e 100644 --- a/activerecord/test/fixtures/subscribers.yml +++ b/activerecord/test/fixtures/subscribers.yml @@ -6,6 +6,6 @@ second: nick: webster132 name: David Heinemeier Hansson -thrid: +third: nick: swistak name: Marcin Raczkowski
\ No newline at end of file diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 4561b3132b..20e37710e7 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -216,14 +216,12 @@ class Account < ActiveRecord::Base validate :check_empty_credit_limit - protected + private def check_empty_credit_limit errors.add("credit_limit", :blank) if credit_limit.blank? end - private - def private_method "Sir, yes sir!" end diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb index 60af7c2247..3d40cb1ace 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -56,7 +56,7 @@ class GpsLocation end def ==(other) - self.latitude == other.latitude && self.longitude == other.longitude + latitude == other.latitude && longitude == other.longitude end end diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index ea4f719517..654830ba11 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -260,3 +260,9 @@ class CachedDeveloper < ActiveRecord::Base self.table_name = "developers" self.cache_timestamp_format = :number end + +class DeveloperWithIncorrectlyOrderedHasManyThrough < ActiveRecord::Base + self.table_name = "developers" + has_many :companies, through: :contracts + has_many :contracts, foreign_key: :developer_id +end diff --git a/activerecord/test/models/family.rb b/activerecord/test/models/family.rb new file mode 100644 index 0000000000..5ae5a78c95 --- /dev/null +++ b/activerecord/test/models/family.rb @@ -0,0 +1,4 @@ +class Family < ActiveRecord::Base + has_many :family_trees, -> { where(token: nil) } + has_many :members, through: :family_trees +end diff --git a/activerecord/test/models/family_tree.rb b/activerecord/test/models/family_tree.rb new file mode 100644 index 0000000000..cd9829fedd --- /dev/null +++ b/activerecord/test/models/family_tree.rb @@ -0,0 +1,4 @@ +class FamilyTree < ActiveRecord::Base + belongs_to :member, class_name: "User", foreign_key: "member_id" + belongs_to :family +end diff --git a/activerecord/test/models/non_primary_key.rb b/activerecord/test/models/non_primary_key.rb new file mode 100644 index 0000000000..1cafb09608 --- /dev/null +++ b/activerecord/test/models/non_primary_key.rb @@ -0,0 +1,2 @@ +class NonPrimaryKey < ActiveRecord::Base +end diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 5009f8f54b..4fbd986e40 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -11,7 +11,7 @@ class Project < ActiveRecord::Base after_add: Proc.new { |o, r| o.developers_log << "after_adding#{r.id || '<new>'}" }, before_remove: Proc.new { |o, r| o.developers_log << "before_removing#{r.id}" }, after_remove: Proc.new { |o, r| o.developers_log << "after_removing#{r.id}" } - has_and_belongs_to_many :well_payed_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, class_name: "Developer" + has_and_belongs_to_many :well_paid_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, class_name: "Developer" belongs_to :firm has_one :lead_developer, through: :firm, inverse_of: :contracted_projects diff --git a/activerecord/test/models/user.rb b/activerecord/test/models/user.rb index 47649e0a77..5089a795f4 100644 --- a/activerecord/test/models/user.rb +++ b/activerecord/test/models/user.rb @@ -5,8 +5,12 @@ class User < ActiveRecord::Base has_secure_token :auth_token has_and_belongs_to_many :jobs_pool, - class_name: Job, + class_name: "Job", join_table: "jobs_pool" + + has_one :family_tree, -> { where(token: nil) }, foreign_key: "member_id" + has_one :family, through: :family_tree + has_many :family_members, through: :family, source: :members end class UserWithNotification < User diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 15ba2d67ab..860c63b27c 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -23,16 +23,24 @@ ActiveRecord::Schema.define do t.string :char2, limit: 50, default: "a varchar field" t.text :char3, default: "a text field" t.bigint :bigint_default, default: -> { "0::bigint" } - t.text :multiline_default, default: '--- [] + t.text :multiline_default, default: "--- [] -' +" end - %w(postgresql_times postgresql_oids postgresql_timestamp_with_zones - postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| - drop_table table_name, if_exists: true + create_table :postgresql_times, force: true do |t| + t.interval :time_interval + t.interval :scaled_time_interval, precision: 6 end + create_table :postgresql_oids, force: true do |t| + t.oid :obj_id + end + + drop_table "postgresql_timestamp_with_zones", if_exists: true + drop_table "postgresql_partitioned_table", if_exists: true + drop_table "postgresql_partitioned_table_parent", if_exists: true + execute "DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE" execute "CREATE SEQUENCE companies_nonstd_seq START 101 OWNED BY companies.id" execute "ALTER TABLE companies ALTER COLUMN id SET DEFAULT nextval('companies_nonstd_seq')" @@ -45,21 +53,6 @@ ActiveRecord::Schema.define do end execute <<_SQL - CREATE TABLE postgresql_times ( - id SERIAL PRIMARY KEY, - time_interval INTERVAL, - scaled_time_interval INTERVAL(6) - ); -_SQL - - execute <<_SQL - CREATE TABLE postgresql_oids ( - id SERIAL PRIMARY KEY, - obj_id OID - ); -_SQL - - execute <<_SQL CREATE TABLE postgresql_timestamp_with_zones ( id SERIAL PRIMARY KEY, time TIMESTAMP WITH TIME ZONE diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index ba6f5de894..08bef08abc 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -54,8 +54,8 @@ ActiveRecord::Schema.define do create_table :authors, force: true do |t| t.string :name, null: false - t.bigint :author_address_id - t.integer :author_address_extra_id + t.references :author_address + t.references :author_address_extra t.string :organization_id t.string :owned_essay_id end @@ -88,7 +88,7 @@ ActiveRecord::Schema.define do end create_table :books, force: true do |t| - t.integer :author_id + t.references :author t.string :format t.column :name, :string t.column :status, :integer, default: 0 @@ -201,7 +201,7 @@ ActiveRecord::Schema.define do t.integer :account_id t.string :description, default: "" t.index [:firm_id, :type, :rating], name: "company_index", length: { type: 10 }, order: { rating: :desc } - t.index [:firm_id, :type], name: "company_partial_index", where: "rating > 10" + t.index [:firm_id, :type], name: "company_partial_index", where: "(rating > 10)" t.index :name, name: "company_name_index", using: :btree t.index "lower(name)", name: "company_expression_index" if supports_expression_index? end @@ -306,7 +306,7 @@ ActiveRecord::Schema.define do end create_table :engines, force: true do |t| - t.bigint :car_id + t.references :car, index: false end create_table :entrants, force: true do |t| @@ -329,6 +329,15 @@ ActiveRecord::Schema.define do create_table :eyes, force: true do |t| end + create_table :families, force: true do |t| + end + + create_table :family_trees, force: true do |t| + t.references :family + t.references :member + t.string :token + end + create_table :funny_jokes, force: true do |t| t.string :name end @@ -655,8 +664,8 @@ ActiveRecord::Schema.define do end create_table :posts, force: true do |t| - t.integer :author_id - t.string :title, null: false + t.references :author + t.string :title, null: false # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in # Oracle SELECT WHERE clause which causes many unit test failures if current_adapter?(:OracleAdapter) @@ -1005,16 +1014,14 @@ ActiveRecord::Schema.define do create_table :records, force: true do |t| end - if supports_foreign_keys? - # fk_test_has_fk should be before fk_test_has_pk - create_table :fk_test_has_fk, force: true do |t| - t.bigint :fk_id, null: false + disable_referential_integrity do + create_table :fk_test_has_pk, primary_key: "pk_id", force: :cascade do |t| end - create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t| + create_table :fk_test_has_fk, force: true do |t| + t.references :fk, null: false + t.foreign_key :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" end - - add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" end create_table :overloaded_types, force: true do |t| @@ -1032,6 +1039,10 @@ ActiveRecord::Schema.define do create_table :test_with_keyword_column_name, force: true do |t| t.string :desc end + + create_table :non_primary_keys, force: true, id: false do |t| + t.integer :id + end end Course.connection.create_table :courses, force: true do |t| diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb deleted file mode 100644 index cc7c36fe2b..0000000000 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ /dev/null @@ -1,18 +0,0 @@ -ActiveRecord::Schema.define do - execute "DROP TABLE fk_test_has_fk" rescue nil - execute "DROP TABLE fk_test_has_pk" rescue nil - execute <<_SQL - CREATE TABLE 'fk_test_has_pk' ( - 'pk_id' INTEGER NOT NULL PRIMARY KEY - ); -_SQL - - execute <<_SQL - CREATE TABLE 'fk_test_has_fk' ( - 'id' INTEGER NOT NULL PRIMARY KEY, - 'fk_id' INTEGER NOT NULL, - - FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id') - ); -_SQL -end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 873a39dbf6..7962427032 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,75 @@ +* Cache `ActiveSupport::TimeWithZone#to_datetime` before freezing. + + *Adam Rice* + +* Deprecate `.halt_callback_chains_on_return_false`. + + *Rafael Mendonça França* + +* Remove deprecated behavior that halts callbacks when the return is false. + + *Rafael Mendonça França* + +* Deprecate passing string to `:if` and `:unless` conditional options + on `set_callback` and `skip_callback`. + + *Ryuta Kamizono* + +* Raise `ArgumentError` when passing string to define callback. + + *Ryuta Kamizono* + +* Updated Unicode version to 9.0.0 + + Now we can handle new emojis such like "👩👩👧👦" ("\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}"). + + version 8.0.0 + + "👩👩👧👦".mb_chars.grapheme_length # => 4 + "👩👩👧👦".mb_chars.reverse # => "👦👧👩👩" + + version 9.0.0 + + "👩👩👧👦".mb_chars.grapheme_length # => 1 + "👩👩👧👦".mb_chars.reverse # => "👩👩👧👦" + + *Fumiaki MATSUSHIMA* + +* Changed `ActiveSupport::Inflector#transliterate` to raise `ArgumentError` when it receives + anything except a string. + + *Kevin McPhillips* + +* Fixed bugs that `StringInquirer#respond_to_missing?` and + `ArrayInquirer#respond_to_missing?` do not fallback to `super`. + + *Akira Matsuda* + +* Fix inconsistent results when parsing large durations and constructing durations from code + + ActiveSupport::Duration.parse('P3Y') == 3.years # It should be true + + Duration parsing made independent from any moment of time: + Fixed length in seconds is assigned to each duration part during parsing. + + Changed duration of months and years in seconds to more accurate and logical: + + 1. The value of 365.2425 days in Gregorian year is more accurate + as it accounts for every 400th non-leap year. + + 2. Month's length is bound to year's duration, which makes + sensible comparisons like `12.months == 1.year` to be `true` + and nonsensical ones like `30.days == 1.month` to be `false`. + + Calculations on times and dates with durations shouldn't be affected as + duration's numeric value isn't used in calculations, only parts are used. + + Methods on `Numeric` like `2.days` now use these predefined durations + to avoid duplicating of duration constants through the codebase and + eliminate creation of intermediate durations. + + *Andrey Novikov, Andrew White* + * Change return value of `Rational#duplicable?`, `ComplexClass#duplicable?` to false. @@ -9,76 +81,76 @@ *Yuji Yaginuma* -* Remove deprecated class `ActiveSupport::Concurrency::Latch` +* Remove deprecated class `ActiveSupport::Concurrency::Latch`. *Andrew White* -* Remove deprecated separator argument from `parameterize` +* Remove deprecated separator argument from `parameterize`. *Andrew White* -* Remove deprecated method `Numeric#to_formatted_s` +* Remove deprecated method `Numeric#to_formatted_s`. *Andrew White* -* Remove deprecated method `alias_method_chain` +* Remove deprecated method `alias_method_chain`. *Andrew White* -* Remove deprecated constant `MissingSourceFile` +* Remove deprecated constant `MissingSourceFile`. *Andrew White* * Remove deprecated methods `Module.qualified_const_defined?`, - `Module.qualified_const_get` and `Module.qualified_const_set` + `Module.qualified_const_get` and `Module.qualified_const_set`. *Andrew White* -* Remove deprecated `:prefix` option from `number_to_human_size` +* Remove deprecated `:prefix` option from `number_to_human_size`. *Andrew White* -* Remove deprecated method `ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default` +* Remove deprecated method `ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default`. *Andrew White* -* Remove deprecated file `active_support/core_ext/time/marshal.rb` +* Remove deprecated file `active_support/core_ext/time/marshal.rb`. *Andrew White* -* Remove deprecated file `active_support/core_ext/struct.rb` +* Remove deprecated file `active_support/core_ext/struct.rb`. *Andrew White* -* Remove deprecated file `active_support/core_ext/module/method_transplanting.rb` +* Remove deprecated file `active_support/core_ext/module/method_transplanting.rb`. *Andrew White* -* Remove deprecated method `Module.local_constants` +* Remove deprecated method `Module.local_constants`. *Andrew White* -* Remove deprecated file `active_support/core_ext/kernel/debugger.rb` +* Remove deprecated file `active_support/core_ext/kernel/debugger.rb`. *Andrew White* -* Remove deprecated method `ActiveSupport::Cache::Store#namespaced_key` +* Remove deprecated method `ActiveSupport::Cache::Store#namespaced_key`. *Andrew White* -* Remove deprecated method `ActiveSupport::Cache::Strategy::LocalCache::LocalStore#set_cache_value` +* Remove deprecated method `ActiveSupport::Cache::Strategy::LocalCache::LocalStore#set_cache_value`. *Andrew White* -* Remove deprecated method `ActiveSupport::Cache::MemCacheStore#escape_key` +* Remove deprecated method `ActiveSupport::Cache::MemCacheStore#escape_key`. *Andrew White* -* Remove deprecated method `ActiveSupport::Cache::FileStore#key_file_path` +* Remove deprecated method `ActiveSupport::Cache::FileStore#key_file_path`. *Andrew White* -* Ensure duration parsing is consistent across DST changes +* Ensure duration parsing is consistent across DST changes. Previously `ActiveSupport::Duration.parse` used `Time.current` and `Time#advance` to calculate the number of seconds in the duration diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables index 5d912f375c..aa36a01b5b 100755 --- a/activesupport/bin/generate_tables +++ b/activesupport/bin/generate_tables @@ -8,6 +8,7 @@ end require "open-uri" require "tmpdir" +require "fileutils" module ActiveSupport module Multibyte @@ -101,9 +102,10 @@ module ActiveSupport def parse SOURCES.each do |type, url| - filename = File.join(Dir.tmpdir, "#{url.split('/').last}") + filename = File.join(Dir.tmpdir, UNICODE_VERSION, "#{url.split('/').last}") unless File.exist?(filename) $stderr.puts "Downloading #{url.split('/').last}" + FileUtils.mkdir_p(File.dirname(filename)) File.open(filename, "wb") do |target| open(url) do |source| source.each_line { |line| target.write line } diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 267fa755c6..03e3ce821a 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -80,11 +80,15 @@ module ActiveSupport cattr_accessor :test_order # :nodoc: def self.halt_callback_chains_on_return_false - Callbacks.halt_and_display_warning_on_return_false + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport.halt_callback_chains_on_return_false is deprecated and will be removed in Rails 5.2. + MSG end def self.halt_callback_chains_on_return_false=(value) - Callbacks.halt_and_display_warning_on_return_false = value + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveSupport.halt_callback_chains_on_return_false= is deprecated and will be removed in Rails 5.2. + MSG end def self.to_time_preserves_timezone diff --git a/activesupport/lib/active_support/array_inquirer.rb b/activesupport/lib/active_support/array_inquirer.rb index 85122e39b2..befa1746c6 100644 --- a/activesupport/lib/active_support/array_inquirer.rb +++ b/activesupport/lib/active_support/array_inquirer.rb @@ -20,7 +20,7 @@ module ActiveSupport # variants.any?(:phone, :tablet) # => true # variants.any?('phone', 'desktop') # => true # variants.any?(:desktop, :watch) # => false - def any?(*candidates, &block) + def any?(*candidates) if candidates.none? super else @@ -32,7 +32,7 @@ module ActiveSupport private def respond_to_missing?(name, include_private = false) - name[-1] == "?" + (name[-1] == "?") || super end def method_missing(name, *args) diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index 169a58ecd1..e47c90597f 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -12,7 +12,7 @@ module ActiveSupport # is to exclude the output of a noisy library from the backtrace, so that you # can focus on the rest. # - # bc = BacktraceCleaner.new + # bc = ActiveSupport::BacktraceCleaner.new # bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix # bc.add_silencer { |line| line =~ /puma|rubygems/ } # skip any lines from puma or rubygems # bc.clean(exception.backtrace) # perform the cleanup diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index d1bbd2f405..4d8c2046e8 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -6,7 +6,6 @@ require "active_support/core_ext/numeric/bytes" require "active_support/core_ext/numeric/time" require "active_support/core_ext/object/to_param" require "active_support/core_ext/string/inflections" -require "active_support/core_ext/string/strip" module ActiveSupport # See ActiveSupport::Cache::Store for documentation. @@ -71,8 +70,8 @@ module ActiveSupport # each of elements in the array will be turned into parameters/keys and # concatenated into a single key. For example: # - # expand_cache_key([:foo, :bar]) # => "foo/bar" - # expand_cache_key([:foo, :bar], "namespace") # => "namespace/foo/bar" + # ActiveSupport::Cache.expand_cache_key([:foo, :bar]) # => "foo/bar" + # ActiveSupport::Cache.expand_cache_key([:foo, :bar], "namespace") # => "namespace/foo/bar" # # The +key+ argument can also respond to +cache_key+ or +to_param+. def expand_cache_key(key, namespace = nil) diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index e6c79f2a38..ea71569ca8 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -4,7 +4,6 @@ require "active_support/core_ext/array/extract_options" require "active_support/core_ext/class/attribute" require "active_support/core_ext/kernel/reporting" require "active_support/core_ext/kernel/singleton_class" -require "active_support/core_ext/module/attribute_accessors" require "active_support/core_ext/string/filters" require "active_support/deprecation" require "thread" @@ -69,12 +68,6 @@ module ActiveSupport CALLBACK_FILTER_TYPES = [:before, :after, :around] - # If true, Active Record and Active Model callbacks returning +false+ will - # halt the entire callback chain and display a deprecation message. - # If false, callback chains will only be halted by calling +throw :abort+. - # Defaults to +true+. - mattr_accessor(:halt_and_display_warning_on_return_false, instance_writer: false) { true } - # Runs the callbacks for the given event. # # Calls the before and around callbacks in the order they were set, yields @@ -286,9 +279,9 @@ module ActiveSupport class Callback #:nodoc:# def self.build(chain, filter, kind, options) if filter.is_a?(String) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing string to define callback is deprecated and will be removed - in Rails 5.1 without replacement. + raise ArgumentError, <<-MSG.squish + Passing string to define a callback is not supported. See the `.set_callback` + documentation to see supported values. MSG end @@ -643,9 +636,8 @@ module ActiveSupport # set_callback :save, :before_method # # The callback can be specified as a symbol naming an instance method; as a - # proc, lambda, or block; as a string to be instance evaluated(deprecated); or as an - # object that responds to a certain method determined by the <tt>:scope</tt> - # argument to +define_callbacks+. + # proc, lambda, or block; or as an object that responds to a certain method + # determined by the <tt>:scope</tt> argument to +define_callbacks+. # # If a proc, lambda, or block is given, its body is evaluated in the context # of the current object. It can also optionally accept the current object as @@ -659,16 +651,24 @@ module ActiveSupport # # ===== Options # - # * <tt>:if</tt> - A symbol, a string or an array of symbols and strings, + # * <tt>:if</tt> - A symbol, a string (deprecated) or an array of symbols, # each naming an instance method or a proc; the callback will be called # only when they all return a true value. - # * <tt>:unless</tt> - A symbol, a string or an array of symbols and - # strings, each naming an instance method or a proc; the callback will - # be called only when they all return a false value. + # * <tt>:unless</tt> - A symbol, a string (deprecated) or an array of symbols, + # each naming an instance method or a proc; the callback will be called + # only when they all return a false value. # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the # existing chain rather than appended. def set_callback(name, *filter_list, &block) type, filters, options = normalize_callback_params(filter_list, block) + + if options[:if].is_a?(String) || options[:unless].is_a?(String) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing string to :if and :unless conditional options is deprecated + and will be removed in Rails 5.2 without replacement. + MSG + end + self_chain = get_callbacks name mapped = filters.map do |filter| Callback.build(self_chain, filter, type, options) @@ -692,6 +692,14 @@ module ActiveSupport # already been set (unless the <tt>:raise</tt> option is set to <tt>false</tt>). def skip_callback(name, *filter_list, &block) type, filters, options = normalize_callback_params(filter_list, block) + + if options[:if].is_a?(String) || options[:unless].is_a?(String) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing string to :if and :unless conditional options is deprecated + and will be removed in Rails 5.2 without replacement. + MSG + end + options[:raise] = true unless options.key?(:raise) __update_callbacks(name) do |target, chain| @@ -841,30 +849,6 @@ module ActiveSupport def set_callbacks(name, callbacks) # :nodoc: self.__callbacks = __callbacks.merge(name.to_sym => callbacks) end - - def deprecated_false_terminator # :nodoc: - Proc.new do |target, result_lambda| - terminate = true - catch(:abort) do - result = result_lambda.call if result_lambda.is_a?(Proc) - if Callbacks.halt_and_display_warning_on_return_false && result == false - display_deprecation_warning_for_false_terminator - else - terminate = false - end - end - terminate - end - end - - private - - def display_deprecation_warning_for_false_terminator - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in Rails 5.1. - To explicitly halt the callback chain, please use `throw :abort` instead. - MSG - end end end end diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb index 52706c3d7a..f397f658f3 100644 --- a/activesupport/lib/active_support/core_ext.rb +++ b/activesupport/lib/active_support/core_ext.rb @@ -1,4 +1,3 @@ -DEPRECATED_FILES = ["#{File.dirname(__FILE__)}/core_ext/struct.rb"] -(Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"] - DEPRECATED_FILES).each do |path| +(Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"]).each do |path| require path end diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index 37d833887a..fca33c9d69 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -31,7 +31,7 @@ class Array # # people = ["David", "Rafael", "Aaron", "Todd"] # people.without "Aaron", "Todd" - # => ["David", "Rafael"] + # # => ["David", "Rafael"] # # Note: This is an optimization of `Enumerable#without` that uses `Array#-` # instead of `Array#reject` for performance reasons. diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index ea9d85f6e3..0d798e5c4e 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -89,7 +89,7 @@ class Array # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]] # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]] def split(value = nil) - arr = self.dup + arr = dup result = [] if block_given? while (idx = arr.index { |i| yield i }) diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index 2e64097933..d6f60cac04 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -133,7 +133,7 @@ class Date # Allow Date to be compared with Time by converting to DateTime and relying on the <=> from there. def compare_with_coercion(other) if other.is_a?(Time) - self.to_datetime <=> other + to_datetime <=> other else compare_without_coercion(other) end diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index ae08fd90c1..90d7d2947f 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -12,7 +12,7 @@ module Enumerable # # [5, 15, 10].sum # => 30 # ['foo', 'bar'].sum # => "foobar" - # [[1, 2], [3, 1, 5]].sum => [1, 2, 3, 1, 5] + # [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5] # # The default sum of an empty list is zero. You can override this default: # @@ -29,9 +29,9 @@ module Enumerable # Convert an enumerable to a hash. # # people.index_by(&:login) - # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...} + # # => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...} # people.index_by { |person| "#{person.first_name} #{person.last_name}" } - # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...} + # # => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...} def index_by if block_given? result = {} @@ -67,10 +67,10 @@ module Enumerable # Returns a copy of the enumerable without the specified elements. # # ["David", "Rafael", "Aaron", "Todd"].without "Aaron", "Todd" - # => ["David", "Rafael"] + # # => ["David", "Rafael"] # # {foo: 1, bar: 2, baz: 3}.without :bar - # => {foo: 1, baz: 3} + # # => {foo: 1, baz: 3} def without(*elements) reject { |element| elements.include?(element) } end @@ -78,10 +78,10 @@ module Enumerable # Convert an enumerable to an array based on the given key. # # [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) - # => ["David", "Rafael", "Aaron"] + # # => ["David", "Rafael", "Aaron"] # # [{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pluck(:id, :name) - # => [[1, "David"], [2, "Rafael"]] + # # => [[1, "David"], [2, "Rafael"]] def pluck(*keys) if keys.many? map { |element| keys.map { |key| element[key] } } diff --git a/activesupport/lib/active_support/core_ext/integer/time.rb b/activesupport/lib/active_support/core_ext/integer/time.rb index 4a64872392..74baae3639 100644 --- a/activesupport/lib/active_support/core_ext/integer/time.rb +++ b/activesupport/lib/active_support/core_ext/integer/time.rb @@ -18,12 +18,12 @@ class Integer # # equivalent to Time.now.advance(months: 4, years: 5) # (4.months + 5.years).from_now def months - ActiveSupport::Duration.new(self * 30.days, [[:months, self]]) + ActiveSupport::Duration.months(self) end alias :month :months def years - ActiveSupport::Duration.new(self * 365.25.days.to_i, [[:years, self]]) + ActiveSupport::Duration.years(self) end alias :year :years end diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index 3cf7ea0a97..d273487010 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -1,6 +1,3 @@ -require "active_support/deprecation" -require "active_support/deprecation/proxy_wrappers" - class LoadError REGEXPS = [ /^no such file to load -- (.+)$/i, @@ -9,18 +6,6 @@ class LoadError /^cannot load such file -- (.+)$/i, ] - unless method_defined?(:path) - # Returns the path which was unable to be loaded. - def path - @path ||= begin - REGEXPS.find do |regex| - message =~ regex - end - $1 - end - end - end - # Returns true if the given path name (except perhaps for the ".rb" # extension) is the missing file which caused the exception to be raised. def is_missing?(location) diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index 90989f4f3d..2c24081eb9 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -7,7 +7,8 @@ require "active_support/core_ext/regexp" class Module # Defines a class attribute and creates a class and instance reader methods. # The underlying class variable is set to +nil+, if it is not previously - # defined. + # defined. All class and instance methods created will be public, even if + # this method is called with a private or protected access modifier. # # module HairColors # mattr_reader :hair_colors @@ -76,7 +77,9 @@ class Module alias :cattr_reader :mattr_reader # Defines a class attribute and creates a class and instance writer methods to - # allow assignment to the attribute. + # allow assignment to the attribute. All class and instance methods created + # will be public, even if this method is called with a private or protected + # access modifier. # # module HairColors # mattr_writer :hair_colors @@ -142,6 +145,8 @@ class Module alias :cattr_writer :mattr_writer # Defines both class and instance accessors for class attributes. + # All class and instance methods created will be public, even if + # this method is called with a private or protected access modifier. # # module HairColors # mattr_accessor :hair_colors diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 19f692e943..cdf27f49ad 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -20,7 +20,8 @@ class Module # ==== Options # * <tt>:to</tt> - Specifies the target object # * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix - # * <tt>:allow_nil</tt> - if set to true, prevents a +NoMethodError+ from being raised + # * <tt>:allow_nil</tt> - if set to true, prevents a +Module::DelegationError+ + # from being raised # # The macro receives one or more method names (specified as symbols or # strings) and the name of the target object via the <tt>:to</tt> option @@ -112,18 +113,19 @@ class Module # invoice.customer_address # => 'Vimmersvej 13' # # If the target is +nil+ and does not respond to the delegated method a - # +NoMethodError+ is raised, as with any other value. Sometimes, however, it - # makes sense to be robust to that situation and that is the purpose of the - # <tt>:allow_nil</tt> option: If the target is not +nil+, or it is and - # responds to the method, everything works as usual. But if it is +nil+ and - # does not respond to the delegated method, +nil+ is returned. + # +Module::DelegationError+ is raised, as with any other value. Sometimes, + # however, it makes sense to be robust to that situation and that is the + # purpose of the <tt>:allow_nil</tt> option: If the target is not +nil+, or it + # is and responds to the method, everything works as usual. But if it is +nil+ + # and does not respond to the delegated method, +nil+ is returned. # # class User < ActiveRecord::Base # has_one :profile # delegate :age, to: :profile # end # - # User.new.age # raises NoMethodError: undefined method `age' + # User.new.age + # # => Module::DelegationError: User#age delegated to profile.age, but profile is nil # # But if not having a profile yet is fine and should not be an error # condition: @@ -258,7 +260,7 @@ class Module # end # # The target can be anything callable within the object. E.g. instance - # variables, methods, constants ant the likes. + # variables, methods, constants and the likes. def delegate_missing_to(target) target = target.to_s target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target) diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb index 0665aa88bc..ca20a6d4c5 100644 --- a/activesupport/lib/active_support/core_ext/module/introspection.rb +++ b/activesupport/lib/active_support/core_ext/module/introspection.rb @@ -5,10 +5,12 @@ class Module # # M::N.parent_name # => "M" def parent_name - if defined? @parent_name + if defined?(@parent_name) @parent_name else - @parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil + parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil + @parent_name = parent_name unless frozen? + parent_name end end diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index 946f8ddeab..4f6621693e 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -99,31 +99,30 @@ module ActiveSupport::NumericWithFormat # 1234567.to_s(:human, precision: 1, # separator: ',', # significant: false) # => "1,2 Million" - def to_s(*args) - format, options = args - options ||= {} - + def to_s(format = nil, options = nil) case format + when nil + super() + when Integer, String + super(format) when :phone - return ActiveSupport::NumberHelper.number_to_phone(self, options) + return ActiveSupport::NumberHelper.number_to_phone(self, options || {}) when :currency - return ActiveSupport::NumberHelper.number_to_currency(self, options) + return ActiveSupport::NumberHelper.number_to_currency(self, options || {}) when :percentage - return ActiveSupport::NumberHelper.number_to_percentage(self, options) + return ActiveSupport::NumberHelper.number_to_percentage(self, options || {}) when :delimited - return ActiveSupport::NumberHelper.number_to_delimited(self, options) + return ActiveSupport::NumberHelper.number_to_delimited(self, options || {}) when :rounded - return ActiveSupport::NumberHelper.number_to_rounded(self, options) + return ActiveSupport::NumberHelper.number_to_rounded(self, options || {}) when :human - return ActiveSupport::NumberHelper.number_to_human(self, options) + return ActiveSupport::NumberHelper.number_to_human(self, options || {}) when :human_size - return ActiveSupport::NumberHelper.number_to_human_size(self, options) + return ActiveSupport::NumberHelper.number_to_human_size(self, options || {}) + when Symbol + super() else - if is_a?(Float) || format.is_a?(Symbol) - super() - else - super - end + super(format) end end end diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index 809dfd4e07..2e6c70d418 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -19,7 +19,7 @@ class Numeric # # equivalent to Time.current.advance(months: 4, years: 5) # (4.months + 5.years).from_now def seconds - ActiveSupport::Duration.new(self, [[:seconds, self]]) + ActiveSupport::Duration.seconds(self) end alias :second :seconds @@ -27,7 +27,7 @@ class Numeric # # 2.minutes # => 2 minutes def minutes - ActiveSupport::Duration.new(self * 60, [[:minutes, self]]) + ActiveSupport::Duration.minutes(self) end alias :minute :minutes @@ -35,7 +35,7 @@ class Numeric # # 2.hours # => 2 hours def hours - ActiveSupport::Duration.new(self * 3600, [[:hours, self]]) + ActiveSupport::Duration.hours(self) end alias :hour :hours @@ -43,7 +43,7 @@ class Numeric # # 2.days # => 2 days def days - ActiveSupport::Duration.new(self * 24.hours, [[:days, self]]) + ActiveSupport::Duration.days(self) end alias :day :days @@ -51,7 +51,7 @@ class Numeric # # 2.weeks # => 2 weeks def weeks - ActiveSupport::Duration.new(self * 7.days, [[:weeks, self]]) + ActiveSupport::Duration.weeks(self) end alias :week :weeks @@ -59,7 +59,7 @@ class Numeric # # 2.fortnights # => 4 weeks def fortnights - ActiveSupport::Duration.new(self * 2.weeks, [[:weeks, self * 2]]) + ActiveSupport::Duration.weeks(self * 2) end alias :fortnight :fortnights diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index 9485e74816..ea81df2bd8 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -74,7 +74,7 @@ end class Symbol begin :symbol.dup # Ruby 2.4.x. - 'symbol_from_string'.to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0. + "symbol_from_string".to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0. rescue TypeError # Symbols are not duplicable: diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 4eabce79e2..7a2fc5c1b5 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -128,10 +128,10 @@ class String # Removes the module part from the constant expression in the string. # - # 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections" - # 'Inflections'.demodulize # => "Inflections" - # '::Inflections'.demodulize # => "Inflections" - # ''.demodulize # => '' + # 'ActiveSupport::Inflector::Inflections'.demodulize # => "Inflections" + # 'Inflections'.demodulize # => "Inflections" + # '::Inflections'.demodulize # => "Inflections" + # ''.demodulize # => '' # # See also +deconstantize+. def demodulize diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 227c34b032..94ce3f6a61 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -152,18 +152,16 @@ module ActiveSupport #:nodoc: def [](*args) if args.size < 2 super - else - if html_safe? - new_safe_buffer = super - - if new_safe_buffer - new_safe_buffer.instance_variable_set :@html_safe, true - end + elsif html_safe? + new_safe_buffer = super - new_safe_buffer - else - to_str[*args] + if new_safe_buffer + new_safe_buffer.instance_variable_set :@html_safe, true end + + new_safe_buffer + else + to_str[*args] end end diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb index 7655fa4f99..930d71e8d2 100644 --- a/activesupport/lib/active_support/deprecation/method_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb @@ -18,7 +18,7 @@ module ActiveSupport # # Using the default deprecator: # ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead') - # # => [:aaa, :bbb, :ccc] + # # => Fred # # Fred.aaa # # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.1. (called from irb_binding at (irb):10) diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index 1c6618b19a..ce39e9a232 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -122,10 +122,11 @@ module ActiveSupport # (Backtrace information…) # ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"] class DeprecatedConstantProxy < DeprecationProxy - def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance) + def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance, message: "#{old_const} is deprecated! Use #{new_const} instead.") @old_const = old_const @new_const = new_const @deprecator = deprecator + @message = message end # Returns the class of the new constant. @@ -143,7 +144,7 @@ module ActiveSupport end def warn(callstack, called, args) - @deprecator.warn("#{@old_const} is deprecated! Use #{@new_const} instead.", callstack) + @deprecator.warn(@message, callstack) end end end diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index b8d200ba94..58c5c50e30 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -48,11 +48,11 @@ module ActiveSupport private # Outputs a deprecation warning message # - # ActiveSupport::Deprecation.deprecated_method_warning(:method_name) + # deprecated_method_warning(:method_name) # # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon}" - # ActiveSupport::Deprecation.deprecated_method_warning(:method_name, :another_method) + # deprecated_method_warning(:method_name, :another_method) # # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon} (use another_method instead)" - # ActiveSupport::Deprecation.deprecated_method_warning(:method_name, "Optional message") + # deprecated_method_warning(:method_name, "Optional message") # # => "method_name is deprecated and will be removed from Rails #{deprecation_horizon} (Optional message)" def deprecated_method_warning(method_name, message = nil) warning = "#{method_name} is deprecated and will be removed from #{gem_name} #{deprecation_horizon}" diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index c9e8c8fdc4..003f6203ef 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -7,13 +7,82 @@ module ActiveSupport # # 1.month.ago # equivalent to Time.now.advance(months: -1) class Duration - EPOCH = ::Time.utc(2000) + SECONDS_PER_MINUTE = 60 + SECONDS_PER_HOUR = 3600 + SECONDS_PER_DAY = 86400 + SECONDS_PER_WEEK = 604800 + SECONDS_PER_MONTH = 2629746 # 1/12 of a gregorian year + SECONDS_PER_YEAR = 31556952 # length of a gregorian year (365.2425 days) + + PARTS_IN_SECONDS = { + seconds: 1, + minutes: SECONDS_PER_MINUTE, + hours: SECONDS_PER_HOUR, + days: SECONDS_PER_DAY, + weeks: SECONDS_PER_WEEK, + months: SECONDS_PER_MONTH, + years: SECONDS_PER_YEAR + }.freeze attr_accessor :value, :parts autoload :ISO8601Parser, "active_support/duration/iso8601_parser" autoload :ISO8601Serializer, "active_support/duration/iso8601_serializer" + class << self + # Creates a new Duration from string formatted according to ISO 8601 Duration. + # + # See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information. + # This method allows negative parts to be present in pattern. + # If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+. + def parse(iso8601duration) + parts = ISO8601Parser.new(iso8601duration).parse! + new(calculate_total_seconds(parts), parts) + end + + def ===(other) #:nodoc: + other.is_a?(Duration) + rescue ::NoMethodError + false + end + + def seconds(value) #:nodoc: + new(value, [[:seconds, value]]) + end + + def minutes(value) #:nodoc: + new(value * SECONDS_PER_MINUTE, [[:minutes, value]]) + end + + def hours(value) #:nodoc: + new(value * SECONDS_PER_HOUR, [[:hours, value]]) + end + + def days(value) #:nodoc: + new(value * SECONDS_PER_DAY, [[:days, value]]) + end + + def weeks(value) #:nodoc: + new(value * SECONDS_PER_WEEK, [[:weeks, value]]) + end + + def months(value) #:nodoc: + new(value * SECONDS_PER_MONTH, [[:months, value]]) + end + + def years(value) #:nodoc: + new(value * SECONDS_PER_YEAR, [[:years, value]]) + end + + private + + def calculate_total_seconds(parts) + parts.inject(0) do |total, (part, value)| + total + value * PARTS_IN_SECONDS[part] + end + end + end + def initialize(value, parts) #:nodoc: @value, @parts = value, parts.to_h @parts.default = 0 @@ -78,14 +147,14 @@ module ActiveSupport # 1.day.to_i # => 86400 # # Note that this conversion makes some assumptions about the - # duration of some periods, e.g. months are always 30 days - # and years are 365.25 days: + # duration of some periods, e.g. months are always 1/12 of year + # and years are 365.2425 days: # - # # equivalent to 30.days.to_i - # 1.month.to_i # => 2592000 + # # equivalent to (1.year / 12).to_i + # 1.month.to_i # => 2629746 # - # # equivalent to 365.25.days.to_i - # 1.year.to_i # => 31557600 + # # equivalent to 365.2425.days.to_i + # 1.year.to_i # => 31556952 # # In such cases, Ruby's core # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and @@ -105,12 +174,6 @@ module ActiveSupport @value.hash end - def self.===(other) #:nodoc: - other.is_a?(Duration) - rescue ::NoMethodError - false - end - # Calculates a new Time or Date that is as far in the future # as this Duration represents. def since(time = ::Time.current) @@ -141,16 +204,6 @@ module ActiveSupport @value.respond_to?(method, include_private) end - # Creates a new Duration from string formatted according to ISO 8601 Duration. - # - # See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information. - # This method allows negative parts to be present in pattern. - # If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+. - def self.parse(iso8601duration) - parts = ISO8601Parser.new(iso8601duration).parse! - new(EPOCH.advance(parts) - EPOCH, parts) - end - # Build ISO 8601 Duration string for this duration. # The +precision+ parameter can be used to limit seconds' precision of duration. def iso8601(precision: nil) diff --git a/activesupport/lib/active_support/evented_file_update_checker.rb b/activesupport/lib/active_support/evented_file_update_checker.rb index f54f88eb0a..8e0dc71dca 100644 --- a/activesupport/lib/active_support/evented_file_update_checker.rb +++ b/activesupport/lib/active_support/evented_file_update_checker.rb @@ -17,7 +17,7 @@ module ActiveSupport # # Example: # - # checker = EventedFileUpdateChecker.new(["/tmp/foo"], -> { puts "changed" }) + # checker = ActiveSupport::EventedFileUpdateChecker.new(["/tmp/foo"]) { puts "changed" } # checker.updated? # # => false # checker.execute_if_updated @@ -32,6 +32,10 @@ module ActiveSupport # class EventedFileUpdateChecker #:nodoc: all def initialize(files, dirs = {}, &block) + unless block + raise ArgumentError, "A block is required to initialize an EventedFileUpdateChecker" + end + @ph = PathHelper.new @files = files.map { |f| @ph.xpath(f) }.to_set diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb index 3384d12d5b..ca88e7876b 100644 --- a/activesupport/lib/active_support/execution_wrapper.rb +++ b/activesupport/lib/active_support/execution_wrapper.rb @@ -19,14 +19,14 @@ module ActiveSupport set_callback(:complete, *args, &block) end - class RunHook < Struct.new(:hook) # :nodoc: + RunHook = Struct.new(:hook) do # :nodoc: def before(target) hook_state = target.send(:hook_state) hook_state[hook] = hook.run end end - class CompleteHook < Struct.new(:hook) # :nodoc: + CompleteHook = Struct.new(:hook) do # :nodoc: def before(target) hook_state = target.send(:hook_state) if hook_state.key?(hook) diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index 2dbbfadac1..2b5e3c1350 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -38,6 +38,10 @@ module ActiveSupport # changes. The array of files and list of directories cannot be changed # after FileUpdateChecker has been initialized. def initialize(files, dirs = {}, &block) + unless block + raise ArgumentError, "A block is required to initialize a FileUpdateChecker" + end + @files = files.freeze @glob = compile_glob(dirs) @block = block diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index d3f7b46e77..79e7feaf47 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -269,6 +269,10 @@ module ActiveSupport dup.tap { |hash| hash.transform_values!(*args, &block) } end + def compact + dup.compact! + end + # Convert to a regular hash with string keys. def to_hash _new_hash = Hash.new diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index b94368df14..b749913ee9 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -2,6 +2,8 @@ require "active_support" require "active_support/file_update_checker" require "active_support/core_ext/array/wrap" +# :enddoc: + module I18n class Railtie < Rails::Railtie config.i18n = ActiveSupport::OrderedOptions.new diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 8fea96a82a..8ccb735c6d 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -198,10 +198,10 @@ module ActiveSupport # Removes the module part from the expression in the string. # - # demodulize('ActiveRecord::CoreExtensions::String::Inflections') # => "Inflections" - # demodulize('Inflections') # => "Inflections" - # demodulize('::Inflections') # => "Inflections" - # demodulize('') # => "" + # demodulize('ActiveSupport::Inflector::Inflections') # => "Inflections" + # demodulize('Inflections') # => "Inflections" + # demodulize('::Inflections') # => "Inflections" + # demodulize('') # => "" # # See also #deconstantize. def demodulize(path) diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index 3e78986e8e..de6dd2720b 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -57,6 +57,8 @@ module ActiveSupport # transliterate('Jürgen') # # => "Juergen" def transliterate(string, replacement = "?".freeze) + raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String) + I18n.transliterate(ActiveSupport::Multibyte::Unicode.normalize( ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c), replacement: replacement) diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 7b33dc3481..0671469788 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -14,10 +14,10 @@ module ActiveSupport # where you don't want users to be able to determine the value of the payload. # # salt = SecureRandom.random_bytes(64) - # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt) # => "\x89\xE0\x156\xAC..." - # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...> - # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." - # crypt.decrypt_and_verify(encrypted_data) # => "my secret data" + # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt, 32) # => "\x89\xE0\x156\xAC..." + # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...> + # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." + # crypt.decrypt_and_verify(encrypted_data) # => "my secret data" class MessageEncryptor DEFAULT_CIPHER = "aes-256-cbc" diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 65d6259a06..8c58466556 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -16,7 +16,8 @@ module ActiveSupport #:nodoc: # through the +mb_chars+ method. Methods which would normally return a # String object now return a Chars object so methods can be chained. # - # 'The Perfect String '.mb_chars.downcase.strip.normalize # => "the perfect string" + # 'The Perfect String '.mb_chars.downcase.strip.normalize + # # => #<ActiveSupport::Multibyte::Chars:0x007fdc434ccc10 @wrapped_string="the perfect string"> # # Chars objects are perfectly interchangeable with String objects as long as # no explicit class checks are made. If certain methods do explicitly check @@ -134,7 +135,7 @@ module ActiveSupport #:nodoc: # Converts characters in the string to the opposite case. # - # 'El Cañón".mb_chars.swapcase.to_s # => "eL cAÑÓN" + # 'El Cañón'.mb_chars.swapcase.to_s # => "eL cAÑÓN" def swapcase chars Unicode.swapcase(@wrapped_string) end @@ -148,8 +149,8 @@ module ActiveSupport #:nodoc: # Capitalizes the first letter of every word, when possible. # - # "ÉL QUE SE ENTERÓ".mb_chars.titleize # => "Él Que Se Enteró" - # "日本語".mb_chars.titleize # => "日本語" + # "ÉL QUE SE ENTERÓ".mb_chars.titleize.to_s # => "Él Que Se Enteró" + # "日本語".mb_chars.titleize.to_s # => "日本語" def titleize chars(downcase.to_s.gsub(/\b('?\S)/u) { Unicode.upcase($1) }) end diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 05cfb249c3..0912912aba 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -9,7 +9,7 @@ module ActiveSupport NORMALIZATION_FORMS = [:c, :kc, :d, :kd] # The Unicode version that is supported by the implementation - UNICODE_VERSION = "8.0.0" + UNICODE_VERSION = "9.0.0" # The default normalization used for operations that require # normalization. It can be set to any of the normalizations @@ -57,9 +57,12 @@ module ActiveSupport previous = codepoints[pos - 1] current = codepoints[pos] + # See http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules should_break = + if pos == eoc + true # GB3. CR X LF - if previous == database.boundary[:cr] && current == database.boundary[:lf] + elsif previous == database.boundary[:cr] && current == database.boundary[:lf] false # GB4. (Control|CR|LF) ÷ elsif previous && in_char_class?(previous, [:control, :cr, :lf]) @@ -76,11 +79,8 @@ module ActiveSupport # GB8. (LVT|T) X (T) elsif in_char_class?(previous, [:lvt, :t]) && database.boundary[:t] === current false - # GB8a. Regional_Indicator X Regional_Indicator - elsif database.boundary[:regional_indicator] === previous && database.boundary[:regional_indicator] === current - false - # GB9. X Extend - elsif database.boundary[:extend] === current + # GB9. X (Extend | ZWJ) + elsif in_char_class?(current, [:extend, :zwj]) false # GB9a. X SpacingMark elsif database.boundary[:spacingmark] === current @@ -88,7 +88,17 @@ module ActiveSupport # GB9b. Prepend X elsif database.boundary[:prepend] === previous false - # GB10. Any ÷ Any + # GB10. (E_Base | EBG) Extend* X E_Modifier + elsif (marker...pos).any? { |i| in_char_class?(codepoints[i], [:e_base, :e_base_gaz]) && codepoints[i + 1...pos].all? { |c| database.boundary[:extend] === c } } && database.boundary[:e_modifier] === current + false + # GB11. ZWJ X (Glue_After_Zwj | EBG) + elsif database.boundary[:zwj] === previous && in_char_class?(current, [:glue_after_zwj, :e_base_gaz]) + false + # GB12. ^ (RI RI)* RI X RI + # GB13. [^RI] (RI RI)* RI X RI + elsif codepoints[marker..pos].all? { |c| database.boundary[:regional_indicator] === c } && codepoints[marker..pos].count { |c| database.boundary[:regional_indicator] === c }.even? + false + # GB999. Any ÷ Any else true end diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 6000ea44be..880340ca86 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -45,7 +45,7 @@ module ActiveSupport # # number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true) # # => "(755) 6123-4567" - # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/)) + # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/) # # => "133-1234-5678" def number_to_phone(number, options = {}) NumberToPhoneConverter.convert(number, options) @@ -78,7 +78,7 @@ module ActiveSupport # (defaults to "%u%n"). Fields are <tt>%u</tt> for the # currency, and <tt>%n</tt> for the number. # * <tt>:negative_format</tt> - Sets the format for negative - # numbers (defaults to prepending an hyphen to the formatted + # numbers (defaults to prepending a hyphen to the formatted # number given by <tt>:format</tt>). Accepts the same fields # than <tt>:format</tt>, except <tt>%n</tt> is here the # absolute value of the number. diff --git a/activesupport/lib/active_support/string_inquirer.rb b/activesupport/lib/active_support/string_inquirer.rb index 09e1cbb28d..90eac89c9e 100644 --- a/activesupport/lib/active_support/string_inquirer.rb +++ b/activesupport/lib/active_support/string_inquirer.rb @@ -18,7 +18,7 @@ module ActiveSupport private def respond_to_missing?(method_name, include_private = false) - method_name[-1] == "?" + (method_name[-1] == "?") || super end def method_missing(method_name, *arguments) diff --git a/activesupport/lib/active_support/testing/autorun.rb b/activesupport/lib/active_support/testing/autorun.rb index 3108e3e549..a18788f38e 100644 --- a/activesupport/lib/active_support/testing/autorun.rb +++ b/activesupport/lib/active_support/testing/autorun.rb @@ -2,8 +2,8 @@ gem "minitest" require "minitest" -if Minitest.respond_to?(:run_via) && !Minitest.run_via[:rails] - Minitest.run_via[:ruby] = true +if Minitest.respond_to?(:run_via) && !Minitest.run_via.set? + Minitest.run_via = :ruby end Minitest.autorun diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 14edd13a8f..54c3263efa 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -43,7 +43,7 @@ module ActiveSupport end } end - result = Marshal.dump(self.dup) + result = Marshal.dump(dup) end write.puts [result].pack("m") @@ -78,7 +78,7 @@ module ActiveSupport "ISOLATION_OUTPUT" => tmpfile.path } - test_opts = "-n#{self.class.name}##{self.name}" + test_opts = "-n#{self.class.name}##{name}" load_path_args = [] $-I.each do |p| diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index e2f008b4b7..07c9be0604 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -10,7 +10,7 @@ module ActiveSupport @stubs = Concurrent::Map.new { |h, k| h[k] = {} } end - def stub_object(object, method_name, return_value) + def stub_object(object, method_name, &block) if stub = stubbing(object, method_name) unstub_object(stub) end @@ -20,7 +20,7 @@ module ActiveSupport @stubs[object.object_id][method_name] = Stub.new(object, method_name, new_name) object.singleton_class.send :alias_method, new_name, method_name - object.define_singleton_method(method_name) { return_value } + object.define_singleton_method(method_name, &block) end def unstub_all! @@ -134,9 +134,9 @@ module ActiveSupport now = date_or_time.to_time.change(usec: 0) end - simple_stubs.stub_object(Time, :now, now) - simple_stubs.stub_object(Date, :today, now.to_date) - simple_stubs.stub_object(DateTime, :now, now.to_datetime) + simple_stubs.stub_object(Time, :now) { at(now.to_i) } + simple_stubs.stub_object(Date, :today) { jd(now.to_date.jd) } + simple_stubs.stub_object(DateTime, :now) { jd(now.to_date.jd, now.hour, now.min, now.sec, Rational(now.utc_offset, 86400)) } if block_given? begin diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 889f71c4f3..857cc1a664 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -427,7 +427,8 @@ module ActiveSupport end def freeze - period; utc; time # preload instance variables before freezing + # preload instance variables before freezing + period; utc; time; to_datetime super end diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differindex dd2c178fb6..f7d9c48bbe 100644 --- a/activesupport/lib/active_support/values/unicode_tables.dat +++ b/activesupport/lib/active_support/values/unicode_tables.dat diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb index 44b0bdb7dc..cde2967132 100644 --- a/activesupport/lib/active_support/xml_mini/libxml.rb +++ b/activesupport/lib/active_support/xml_mini/libxml.rb @@ -74,5 +74,7 @@ module LibXML #:nodoc: end end +# :enddoc: + LibXML::XML::Document.include(LibXML::Conversions::Document) LibXML::XML::Node.include(LibXML::Conversions::Node) diff --git a/activesupport/lib/active_support/xml_mini/libxmlsax.rb b/activesupport/lib/active_support/xml_mini/libxmlsax.rb index 4edb0bd2b1..8a43b05b17 100644 --- a/activesupport/lib/active_support/xml_mini/libxmlsax.rb +++ b/activesupport/lib/active_support/xml_mini/libxmlsax.rb @@ -73,7 +73,7 @@ module ActiveSupport LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER) parser = LibXML::XML::SaxParser.io(data) - document = self.document_class.new + document = document_class.new parser.callbacks = document parser.parse diff --git a/activesupport/lib/active_support/xml_mini/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb index b8b85146c5..7388bea5d8 100644 --- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb +++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb @@ -76,7 +76,7 @@ module ActiveSupport {} else data.ungetc(char) - document = self.document_class.new + document = document_class.new parser = Nokogiri::XML::SAX::Parser.new(document) parser.parse(data) document.hash diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 4e564591b4..c4f34c0abf 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -3,8 +3,8 @@ ORIG_ARGV = ARGV.dup require "active_support/core_ext/kernel/reporting" silence_warnings do - Encoding.default_internal = "UTF-8" - Encoding.default_external = "UTF-8" + Encoding.default_internal = Encoding::UTF_8 + Encoding.default_external = Encoding::UTF_8 end require "active_support/testing/autorun" @@ -24,16 +24,16 @@ ActiveSupport.to_time_preserves_timezone = ENV["PRESERVE_TIMEZONES"] == "1" # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" -end - -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) -end - class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions + + # Skips the current run on Rubinius using Minitest::Assertions#skip + private def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + + # Skips the current run on JRuby using Minitest::Assertions#skip + private def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/activesupport/test/array_inquirer_test.rb b/activesupport/test/array_inquirer_test.rb index 4d3f5b001c..5b2bc82905 100644 --- a/activesupport/test/array_inquirer_test.rb +++ b/activesupport/test/array_inquirer_test.rb @@ -38,4 +38,24 @@ class ArrayInquirerTest < ActiveSupport::TestCase assert_instance_of ActiveSupport::ArrayInquirer, result assert_equal @array_inquirer, result end + + def test_respond_to_fallback_to_array_respond_to + Array.class_eval do + def respond_to_missing?(name, include_private = false) + (name == :foo) || super + end + end + arr = ActiveSupport::ArrayInquirer.new([:x]) + + assert_respond_to arr, :can_you_hear_me? + assert_respond_to arr, :foo + assert_not_respond_to arr, :nope + ensure + Array.class_eval do + undef_method :respond_to_missing? + def respond_to_missing?(name, include_private = false) + super + end + end + end end diff --git a/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb b/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb index 829ee917d2..e477ab21d0 100644 --- a/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb +++ b/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb @@ -1,4 +1,4 @@ RaisesArbitraryException = 1 _ = A::B # Autoloading recursion, also expected to be watched and discarded. -raise Exception, "arbitray exception message" +raise Exception, "arbitrary exception message" diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 28caa30bf1..4f00afb581 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -23,10 +23,6 @@ module CallbacksTest method_name end - def callback_string(callback_method) - "history << [#{callback_method.to_sym.inspect}, :string]" - end - def callback_proc(callback_method) Proc.new { |model| model.history << [callback_method, :proc] } end @@ -61,7 +57,6 @@ module CallbacksTest [:before_save, :after_save].each do |callback_method| callback_method_sym = callback_method.to_sym send(callback_method, callback_symbol(callback_method_sym)) - ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method_sym)) } send(callback_method, callback_proc(callback_method_sym)) send(callback_method, callback_object(callback_method_sym.to_s.gsub(/_save/, ""))) send(callback_method, CallbackClass) @@ -197,10 +192,12 @@ module CallbacksTest before_save Proc.new { |r| r.history << [:before_save, :symbol] }, unless: :no before_save Proc.new { |r| r.history << "b00m" }, unless: :yes # string - before_save Proc.new { |r| r.history << [:before_save, :string] }, if: "yes" - before_save Proc.new { |r| r.history << "b00m" }, if: "no" - before_save Proc.new { |r| r.history << [:before_save, :string] }, unless: "no" - before_save Proc.new { |r| r.history << "b00m" }, unless: "yes" + ActiveSupport::Deprecation.silence do + before_save Proc.new { |r| r.history << [:before_save, :string] }, if: "yes" + before_save Proc.new { |r| r.history << "b00m" }, if: "no" + before_save Proc.new { |r| r.history << [:before_save, :string] }, unless: "no" + before_save Proc.new { |r| r.history << "b00m" }, unless: "yes" + end # Combined if and unless before_save Proc.new { |r| r.history << [:before_save, :combined_symbol] }, if: :yes, unless: :no before_save Proc.new { |r| r.history << "b00m" }, if: :yes, unless: :yes @@ -272,7 +269,6 @@ module CallbacksTest set_callback :save, :before, :nope, if: :no set_callback :save, :before, :nope, unless: :yes set_callback :save, :after, :tweedle - ActiveSupport::Deprecation.silence { set_callback :save, :before, "tweedle_dee" } set_callback :save, :before, proc { |m| m.history << "yup" } set_callback :save, :before, :nope, if: proc { false } set_callback :save, :before, :nope, unless: proc { true } @@ -302,10 +298,6 @@ module CallbacksTest yield end - def tweedle_dee - @history << "tweedle dee" - end - def tweedle_dum @history << "tweedle dum pre" yield @@ -421,7 +413,6 @@ module CallbacksTest around = AroundPerson.new around.save assert_equal [ - "tweedle dee", "yup", "yup", "tweedle dum pre", "w0tyes before", @@ -540,7 +531,6 @@ module CallbacksTest assert_equal [], person.history person.save assert_equal [ - [:before_save, :string], [:before_save, :proc], [:before_save, :object], [:before_save, :block], @@ -548,7 +538,6 @@ module CallbacksTest [:after_save, :class], [:after_save, :object], [:after_save, :proc], - [:after_save, :string], [:after_save, :symbol] ], person.history end @@ -567,7 +556,6 @@ module CallbacksTest [:after_save, :class], [:after_save, :object], [:after_save, :proc], - [:after_save, :string], [:after_save, :symbol] ], person.history end @@ -580,7 +568,6 @@ module CallbacksTest person.save assert_equal [ [:before_save, :symbol], - [:before_save, :string], [:before_save, :proc], [:before_save, :object], [:before_save, :class], @@ -589,7 +576,6 @@ module CallbacksTest [:after_save, :class], [:after_save, :object], [:after_save, :proc], - [:after_save, :string], [:after_save, :symbol] ], person.history end @@ -883,34 +869,8 @@ module CallbacksTest end end - class CallbackFalseTerminatorWithoutConfigTest < ActiveSupport::TestCase - def test_returning_false_does_not_halt_callback_if_config_variable_is_not_set - obj = CallbackFalseTerminator.new - obj.save - assert_nil obj.halted - assert obj.saved - end - end - - class CallbackFalseTerminatorWithConfigTrueTest < ActiveSupport::TestCase - def setup - ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = true - end - - def test_returning_false_does_not_halt_callback_if_config_variable_is_true - obj = CallbackFalseTerminator.new - obj.save - assert_nil obj.halted - assert obj.saved - end - end - - class CallbackFalseTerminatorWithConfigFalseTest < ActiveSupport::TestCase - def setup - ActiveSupport::Callbacks.halt_and_display_warning_on_return_false = false - end - - def test_returning_false_does_not_halt_callback_if_config_variable_is_false + class CallbackFalseTerminatorTest < ActiveSupport::TestCase + def test_returning_false_does_not_halt_callback obj = CallbackFalseTerminator.new obj.save assert_nil obj.halted @@ -939,7 +899,6 @@ module CallbacksTest writer.save assert_equal [ [:before_save, :symbol], - [:before_save, :string], [:before_save, :proc], [:before_save, :object], [:before_save, :class], @@ -948,7 +907,6 @@ module CallbacksTest [:after_save, :class], [:after_save, :object], [:after_save, :proc], - [:after_save, :string], [:after_save, :symbol] ], writer.history end @@ -1162,14 +1120,6 @@ module CallbacksTest assert_equal 1, calls.length end - def test_add_eval - calls = [] - klass = ActiveSupport::Deprecation.silence { build_class("bar") } - klass.class_eval { define_method(:bar) { calls << klass } } - klass.new.run - assert_equal 1, calls.length - end - def test_skip_class # removes one at a time calls = [] callback = Class.new { @@ -1204,7 +1154,7 @@ module CallbacksTest def test_skip_string # raises error calls = [] - klass = ActiveSupport::Deprecation.silence { build_class("bar") } + klass = build_class(:bar) klass.class_eval { define_method(:bar) { calls << klass } } assert_raises(ArgumentError) { klass.skip "bar" } klass.new.run @@ -1231,11 +1181,22 @@ module CallbacksTest end class DeprecatedWarningTest < ActiveSupport::TestCase - def test_deprecate_string_callback + def test_deprecate_string_conditional_options klass = Class.new(Record) - assert_deprecated do - klass.send :before_save, "tweedle_dee" + assert_deprecated { klass.before_save :tweedle, if: "true" } + assert_deprecated { klass.after_save :tweedle, unless: "false" } + assert_deprecated { klass.skip_callback :save, :before, :tweedle, if: "true" } + assert_deprecated { klass.skip_callback :save, :after, :tweedle, unless: "false" } + end + end + + class NotPermittedStringCallbackTest < ActiveSupport::TestCase + def test_passing_string_callback_is_not_permitted + klass = Class.new(Record) + + assert_raises(ArgumentError) do + klass.before_save "tweedle" end end end diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb index 5a90210bb8..50bb1004f7 100644 --- a/activesupport/test/core_ext/date_ext_test.rb +++ b/activesupport/test/core_ext/date_ext_test.rb @@ -395,6 +395,10 @@ class DateExtBehaviorTest < ActiveSupport::TestCase assert Date.new.acts_like_date? end + def test_blank? + assert_not Date.new.blank? + end + def test_freeze_doesnt_clobber_memoized_instance_methods assert_nothing_raised do Date.today.freeze.inspect diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index e9be181749..e3b31c10f5 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -318,6 +318,10 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase assert DateTime.new.acts_like_time? end + def test_blank? + assert_not DateTime.new.blank? + end + def test_utc? assert_equal true, DateTime.civil(2005, 2, 21, 10, 11, 12).utc? assert_equal true, DateTime.civil(2005, 2, 21, 10, 11, 12, 0).utc? diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index fc0dd41d0e..6a275d1d5b 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -237,6 +237,29 @@ class DurationTest < ActiveSupport::TestCase assert_equal(1, (61 <=> 1.minute)) end + def test_twelve_months_equals_one_year + assert_equal 12.months, 1.year + end + + def test_thirty_days_does_not_equal_one_month + assert_not_equal 30.days, 1.month + end + + def test_adding_one_month_maintains_day_of_month + (1..11).each do |month| + [1, 14, 28].each do |day| + assert_equal Date.civil(2016, month + 1, day), Date.civil(2016, month, day) + 1.month + end + end + + assert_equal Date.civil(2017, 1, 1), Date.civil(2016, 12, 1) + 1.month + assert_equal Date.civil(2017, 1, 14), Date.civil(2016, 12, 14) + 1.month + assert_equal Date.civil(2017, 1, 28), Date.civil(2016, 12, 28) + 1.month + + assert_equal Date.civil(2015, 2, 28), Date.civil(2015, 1, 31) + 1.month + assert_equal Date.civil(2016, 2, 29), Date.civil(2016, 1, 31) + 1.month + end + # ISO8601 string examples are taken from ISO8601 gem at https://github.com/arnau/ISO8601/blob/b93d466840/spec/iso8601/duration_spec.rb # published under the conditions of MIT license at https://github.com/arnau/ISO8601/blob/b93d466840/LICENSE # @@ -345,6 +368,21 @@ class DurationTest < ActiveSupport::TestCase end end + def test_iso8601_parsing_equivalence_with_numeric_extensions_over_long_periods + with_env_tz eastern_time_zone do + with_tz_default "Eastern Time (US & Canada)" do + assert_equal 3.months, ActiveSupport::Duration.parse("P3M") + assert_equal 3.months.to_i, ActiveSupport::Duration.parse("P3M").to_i + assert_equal 10.months, ActiveSupport::Duration.parse("P10M") + assert_equal 10.months.to_i, ActiveSupport::Duration.parse("P10M").to_i + assert_equal 3.years, ActiveSupport::Duration.parse("P3Y") + assert_equal 3.years.to_i, ActiveSupport::Duration.parse("P3Y").to_i + assert_equal 10.years, ActiveSupport::Duration.parse("P10Y") + assert_equal 10.years.to_i, ActiveSupport::Duration.parse("P10Y").to_i + end + end + end + def test_adding_durations_do_not_hold_prior_states time = Time.parse("Nov 29, 2016") # If the implementation adds and subtracts 3 months, the diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index bea0a84b53..05813ad388 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -589,6 +589,16 @@ class HashExtTest < ActiveSupport::TestCase assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings end + def test_indifferent_compact + hash_contain_nil_value = @strings.merge("z" => nil) + hash = ActiveSupport::HashWithIndifferentAccess.new(hash_contain_nil_value) + compacted_hash = hash.compact + + assert_equal(@strings, compacted_hash) + assert_equal(hash_contain_nil_value, hash) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, compacted_hash + end + def test_indifferent_to_hash # Should convert to a Hash with String keys. assert_equal @strings, @mixed.with_indifferent_access.to_hash diff --git a/activesupport/test/core_ext/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb index db0008b735..26f5088ede 100644 --- a/activesupport/test/core_ext/kernel_test.rb +++ b/activesupport/test/core_ext/kernel_test.rb @@ -49,19 +49,3 @@ class KernelSuppressTest < ActiveSupport::TestCase suppress(LoadError, ArgumentError) { raise ArgumentError } end end - -class MockStdErr - attr_reader :output - def puts(message) - @output ||= [] - @output << message - end - - def info(message) - puts(message) - end - - def write(message) - puts(message) - end -end diff --git a/activesupport/test/core_ext/module/attribute_aliasing_test.rb b/activesupport/test/core_ext/module/attribute_aliasing_test.rb index d8c2dfd6b8..fdfa868851 100644 --- a/activesupport/test/core_ext/module/attribute_aliasing_test.rb +++ b/activesupport/test/core_ext/module/attribute_aliasing_test.rb @@ -52,8 +52,8 @@ class AttributeAliasingTest < ActiveSupport::TestCase assert_equal "No, really, this is not a joke.", e.Data assert e.Data? - e.Data = "Uppercased methods are teh suck" - assert_equal "Uppercased methods are teh suck", e.body + e.Data = "Uppercased methods are the suck" + assert_equal "Uppercased methods are the suck", e.body assert e.body? end end diff --git a/activesupport/test/core_ext/module/introspection_test.rb b/activesupport/test/core_ext/module/introspection_test.rb new file mode 100644 index 0000000000..db383850cd --- /dev/null +++ b/activesupport/test/core_ext/module/introspection_test.rb @@ -0,0 +1,37 @@ +require "abstract_unit" +require "active_support/core_ext/module/introspection" + +module ParentA + module B + module C; end + module FrozenC; end + FrozenC.freeze + end + + module FrozenB; end + FrozenB.freeze +end + +class IntrospectionTest < ActiveSupport::TestCase + def test_parent_name + assert_equal "ParentA", ParentA::B.parent_name + assert_equal "ParentA::B", ParentA::B::C.parent_name + assert_nil ParentA.parent_name + end + + def test_parent_name_when_frozen + assert_equal "ParentA", ParentA::FrozenB.parent_name + assert_equal "ParentA::B", ParentA::B::FrozenC.parent_name + end + + def test_parent + assert_equal ParentA::B, ParentA::B::C.parent + assert_equal ParentA, ParentA::B.parent + assert_equal Object, ParentA.parent + end + + def test_parents + assert_equal [ParentA::B, ParentA, Object], ParentA::B::C.parents + assert_equal [ParentA, Object], ParentA::B.parents + end +end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index a4515d1956..a17438bf4d 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -1,30 +1,11 @@ require "abstract_unit" require "active_support/core_ext/module" -module One - Constant1 = "Hello World" - Constant2 = "What's up?" -end - -class Ab - include One - Constant1 = "Hello World" # Will have different object id than One::Constant1 - Constant3 = "Goodbye World" -end - -module Yz - module Zy - class Cd - include One - end - end -end - Somewhere = Struct.new(:street, :city) do attr_accessor :name end -class Someone < Struct.new(:name, :place) +Someone = Struct.new(:name, :place) do delegate :street, :city, :to_f, to: :place delegate :name=, to: :place, prefix: true delegate :upcase, to: "place.city" @@ -35,10 +16,10 @@ class Someone < Struct.new(:name, :place) "some_table" end - FAILED_DELEGATE_LINE = __LINE__ + 1 + self::FAILED_DELEGATE_LINE = __LINE__ + 1 delegate :foo, to: :place - FAILED_DELEGATE_LINE_2 = __LINE__ + 1 + self::FAILED_DELEGATE_LINE_2 = __LINE__ + 1 delegate :bar, to: :place, allow_nil: true private @@ -375,17 +356,6 @@ class ModuleTest < ActiveSupport::TestCase assert_match(/undefined method `my_fake_method' for/, e.message) end - def test_parent - assert_equal Yz::Zy, Yz::Zy::Cd.parent - assert_equal Yz, Yz::Zy.parent - assert_equal Object, Yz.parent - end - - def test_parents - assert_equal [Yz::Zy, Yz, Object], Yz::Zy::Cd.parents - assert_equal [Yz, Object], Yz::Zy.parents - end - def test_delegate_with_case event = Event.new(Tester.new) assert_equal 1, event.foo diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 5361b7b708..3cfbe6e7e6 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -12,7 +12,7 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase 10.minutes => 600, 1.hour + 15.minutes => 4500, 2.days + 4.hours + 30.minutes => 189000, - 5.years + 1.month + 1.fortnight => 161589600 + 5.years + 1.month + 1.fortnight => 161624106 } end @@ -61,10 +61,10 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase end def test_duration_after_conversion_is_no_longer_accurate - assert_equal 30.days.to_i.seconds.since(@now), 1.month.to_i.seconds.since(@now) - assert_equal 365.25.days.to_f.seconds.since(@now), 1.year.to_f.seconds.since(@now) - assert_equal 30.days.to_i.seconds.since(@dtnow), 1.month.to_i.seconds.since(@dtnow) - assert_equal 365.25.days.to_f.seconds.since(@dtnow), 1.year.to_f.seconds.since(@dtnow) + assert_equal (1.year / 12).to_i.seconds.since(@now), 1.month.to_i.seconds.since(@now) + assert_equal 365.2425.days.to_f.seconds.since(@now), 1.year.to_f.seconds.since(@now) + assert_equal (1.year / 12).to_i.seconds.since(@dtnow), 1.month.to_i.seconds.since(@dtnow) + assert_equal 365.2425.days.to_f.seconds.since(@dtnow), 1.year.to_f.seconds.since(@dtnow) end def test_add_one_year_to_leap_day @@ -394,6 +394,10 @@ class NumericExtFormattingTest < ActiveSupport::TestCase assert_equal "1000010.0", BigDecimal("1000010").to_s assert_equal "10000 10.0", BigDecimal("1000010").to_s("5F") + + assert_raises TypeError do + 1.to_s({}) + end end def test_in_milliseconds diff --git a/activesupport/test/core_ext/object/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb index 1bedc76320..7fd3fed042 100644 --- a/activesupport/test/core_ext/object/blank_test.rb +++ b/activesupport/test/core_ext/object/blank_test.rb @@ -15,7 +15,7 @@ class BlankTest < ActiveSupport::TestCase end BLANK = [ EmptyTrue.new, nil, false, "", " ", " \n\t \r ", " ", "\u00a0", [], {} ] - NOT = [ EmptyFalse.new, Object.new, true, 0, 1, "a", [nil], { nil => 0 } ] + NOT = [ EmptyFalse.new, Object.new, true, 0, 1, "a", [nil], { nil => 0 }, Time.now ] def test_blank BLANK.each { |v| assert_equal true, v.blank?, "#{v.inspect} should be blank" } diff --git a/activesupport/test/core_ext/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb index fb140a5b76..e6f3c8aef2 100644 --- a/activesupport/test/core_ext/object/duplicable_test.rb +++ b/activesupport/test/core_ext/object/duplicable_test.rb @@ -4,8 +4,11 @@ require "active_support/core_ext/object/duplicable" require "active_support/core_ext/numeric/time" class DuplicableTest < ActiveSupport::TestCase - if RUBY_VERSION >= "2.4.0" - RAISE_DUP = [method(:puts), Complex(1), Rational(1), 'symbol_from_string'.to_sym] + if RUBY_VERSION >= "2.4.1" + RAISE_DUP = [method(:puts), Complex(1), Rational(1)] + ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal.new("4.56"), nil, false, true, 1, 2.3] + elsif RUBY_VERSION >= "2.4.0" # Due to 2.4.0 bug. This elsif cannot be removed unless we drop 2.4.0 support... + RAISE_DUP = [method(:puts), Complex(1), Rational(1), "symbol_from_string".to_sym] ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal.new("4.56"), nil, false, true, 1, 2.3] else RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, method(:puts), Complex(1), Rational(1)] @@ -17,7 +20,7 @@ class DuplicableTest < ActiveSupport::TestCase "* https://github.com/rubinius/rubinius/issues/3089" RAISE_DUP.each do |v| - assert !v.duplicable? + assert !v.duplicable?, "#{ v.inspect } should not be duplicable" assert_raises(TypeError, v.class.name) { v.dup } end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 00685cd952..41cc9888c6 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -1,5 +1,6 @@ require "date" require "abstract_unit" +require "timeout" require "inflector_test_cases" require "constantize_test_cases" @@ -222,7 +223,7 @@ class StringInflectionsTest < ActiveSupport::TestCase original = %{\u205f\u3000 A string surrounded by various unicode spaces, with tabs(\t\t), newlines(\n\n), unicode nextlines(\u0085\u0085) and many spaces( ). \u00a0\u2007} - expected = "A string surrounded by various unicode spaces, " + + expected = "A string surrounded by various unicode spaces, " \ "with tabs( ), newlines( ), unicode nextlines( ) and many spaces( )." # Make sure squish returns what we expect: diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 629666947f..1534daacb9 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -455,6 +455,10 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_equal false, ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone).acts_like?(:date) end + def test_blank? + assert_not @twz.blank? + end + def test_is_a assert_kind_of Time, @twz assert_kind_of Time, @twz @@ -503,6 +507,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase assert_nothing_raised do @twz.period @twz.time + @twz.to_datetime end end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index e772d15d53..e38d4e83e5 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -271,7 +271,8 @@ class DependenciesTest < ActiveSupport::TestCase def test_raising_discards_autoloaded_constants with_autoloading_fixtures do - assert_raises(Exception, "arbitray exception message") { RaisesArbitraryException } + e = assert_raises(Exception) { RaisesArbitraryException } + assert_equal("arbitrary exception message", e.message) assert_not defined?(A) assert_not defined?(RaisesArbitraryException) end @@ -397,14 +398,17 @@ class DependenciesTest < ActiveSupport::TestCase end end - def failing_test_access_thru_and_upwards_fails - with_autoloading_fixtures do - assert_not defined?(ModuleFolder) - assert_raise(NameError) { ModuleFolder::Object } - assert_raise(NameError) { ModuleFolder::NestedClass::Object } + # This raises only on 2.5.. (warns on ..2.4) + if RUBY_VERSION > "2.5" + def test_access_thru_and_upwards_fails + with_autoloading_fixtures do + assert_not defined?(ModuleFolder) + assert_raise(NameError) { ModuleFolder::Object } + assert_raise(NameError) { ModuleFolder::NestedClass::Object } + end + ensure + remove_constants(:ModuleFolder) end - ensure - remove_constants(:ModuleFolder) end def test_non_existing_const_raises_name_error_with_fully_qualified_name @@ -548,7 +552,6 @@ class DependenciesTest < ActiveSupport::TestCase assert_equal autoload + "/conflict.rb", ActiveSupport::Dependencies.search_for_file("conflict") end - end def test_custom_const_missing_should_work diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index 5be93f3a1a..5f72fbf662 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -285,6 +285,16 @@ class DeprecationTest < ActiveSupport::TestCase end end + def test_deprecated_constant_with_custom_message + deprecator = deprecator_with_messages + + klass = Class.new + klass.const_set(:OLD, ActiveSupport::Deprecation::DeprecatedConstantProxy.new("klass::OLD", "Object", deprecator, message: "foo")) + + klass::OLD.to_s + assert_match "foo", deprecator.messages.last + end + def test_deprecated_instance_variable_with_instance_deprecator deprecator = deprecator_with_messages diff --git a/activesupport/test/evented_file_update_checker_test.rb b/activesupport/test/evented_file_update_checker_test.rb index 77d8dcb0f8..f33a5f5764 100644 --- a/activesupport/test/evented_file_update_checker_test.rb +++ b/activesupport/test/evented_file_update_checker_test.rb @@ -7,6 +7,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase def setup skip if ENV["LISTEN"] == "0" + require "listen" super end @@ -30,11 +31,6 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase wait # wait for the events to fire end - def rm_f(files) - super - wait - end - test "notifies forked processes" do jruby_skip "Forking not available on JRuby" @@ -43,7 +39,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase checker = new_checker(tmpfiles) {} assert !checker.updated? - # Pipes used for flow controll across fork. + # Pipes used for flow control across fork. boot_reader, boot_writer = IO.pipe touch_reader, touch_writer = IO.pipe diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb index 48cd387196..361e7e2349 100644 --- a/activesupport/test/file_update_checker_shared_tests.rb +++ b/activesupport/test/file_update_checker_shared_tests.rb @@ -273,4 +273,10 @@ module FileUpdateCheckerSharedTests assert checker.execute_if_updated assert_equal 2, i end + + test "initialize raises an ArgumentError if no block given" do + assert_raise ArgumentError do + new_checker([]) + end + end end diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 8d39303f9b..03d7b3fe94 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -271,35 +271,25 @@ class InflectorTest < ActiveSupport::TestCase end end - # FIXME: get following tests to pass on jruby, currently skipped - # - # Currently this fails because ActiveSupport::Multibyte::Unicode#tidy_bytes - # required a specific Encoding::Converter(UTF-8 to UTF8-MAC) which unavailable on JRuby - # causing our tests to error out. - # related bug http://jira.codehaus.org/browse/JRUBY-7194 def test_parameterize - jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterized.each do |some_string, parameterized_string| assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string)) end end def test_parameterize_and_normalize - jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterizedAndNormalized.each do |some_string, parameterized_string| assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string)) end end def test_parameterize_with_custom_separator - jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterizeWithUnderscore.each do |some_string, parameterized_string| assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, separator: "_")) end end def test_parameterize_with_multi_character_separator - jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable" StringToParameterized.each do |some_string, parameterized_string| assert_equal(parameterized_string.gsub("-", "__sep__"), ActiveSupport::Inflector.parameterize(some_string, separator: "__sep__")) end diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index 6d1d8f1b95..6f5051c312 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -75,12 +75,17 @@ class TestJSONDecoding < ActiveSupport::TestCase } TESTS.each_with_index do |(json, expected), index| + fail_message = "JSON decoding failed for #{json}" + test "json decodes #{index}" do with_tz_default "Eastern Time (US & Canada)" do with_parse_json_times(true) do silence_warnings do - assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \ - failed for #{json}" + if expected.nil? + assert_nil ActiveSupport::JSON.decode(json), fail_message + else + assert_equal expected, ActiveSupport::JSON.decode(json), fail_message + end end end end diff --git a/activesupport/test/json/encoding_test_cases.rb b/activesupport/test/json/encoding_test_cases.rb index 8eac246937..7e4775cec8 100644 --- a/activesupport/test/json/encoding_test_cases.rb +++ b/activesupport/test/json/encoding_test_cases.rb @@ -1,4 +1,8 @@ require "bigdecimal" +require "date" +require "time" +require "pathname" +require "uri" module JSONTest class Foo @@ -23,7 +27,7 @@ module JSONTest end end - class MyStruct < Struct.new(:name, :value) + MyStruct = Struct.new(:name, :value) do def initialize(*) @unused = "unused instance variable" super diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 68ce42bf72..d80d340986 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -50,7 +50,7 @@ class MultibyteCharsTest < ActiveSupport::TestCase assert_equal BYTE_STRING.length, BYTE_STRING.mb_chars.length end - def test_forwarded_method_with_non_string_result_should_be_returned_vertabim + def test_forwarded_method_with_non_string_result_should_be_returned_verbatim str = "" str.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end } @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end } @@ -664,7 +664,6 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase end def test_tidy_bytes_should_tidy_bytes - single_byte_cases = { "\x21" => "!", # Valid ASCII byte, low "\x41" => "A", # Valid ASCII byte, mid diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb index 2201860d8a..a70516bb08 100644 --- a/activesupport/test/multibyte_test_helpers.rb +++ b/activesupport/test/multibyte_test_helpers.rb @@ -18,7 +18,7 @@ module MultibyteTestHelpers end UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd" - CACHE_DIR = "#{Dir.tmpdir}/cache/unicode_conformance" + CACHE_DIR = "#{Dir.tmpdir}/cache/unicode_conformance/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}" FileUtils.mkdir_p(CACHE_DIR) UNICODE_STRING = "こにちわ".freeze diff --git a/activesupport/test/number_helper_i18n_test.rb b/activesupport/test/number_helper_i18n_test.rb index 4f58e6607a..a1d1c41dc2 100644 --- a/activesupport/test/number_helper_i18n_test.rb +++ b/activesupport/test/number_helper_i18n_test.rb @@ -1,5 +1,6 @@ require "abstract_unit" require "active_support/number_helper" +require "active_support/core_ext/hash/keys" module ActiveSupport class NumberHelperI18nTest < ActiveSupport::TestCase diff --git a/activesupport/test/reloader_test.rb b/activesupport/test/reloader_test.rb index 67d8c4b0e3..bdd80307c7 100644 --- a/activesupport/test/reloader_test.rb +++ b/activesupport/test/reloader_test.rb @@ -2,12 +2,15 @@ require "abstract_unit" class ReloaderTest < ActiveSupport::TestCase def test_prepare_callback - prepared = false + prepared = completed = false reloader.to_prepare { prepared = true } + reloader.to_complete { completed = true } assert !prepared + assert !completed reloader.prepare! assert prepared + assert !completed prepared = false reloader.wrap do @@ -17,6 +20,15 @@ class ReloaderTest < ActiveSupport::TestCase assert !prepared end + def test_prepend_prepare_callback + i = 10 + reloader.to_prepare { i += 1 } + reloader.to_prepare(prepend: true) { i = 0 } + + reloader.prepare! + assert_equal 1, i + end + def test_only_run_when_check_passes r = new_reloader { true } invoked = false diff --git a/activesupport/test/security_utils_test.rb b/activesupport/test/security_utils_test.rb index 842bdd469d..e8f762da22 100644 --- a/activesupport/test/security_utils_test.rb +++ b/activesupport/test/security_utils_test.rb @@ -4,6 +4,11 @@ require "active_support/security_utils" class SecurityUtilsTest < ActiveSupport::TestCase def test_secure_compare_should_perform_string_comparison assert ActiveSupport::SecurityUtils.secure_compare("a", "a") - assert !ActiveSupport::SecurityUtils.secure_compare("a", "b") + assert_not ActiveSupport::SecurityUtils.secure_compare("a", "b") + end + + def test_variable_size_secure_compare_should_perform_string_comparison + assert ActiveSupport::SecurityUtils.variable_size_secure_compare("a", "a") + assert_not ActiveSupport::SecurityUtils.variable_size_secure_compare("a", "b") end end diff --git a/activesupport/test/string_inquirer_test.rb b/activesupport/test/string_inquirer_test.rb index d41e4d6800..79a715349c 100644 --- a/activesupport/test/string_inquirer_test.rb +++ b/activesupport/test/string_inquirer_test.rb @@ -20,4 +20,25 @@ class StringInquirerTest < ActiveSupport::TestCase def test_respond_to assert_respond_to @string_inquirer, :development? end + + def test_respond_to_fallback_to_string_respond_to + String.class_eval do + def respond_to_missing?(name, include_private = false) + (name == :bar) || super + end + end + str = ActiveSupport::StringInquirer.new("hello") + + assert_respond_to str, :are_you_ready? + assert_respond_to str, :bar + assert_not_respond_to str, :nope + + ensure + String.class_eval do + undef_method :respond_to_missing? + def respond_to_missing?(name, include_private = false) + super + end + end + end end diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb index c68be329bc..e0d3fb0cf5 100644 --- a/activesupport/test/time_travel_test.rb +++ b/activesupport/test/time_travel_test.rb @@ -3,6 +3,10 @@ require "active_support/core_ext/date_time" require "active_support/core_ext/numeric/time" class TimeTravelTest < ActiveSupport::TestCase + class TimeSubclass < ::Time; end + class DateSubclass < ::Date; end + class DateTimeSubclass < ::DateTime; end + def test_time_helper_travel Time.stub(:now, Time.now) do begin @@ -90,11 +94,12 @@ class TimeTravelTest < ActiveSupport::TestCase outer_expected_time = Time.new(2004, 11, 24, 01, 04, 44) inner_expected_time = Time.new(2004, 10, 24, 01, 04, 44) travel_to outer_expected_time do - assert_raises(RuntimeError, /Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing./) do + e = assert_raises(RuntimeError) do travel_to(inner_expected_time) do #noop end end + assert_match(/Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing./, e.message) end end end @@ -142,4 +147,19 @@ class TimeTravelTest < ActiveSupport::TestCase end end end + + def test_time_helper_travel_with_time_subclass + assert_equal TimeSubclass, TimeSubclass.now.class + assert_equal DateSubclass, DateSubclass.today.class + assert_equal DateTimeSubclass, DateTimeSubclass.now.class + + travel 1.day do + assert_equal TimeSubclass, TimeSubclass.now.class + assert_equal DateSubclass, DateSubclass.today.class + assert_equal DateTimeSubclass, DateTimeSubclass.now.class + assert_equal Time.now.to_s, TimeSubclass.now.to_s + assert_equal Date.today.to_s, DateSubclass.today.to_s + assert_equal DateTime.now.to_s, DateTimeSubclass.now.to_s + end + end end diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb index 040ddd25fc..466b69bcef 100644 --- a/activesupport/test/transliterate_test.rb +++ b/activesupport/test/transliterate_test.rb @@ -31,4 +31,22 @@ class TransliterateTest < ActiveSupport::TestCase def test_transliterate_should_allow_a_custom_replacement_char assert_equal "a*b", ActiveSupport::Inflector.transliterate("a索b", "*") end + + def test_transliterate_handles_empty_string + assert_equal "", ActiveSupport::Inflector.transliterate("") + end + + def test_transliterate_handles_nil + exception = assert_raises ArgumentError do + ActiveSupport::Inflector.transliterate(nil) + end + assert_equal "Can only transliterate strings. Received NilClass", exception.message + end + + def test_transliterate_handles_unknown_object + exception = assert_raises ArgumentError do + ActiveSupport::Inflector.transliterate(Object.new) + end + assert_equal "Can only transliterate strings. Received Object", exception.message + end end diff --git a/ci/travis.rb b/ci/travis.rb index c49a87d864..f59ce5406a 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -154,7 +154,6 @@ ENV["GEM"].split(",").each do |gem| build = Build.new(gem, isolated: isolated) results[build.key] = build.run! - end end diff --git a/guides/Rakefile b/guides/Rakefile index d2591f523c..ccb42f3273 100644 --- a/guides/Rakefile +++ b/guides/Rakefile @@ -1,10 +1,8 @@ namespace :guides do - desc 'Generate guides (for authors), use ONLY=foo to process just "foo.md"' task generate: "generate:html" namespace :generate do - desc "Generate HTML guides" task :html do ENV["WARNINGS"] = "1" # authors can't disable this @@ -18,7 +16,7 @@ namespace :guides do abort "Please run `setupkindlerb` to install kindlegen" end unless `convert` =~ /convert/ - abort "Please install ImageMagick`" + abort "Please install ImageMagick" end ENV["KINDLE"] = "1" Rake::Task["guides:generate:html"].invoke @@ -33,7 +31,7 @@ namespace :guides do desc "Show help" task :help do - puts <<-help + puts <<HELP Guides are taken from the source directory, and the result goes into the output directory. Assets are stored under files, and copied to output/files as @@ -46,8 +44,9 @@ All of these processes are handled via rake tasks, here's a full list of them: #{%x[rake -T]} Some arguments may be passed via environment variables: - WARNINGS=1 - Internal links (anchors) are checked, also detects duplicated IDs. + RAILS_VERSION=tag + If guides are being generated for a specific Rails version set the Git tag + here, otherwise the current SHA1 is going to be used to generate edge guides. ALL=1 Force generation of all guides. @@ -65,15 +64,12 @@ Some arguments may be passed via environment variables: Use it when you want to generate translated guides in source/<GUIDES_LANGUAGE> folder (such as source/es) - EDGE=1 - Indicate generated guides should be marked as edge. - Examples: - $ rake guides:generate ALL=1 - $ rake guides:generate EDGE=1 - $ rake guides:generate:kindle EDGE=1 + $ rake guides:generate ALL=1 RAILS_VERSION=v5.1.0 + $ rake guides:generate ONLY=migrations + $ rake guides:generate:kindle $ rake guides:generate GUIDES_LANGUAGE=es - help +HELP end end diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 960d269d90..85d698f81b 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "rails", "5.0.0" + gem "rails", "5.0.1" end require "rack/test" diff --git a/guides/bug_report_templates/active_job_gem.rb b/guides/bug_report_templates/active_job_gem.rb index debc46ad54..f715caec95 100644 --- a/guides/bug_report_templates/active_job_gem.rb +++ b/guides/bug_report_templates/active_job_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activejob", "5.0.0" + gem "activejob", "5.0.1" end require "minitest/autorun" diff --git a/guides/bug_report_templates/active_job_master.rb b/guides/bug_report_templates/active_job_master.rb index 7591470440..f61518713f 100644 --- a/guides/bug_report_templates/active_job_master.rb +++ b/guides/bug_report_templates/active_job_master.rb @@ -8,7 +8,6 @@ end gemfile(true) do source "https://rubygems.org" gem "rails", github: "rails/rails" - gem "arel", github: "rails/arel" end require "active_job" diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index e18302fe65..98edcb76b1 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activerecord", "5.0.0" + gem "activerecord", "5.0.1" gem "sqlite3" end diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb index 8bbc1ef19e..7265a671b0 100644 --- a/guides/bug_report_templates/active_record_master.rb +++ b/guides/bug_report_templates/active_record_master.rb @@ -8,7 +8,6 @@ end gemfile(true) do source "https://rubygems.org" gem "rails", github: "rails/rails" - gem "arel", github: "rails/arel" gem "sqlite3" end diff --git a/guides/bug_report_templates/active_record_migrations_gem.rb b/guides/bug_report_templates/active_record_migrations_gem.rb index ba80e6b4ad..ece6ae4f12 100644 --- a/guides/bug_report_templates/active_record_migrations_gem.rb +++ b/guides/bug_report_templates/active_record_migrations_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activerecord", "5.0.0.1" + gem "activerecord", "5.0.1" gem "sqlite3" end diff --git a/guides/bug_report_templates/active_record_migrations_master.rb b/guides/bug_report_templates/active_record_migrations_master.rb index 84a4b71909..13a375d1ba 100644 --- a/guides/bug_report_templates/active_record_migrations_master.rb +++ b/guides/bug_report_templates/active_record_migrations_master.rb @@ -8,7 +8,6 @@ end gemfile(true) do source "https://rubygems.org" gem "rails", github: "rails/rails" - gem "arel", github: "rails/arel" gem "sqlite3" end diff --git a/guides/bug_report_templates/benchmark.rb b/guides/bug_report_templates/benchmark.rb index a0b541d012..54433b34dd 100644 --- a/guides/bug_report_templates/benchmark.rb +++ b/guides/bug_report_templates/benchmark.rb @@ -8,7 +8,6 @@ end gemfile(true) do source "https://rubygems.org" gem "rails", github: "rails/rails" - gem "arel", github: "rails/arel" gem "benchmark-ips" end diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb index a94848e25b..fa9f94eea0 100644 --- a/guides/bug_report_templates/generic_gem.rb +++ b/guides/bug_report_templates/generic_gem.rb @@ -8,7 +8,7 @@ end gemfile(true) do source "https://rubygems.org" # Activate the gem you are reporting the issue against. - gem "activesupport", "5.0.0" + gem "activesupport", "5.0.1" end require "active_support/core_ext/object/blank" diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb index ed45726e92..d3a7ae4ac4 100644 --- a/guides/bug_report_templates/generic_master.rb +++ b/guides/bug_report_templates/generic_master.rb @@ -8,7 +8,6 @@ end gemfile(true) do source "https://rubygems.org" gem "rails", github: "rails/rails" - gem "arel", github: "rails/arel" end require "active_support" diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb index 557d23f78c..0f611c8f2b 100644 --- a/guides/rails_guides.rb +++ b/guides/rails_guides.rb @@ -1,17 +1,27 @@ -pwd = File.dirname(__FILE__) -$:.unshift pwd +$:.unshift __dir__ -begin - # Guides generation in the Rails repo. - as_lib = File.join(pwd, "../activesupport/lib") - ap_lib = File.join(pwd, "../actionpack/lib") +as_lib = File.expand_path("../activesupport/lib", __dir__) +ap_lib = File.expand_path("../actionpack/lib", __dir__) +av_lib = File.expand_path("../actionview/lib", __dir__) - $:.unshift as_lib if File.directory?(as_lib) - $:.unshift ap_lib if File.directory?(ap_lib) -rescue LoadError - # Guides generation from gems. - gem "actionpack", ">= 3.0" -end +$:.unshift as_lib if File.directory?(as_lib) +$:.unshift ap_lib if File.directory?(ap_lib) +$:.unshift av_lib if File.directory?(av_lib) require "rails_guides/generator" -RailsGuides::Generator.new.generate +require "active_support/core_ext/object/blank" + +env_value = ->(name) { ENV[name].presence } +env_flag = ->(name) { "1" == env_value[name] } + +version = env_value["RAILS_VERSION"] +edge = `git rev-parse HEAD`.strip unless version + +RailsGuides::Generator.new( + edge: edge, + version: version, + all: env_flag["ALL"], + only: env_value["ONLY"], + kindle: env_flag["KINDLE"], + language: env_value["GUIDES_LANGUAGE"] +).generate diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb index a818ca9d72..28164a3cb4 100644 --- a/guides/rails_guides/generator.rb +++ b/guides/rails_guides/generator.rb @@ -1,54 +1,3 @@ -# --------------------------------------------------------------------------- -# -# This script generates the guides. It can be invoked via the -# guides:generate rake task within the guides directory. -# -# Guides are taken from the source directory, and the resulting HTML goes into the -# output directory. Assets are stored under files, and copied to output/files as -# part of the generation process. -# -# Some arguments may be passed via environment variables: -# -# WARNINGS -# If you are writing a guide, please work always with WARNINGS=1. Users can -# generate the guides, and thus this flag is off by default. -# -# Internal links (anchors) are checked. If a reference is broken levenshtein -# distance is used to suggest an existing one. This is useful since IDs are -# generated by Markdown from headers and thus edits alter them. -# -# Also detects duplicated IDs. They happen if there are headers with the same -# text. Please do resolve them, if any, so guides are valid XHTML. -# -# ALL -# Set to "1" to force the generation of all guides. -# -# ONLY -# Use ONLY if you want to generate only one or a set of guides. Prefixes are -# enough: -# -# # generates only association_basics.html -# ONLY=assoc rake guides:generate -# -# Separate many using commas: -# -# # generates only association_basics.html and command_line.html -# ONLY=assoc,command rake guides:generate -# -# Note that if you are working on a guide generation will by default process -# only that one, so ONLY is rarely used nowadays. -# -# GUIDES_LANGUAGE -# Use GUIDES_LANGUAGE when you want to generate translated guides in -# <tt>source/<GUIDES_LANGUAGE></tt> folder (such as <tt>source/es</tt>). -# Ignore it when generating English guides. -# -# EDGE -# Set to "1" to indicate generated guides should be marked as edge. This -# inserts a badge and changes the preamble of the home page. -# -# --------------------------------------------------------------------------- - require "set" require "fileutils" @@ -64,46 +13,37 @@ require "rails_guides/levenshtein" module RailsGuides class Generator - attr_reader :guides_dir, :source_dir, :output_dir, :edge, :warnings, :all - GUIDES_RE = /\.(?:erb|md)\z/ - def initialize(output = nil) - set_flags_from_environment + def initialize(edge:, version:, all:, only:, kindle:, language:) + @edge = edge + @version = version + @all = all + @only = only + @kindle = kindle + @language = language - if kindle? + if @kindle check_for_kindlegen register_kindle_mime_types end - initialize_dirs(output) + initialize_dirs create_output_dir_if_needed - end - - def set_flags_from_environment - @edge = ENV["EDGE"] == "1" - @warnings = ENV["WARNINGS"] == "1" - @all = ENV["ALL"] == "1" - @kindle = ENV["KINDLE"] == "1" - @version = ENV["RAILS_VERSION"] || "local" - @lang = ENV["GUIDES_LANGUAGE"] - end - - def register_kindle_mime_types - Mime::Type.register_alias("application/xml", :opf, %w(opf)) - Mime::Type.register_alias("application/xml", :ncx, %w(ncx)) + initialize_markdown_renderer end def generate generate_guides copy_assets - generate_mobi if kindle? + generate_mobi if @kindle end private - def kindle? - @kindle + def register_kindle_mime_types + Mime::Type.register_alias("application/xml", :opf, %w(opf)) + Mime::Type.register_alias("application/xml", :ncx, %w(ncx)) end def check_for_kindlegen @@ -114,29 +54,35 @@ module RailsGuides def generate_mobi require "rails_guides/kindle" - out = "#{output_dir}/kindlegen.out" - Kindle.generate(output_dir, mobi, out) + out = "#{@output_dir}/kindlegen.out" + Kindle.generate(@output_dir, mobi, out) puts "(kindlegen log at #{out})." end def mobi - "ruby_on_rails_guides_#@version%s.mobi" % (@lang.present? ? ".#@lang" : "") + mobi = "ruby_on_rails_guides_#{@version || @edge[0, 7]}" + mobi += ".#{@language}" if @language + mobi += ".mobi" end - def initialize_dirs(output) - @guides_dir = File.join(File.dirname(__FILE__), "..") - @source_dir = "#@guides_dir/source/#@lang" - @output_dir = if output - output - elsif kindle? - "#@guides_dir/output/kindle/#@lang" - else - "#@guides_dir/output/#@lang" - end.sub(%r</$>, "") + def initialize_dirs + @guides_dir = File.expand_path("..", __dir__) + + @source_dir = "#{@guides_dir}/source" + @source_dir += "/#{@language}" if @language + + @output_dir = "#{@guides_dir}/output" + @output_dir += "/kindle" if @kindle + @source_dir += "/#{@language}" if @language end def create_output_dir_if_needed - FileUtils.mkdir_p(output_dir) + FileUtils.mkdir_p(@output_dir) + end + + def initialize_markdown_renderer + Markdown::Renderer.edge = @edge + Markdown::Renderer.version = @version end def generate_guides @@ -147,27 +93,27 @@ module RailsGuides end def guides_to_generate - guides = Dir.entries(source_dir).grep(GUIDES_RE) + guides = Dir.entries(@source_dir).grep(GUIDES_RE) - if kindle? - Dir.entries("#{source_dir}/kindle").grep(GUIDES_RE).map do |entry| + if @kindle + Dir.entries("#{@source_dir}/kindle").grep(GUIDES_RE).map do |entry| next if entry == "KINDLE.md" guides << "kindle/#{entry}" end end - ENV.key?("ONLY") ? select_only(guides) : guides + @only ? select_only(guides) : guides end def select_only(guides) - prefixes = ENV["ONLY"].split(",").map(&:strip) + prefixes = @only.split(",").map(&:strip) guides.select do |guide| - guide.start_with?("kindle".freeze, *prefixes) + guide.start_with?("kindle", *prefixes) end end def copy_assets - FileUtils.cp_r(Dir.glob("#{guides_dir}/assets/*"), output_dir) + FileUtils.cp_r(Dir.glob("#{@guides_dir}/assets/*"), @output_dir) end def output_file_for(guide) @@ -179,22 +125,28 @@ module RailsGuides end def output_path_for(output_file) - File.join(output_dir, File.basename(output_file)) + File.join(@output_dir, File.basename(output_file)) end def generate?(source_file, output_file) - fin = File.join(source_dir, source_file) + fin = File.join(@source_dir, source_file) fout = output_path_for(output_file) - all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin) + @all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin) end def generate_guide(guide, output_file) output_path = output_path_for(output_file) puts "Generating #{guide} as #{output_file}" - layout = kindle? ? "kindle/layout" : "layout" + layout = @kindle ? "kindle/layout" : "layout" File.open(output_path, "w") do |f| - view = ActionView::Base.new(source_dir, edge: @edge, version: @version, mobi: "kindle/#{mobi}", lang: @lang) + view = ActionView::Base.new( + @source_dir, + edge: @edge, + version: @version, + mobi: "kindle/#{mobi}", + language: @language + ) view.extend(Helpers) if guide =~ /\.(\w+)\.erb$/ @@ -202,10 +154,15 @@ module RailsGuides # Passing a template handler in the template name is deprecated. So pass the file name without the extension. result = view.render(layout: layout, formats: [$1], file: $`) else - body = File.read(File.join(source_dir, guide)) - result = RailsGuides::Markdown.new(view, layout).render(body) - - warn_about_broken_links(result) if @warnings + body = File.read("#{@source_dir}/#{guide}") + result = RailsGuides::Markdown.new( + view: view, + layout: layout, + edge: @edge, + version: @version + ).render(body) + + warn_about_broken_links(result) end f.write(result) @@ -231,7 +188,7 @@ module RailsGuides # Footnotes. anchors += Set.new(html.scan(/<p\s+class="footnote"\s+id="([^"]+)/).flatten) anchors += Set.new(html.scan(/<sup\s+class="footnote"\s+id="([^"]+)/).flatten) - return anchors + anchors end def check_fragment_identifiers(html, anchors) diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb index 009e5aff99..bf2cc82c7c 100644 --- a/guides/rails_guides/markdown.rb +++ b/guides/rails_guides/markdown.rb @@ -4,12 +4,14 @@ require "rails_guides/markdown/renderer" module RailsGuides class Markdown - def initialize(view, layout) - @view = view - @layout = layout + def initialize(view:, layout:, edge:, version:) + @view = view + @layout = layout + @edge = edge + @version = version @index_counter = Hash.new(0) - @raw_header = "" - @node_ids = {} + @raw_header = "" + @node_ids = {} end def render(body) @@ -60,7 +62,8 @@ module RailsGuides autolink: true, strikethrough: true, superscript: true, - tables: true) + tables: true + ) end def extract_raw_header_and_body diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb index deab741023..20cbd568c9 100644 --- a/guides/rails_guides/markdown/renderer.rb +++ b/guides/rails_guides/markdown/renderer.rb @@ -1,9 +1,7 @@ module RailsGuides class Markdown class Renderer < Redcarpet::Render::HTML - def initialize(options = {}) - super - end + cattr_accessor :edge, :version def block_code(code, language) <<-HTML @@ -15,6 +13,16 @@ module RailsGuides HTML end + def link(url, title, content) + if url.start_with?("http://api.rubyonrails.org") + %(<a href="#{api_link(url)}">#{content}</a>) + elsif title + %(<a href="#{url}" title="#{title}">#{content}</a>) + else + %(<a href="#{url}">#{content}</a>) + end + end + def header(text, header_level) # Always increase the heading level by 1, so we can use h1, h2 heading in the document header_level += 1 @@ -23,7 +31,9 @@ HTML end def paragraph(text) - if text =~ /^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:]/ + if text =~ %r{^NOTE:\s+Defined\s+in\s+<code>(.*?)</code>\.?$} + %(<div class="note"><p>Defined in <code><a href="#{github_file_url($1)}">#{$1}</a></code>.</p></div>) + elsif text =~ /^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:]/ convert_notes(text) elsif text.include?("DO NOT READ THIS FILE ON GITHUB") elsif text =~ /^\[<sup>(\d+)\]:<\/sup> (.+)$/ @@ -79,6 +89,33 @@ HTML %(<div class="#{css_class}"><p>#{$2.strip}</p></div>) end end + + def github_file_url(file_path) + tree = version || edge + + root = file_path[%r{(.+)/}, 1] + path = case root + when "abstract_controller", "action_controller", "action_dispatch" + "actionpack/lib/#{file_path}" + when /\A(action|active)_/ + "#{root.sub("_", "")}/lib/#{file_path}" + else + file_path + end + + + "https://github.com/rails/rails/tree/#{tree}/#{path}" + end + + def api_link(url) + if url =~ %r{http://api\.rubyonrails\.org/v\d+\.} + url + elsif edge + url.sub("api", "edgeapi") + else + url.sub(/(?<=\.org)/, "/#{version}") + end + end end end end diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md index 39753cbd6f..5f4be07351 100644 --- a/guides/source/5_0_release_notes.md +++ b/guides/source/5_0_release_notes.md @@ -150,7 +150,7 @@ The type of an attribute is given the opportunity to change how dirty tracking is performed. See its -[documentation](http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html) +[documentation](http://api.rubyonrails.org/v5.0.1/classes/ActiveRecord/Attributes/ClassMethods.html) for a detailed write up. @@ -242,7 +242,7 @@ Please refer to the [Changelog][railties] for detailed changes. [Pull Request](https://github.com/rails/rails/pull/22288)) * New applications are generated with the evented file system monitor enabled - on Linux and Mac OS X. The feature can be opted out by passing + on Linux and macOS. The feature can be opted out by passing `--skip-listen` to the generator. ([commit](https://github.com/rails/rails/commit/de6ad5665d2679944a9ee9407826ba88395a1003), [commit](https://github.com/rails/rails/commit/94dbc48887bf39c241ee2ce1741ee680d773f202)) @@ -499,6 +499,9 @@ Please refer to the [Changelog][action-view] for detailed changes. `datetime-local`. ([Pull Request](https://github.com/rails/rails/pull/25469)) +* Allow blocks while rendering with the `render partial:` helper. + ([Pull Request](https://github.com/rails/rails/pull/17974)) + Action Mailer ------------- diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb index f50bcddbe7..5bd1ea4d22 100644 --- a/guides/source/_welcome.html.erb +++ b/guides/source/_welcome.html.erb @@ -1,8 +1,8 @@ -<h2>Ruby on Rails Guides (<%= @edge ? @version[0, 7] : @version %>)</h2> +<h2>Ruby on Rails Guides (<%= @edge ? @edge[0, 7] : @version %>)</h2> <% if @edge %> <p> - These are <b>Edge Guides</b>, based on the current <a href="https://github.com/rails/rails/tree/<%= @version %>">master</a> branch. + These are <b>Edge Guides</b>, based on <a href="https://github.com/rails/rails/tree/<%= @edge %>">master@<%= @edge[0, 7] %></a>. </p> <p> If you are looking for the ones for the stable version, please check diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md index 319277ef68..e929945dd0 100644 --- a/guides/source/action_cable_overview.md +++ b/guides/source/action_cable_overview.md @@ -240,8 +240,8 @@ WebNotificationsChannel.broadcast_to( ``` The `WebNotificationsChannel.broadcast_to` call places a message in the current -subscription adapter (by default `redis` for production and `async` for development and -test environments)'s pubsub queue under a separate broadcasting name for each user. +subscription adapter (by default `redis` for production and `async` for development and +test environments)'s pubsub queue under a separate broadcasting name for each user. For a user with an ID of 1, the broadcasting name would be `web_notifications:1`. The channel has been instructed to stream everything that arrives at @@ -530,7 +530,7 @@ Action Cable has two required configurations: a subscription adapter and allowed ### Subscription Adapter By default, Action Cable looks for a configuration file in `config/cable.yml`. -The file must specify an adapter and a URL for each Rails environment. See the +The file must specify an adapter for each Rails environment. See the [Dependencies](#dependencies) section for additional information on adapters. ```yaml @@ -543,7 +543,28 @@ test: production: adapter: redis url: redis://10.10.3.153:6381 + channel_prefix: appname_production ``` +#### Adapter Configuration + +Below is a list of the subscription adapters available for end users. + +##### Async Adapter + +The async adapter is intended for development/testing and should not be used in production. + +##### Redis Adapter + +Action Cable contains two Redis adapters: "normal" Redis and Evented Redis. Both +of the adapters require users to provide a URL pointing to the Redis server. +Additionally, a channel_prefix may be provided to avoid channel name collisions +when using the same Redis server for multiple applications. See the [Redis PubSub documentation](https://redis.io/topics/pubsub#database-amp-scoping) for more details. + +##### PostgreSQL Adapter + +The PostgreSQL adapter uses Active Record's connection pool, and thus the +application's `config/database.yml` database configuration, for its connection. +This may change in the future. [#27214](https://github.com/rails/rails/issues/27214) ### Allowed Request Origins diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 0825d54cb7..380fdac658 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -160,8 +160,8 @@ When you call the `mail` method now, Action Mailer will detect the two templates #### Calling the Mailer Mailers are really just another way to render a view. Instead of rendering a -view and sending out the HTTP protocol, they are just sending it out through the -email protocols instead. Due to this, it makes sense to just have your +view and sending it over the HTTP protocol, they are just sending it out through +the email protocols instead. Due to this, it makes sense to just have your controller tell the Mailer to send an email when a user is successfully created. Setting this up is painfully simple. @@ -525,7 +525,7 @@ By using the full URL, your links will now work in your emails. #### Generating URLs with `url_for` -`url_for` generate full URL by default in templates. +`url_for` generates a full URL by default in templates. If you did not configure the `:host` option globally make sure to pass it to `url_for`. @@ -574,7 +574,7 @@ Now you can display an image inside your email. ### Sending Multipart Emails Action Mailer will automatically send multipart emails if you have different -templates for the same action. So, for our UserMailer example, if you have +templates for the same action. So, for our `UserMailer` example, if you have `welcome_email.text.erb` and `welcome_email.html.erb` in `app/views/user_mailer`, Action Mailer will automatically send a multipart email with the HTML and text versions setup as different parts. diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 868daf2435..77bd3c97e8 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -202,11 +202,9 @@ The following methods trigger callbacks: * `create` * `create!` -* `decrement!` * `destroy` * `destroy!` * `destroy_all` -* `increment!` * `save` * `save!` * `save(validate: false)` @@ -290,7 +288,7 @@ Article destroyed Conditional Callbacks --------------------- -As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a string, a `Proc` or an `Array`. You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option. +As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a `Proc` or an `Array`. You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option. ### Using `:if` and `:unless` with a `Symbol` @@ -302,16 +300,6 @@ class Order < ApplicationRecord end ``` -### Using `:if` and `:unless` with a String - -You can also use a string that will be evaluated using `eval` and hence needs to contain valid Ruby code. You should use this option only when the string represents a really short condition: - -```ruby -class Order < ApplicationRecord - before_save :normalize_card_number, if: "paid_with_card?" -end -``` - ### Using `:if` and `:unless` with a `Proc` Finally, it is possible to associate `:if` and `:unless` with a `Proc` object. This option is best suited when writing short validation methods, usually one-liners: diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index 665e97c470..32b38cde5e 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -916,18 +916,6 @@ class Order < ApplicationRecord end ``` -### Using a String with `:if` and `:unless` - -You can also use a string that will be evaluated using `eval` and needs to -contain valid Ruby code. You should use this option only when the string -represents a really short condition. - -```ruby -class Person < ApplicationRecord - validates :surname, presence: true, if: "name.nil?" -end -``` - ### Using a Proc with `:if` and `:unless` Finally, it's possible to associate `:if` and `:unless` with a `Proc` object diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 25717e04e4..68dde4482f 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -78,9 +78,9 @@ requests can mean faster loading for your application. Sprockets concatenates all JavaScript files into one master `.js` file and all CSS files into one master `.css` file. As you'll learn later in this guide, you can customize this strategy to group files any way you like. In production, -Rails inserts an MD5 fingerprint into each filename so that the file is cached -by the web browser. You can invalidate the cache by altering this fingerprint, -which happens automatically whenever you change the file contents. +Rails inserts an SHA256 fingerprint into each filename so that the file is +cached by the web browser. You can invalidate the cache by altering this +fingerprint, which happens automatically whenever you change the file contents. The second feature of the asset pipeline is asset minification or compression. For CSS files, this is done by removing whitespace and comments. For JavaScript, @@ -106,7 +106,7 @@ or in web browsers) to keep their own copy of the content. When the content is updated, the fingerprint will change. This will cause the remote clients to request a new copy of the content. This is generally known as _cache busting_. -The technique sprockets uses for fingerprinting is to insert a hash of the +The technique Sprockets uses for fingerprinting is to insert a hash of the content into the name, usually at the end. For example a CSS file `global.css` ``` @@ -207,7 +207,7 @@ default .coffee and .scss files will not be precompiled on their own. See precompiling works. NOTE: You must have an ExecJS supported runtime in order to use CoffeeScript. -If you are using Mac OS X or Windows, you have a JavaScript runtime installed in +If you are using macOS or Windows, you have a JavaScript runtime installed in your operating system. Check [ExecJS](https://github.com/rails/execjs#readme) documentation to know all supported JavaScript runtimes. You can also disable generation of controller specific asset files by adding the @@ -346,9 +346,9 @@ Provided that the pipeline is enabled within your application (and not disabled in the current environment context), this file is served by Sprockets. If a file exists at `public/assets/rails.png` it is served by the web server. -Alternatively, a request for a file with an MD5 hash such as -`public/assets/rails-af27b6a414e6da00003503148be9b409.png` is treated the same -way. How these hashes are generated is covered in the [In +Alternatively, a request for a file with an SHA256 hash such as +`public/assets/rails-f90d8a84c707a8dc923fca1ca1895ae8ed0a09237f6992015fef1e11be77c023.png` +is treated the same way. How these hashes are generated is covered in the [In Production](#in-production) section later on in this guide. Sprockets will also look through the paths specified in `config.assets.paths`, @@ -654,7 +654,7 @@ In the production environment Sprockets uses the fingerprinting scheme outlined above. By default Rails assumes assets have been precompiled and will be served as static assets by your web server. -During the precompilation phase an MD5 is generated from the contents of the +During the precompilation phase an SHA256 is generated from the contents of the compiled files, and inserted into the filenames as they are written to disk. These fingerprinted names are used by the Rails helpers in place of the manifest name. @@ -743,22 +743,24 @@ Rails.application.config.assets.precompile += %w( admin.js admin.css ) NOTE. Always specify an expected compiled filename that ends with .js or .css, even if you want to add Sass or CoffeeScript files to the precompile array. -The task also generates a `manifest-md5hash.json` that contains a list with -all your assets and their respective fingerprints. This is used by the Rails -helper methods to avoid handing the mapping requests back to Sprockets. A -typical manifest file looks like: +The task also generates a `.sprockets-manifest-md5hash.json` (where `md5hash` is +an MD5 hash) that contains a list with all your assets and their respective +fingerprints. This is used by the Rails helper methods to avoid handing the +mapping requests back to Sprockets. A typical manifest file looks like: ```ruby -{"files":{"application-723d1be6cc741a3aabb1cec24276d681.js":{"logical_path":"application.js","mtime":"2013-07-26T22:55:03-07:00","size":302506, -"digest":"723d1be6cc741a3aabb1cec24276d681"},"application-12b3c7dd74d2e9df37e7cbb1efa76a6d.css":{"logical_path":"application.css","mtime":"2013-07-26T22:54:54-07:00","size":1560, -"digest":"12b3c7dd74d2e9df37e7cbb1efa76a6d"},"application-1c5752789588ac18d7e1a50b1f0fd4c2.css":{"logical_path":"application.css","mtime":"2013-07-26T22:56:17-07:00","size":1591, -"digest":"1c5752789588ac18d7e1a50b1f0fd4c2"},"favicon-a9c641bf2b81f0476e876f7c5e375969.ico":{"logical_path":"favicon.ico","mtime":"2013-07-26T23:00:10-07:00","size":1406, -"digest":"a9c641bf2b81f0476e876f7c5e375969"},"my_image-231a680f23887d9dd70710ea5efd3c62.png":{"logical_path":"my_image.png","mtime":"2013-07-26T23:00:27-07:00","size":6646, -"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets":{"application.js": -"application-723d1be6cc741a3aabb1cec24276d681.js","application.css": -"application-1c5752789588ac18d7e1a50b1f0fd4c2.css", -"favicon.ico":"favicona9c641bf2b81f0476e876f7c5e375969.ico","my_image.png": -"my_image-231a680f23887d9dd70710ea5efd3c62.png"}} +{"files":{"application-aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b.js":{"logical_path":"application.js","mtime":"2016-12-23T20:12:03-05:00","size":412383, +"digest":"aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b","integrity":"sha256-ruS+cfEogDeueLmX3ziDMu39JGRxtTPc7aqPn+FWRCs="}, +"application-86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18.css":{"logical_path":"application.css","mtime":"2016-12-23T19:12:20-05:00","size":2994, +"digest":"86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18","integrity":"sha256-hqKStQcHk8N+LA5fOfc7s4dkTq6tp/lub8BAoCixbBg="}, +"favicon-8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda.ico":{"logical_path":"favicon.ico","mtime":"2016-12-23T20:11:00-05:00","size":8629, +"digest":"8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda","integrity":"sha256-jSOHuNTTLOzZP6OQDfDp/4nQGqzYT1DngMF8n2s9Dto="}, +"my_image-f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493.png":{"logical_path":"my_image.png","mtime":"2016-12-23T20:10:54-05:00","size":23414, +"digest":"f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493","integrity":"sha256-9AKBVv1+ygNYTV8vwEcN8eDbxzaequY4sv8DP5iOxJM="}}, +"assets":{"application.js":"application-aee4be71f1288037ae78b997df388332edfd246471b533dcedaa8f9fe156442b.js", +"application.css":"application-86a292b5070793c37e2c0e5f39f73bb387644eaeada7f96e6fc040a028b16c18.css", +"favicon.ico":"favicon-8d2387b8d4d32cecd93fa3900df0e9ff89d01aacd84f50e780c17c9f6b3d0eda.ico", +"my_image.png":"my_image-f4028156fd7eca03584d5f2fc0470df1e0dbc7369eaae638b2ff033f988ec493.png"}} ``` The default location for the manifest is the root of the location specified in @@ -850,7 +852,7 @@ config.assets.compile = true On the first request the assets are compiled and cached as outlined in development above, and the manifest names used in the helpers are altered to -include the MD5 hash. +include the SHA256 hash. Sprockets also sets the `Cache-Control` HTTP header to `max-age=31536000`. This signals all caches between your server and the client browser that this content @@ -1115,7 +1117,7 @@ config.assets.js_compressor = :uglifier ``` NOTE: You will need an [ExecJS](https://github.com/rails/execjs#readme) -supported runtime in order to use `uglifier`. If you are using Mac OS X or +supported runtime in order to use `uglifier`. If you are using macOS or Windows you have a JavaScript runtime installed in your operating system. diff --git a/guides/source/configuring.md b/guides/source/configuring.md index b0334bfe4a..de921e2705 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -108,7 +108,7 @@ application. Accepts a valid week day symbol (e.g. `:monday`). you don't want shown in the logs, such as passwords or credit card numbers. By default, Rails filters out passwords by adding `Rails.application.config.filter_parameters += [:password]` in `config/initializers/filter_parameter_logging.rb`. Parameters filter works by partial matching regular expression. -* `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://edgeapi.rubyonrails.org/classes/ActionDispatch/SSL.html) for details. +* `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://api.rubyonrails.org/classes/ActionDispatch/SSL.html) for details. * `config.log_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes. If you are setting a value for `config.logger` you must manually pass the value of your formatter to your logger before it is wrapped in an `ActiveSupport::TaggedLogging` instance, Rails will not do it for you. @@ -368,6 +368,11 @@ All these configuration options are delegated to the `I18n` library. has_many relationships to be displayed with an index as well as the error. Defaults to `false`. +* `config.active_record.use_schema_cache_dump` enables users to get schema cache information + from `db/schema_cache.yml` (generated by `bin/rails db:schema:cache:dump`), instead of + having to send a query to the database to get this information. + Defaults to `true`. + The MySQL adapter adds one additional configuration option: * `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns as booleans. Defaults to `true`. @@ -626,8 +631,6 @@ There are a few configuration options available in Active Support: * `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`. -* `ActiveSupport.halt_callback_chains_on_return_false` specifies whether Active Record and Active Model callback chains can be halted by returning `false` in a 'before' callback. When set to `false`, callback chains are halted only when explicitly done so with `throw(:abort)`. When set to `true`, callback chains are halted when a callback returns `false` (the previous behavior before Rails 5) and a deprecation warning is given. Defaults to `true` during the deprecation period. New Rails 5 apps generate an initializer file called `new_framework_defaults.rb` which sets the value to `false`. This file is *not* added when running `rails app:update`, so returning `false` will still work on older apps ported to Rails 5 and display a deprecation warning to prompt users to update their code. - * `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`. * `ActiveSupport::Cache::Store.logger` specifies the logger to use within cache store operations. @@ -1305,7 +1308,7 @@ end Otherwise, in every request Rails walks the application tree to check if anything has changed. -On Linux and Mac OS X no additional gems are needed, but some are required +On Linux and macOS no additional gems are needed, but some are required [for *BSD](https://github.com/guard/listen#on-bsd) and [for Windows](https://github.com/guard/listen#on-windows). diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 830a546570..fe5437ae5d 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -67,7 +67,7 @@ can expect it to be marked "invalid" as soon as it's reviewed. Sometimes, the line between 'bug' and 'feature' is a hard one to draw. Generally, a feature is anything that adds new behavior, while a bug is anything that causes incorrect behavior. Sometimes, -the core team will have to make a judgement call. That said, the distinction +the core team will have to make a judgment call. That said, the distinction generally just affects which release your patch will get in to; we love feature submissions! They just won't get backported to maintenance branches. @@ -335,10 +335,12 @@ file. #### Testing Active Record -First, create the databases you'll need. For MySQL and PostgreSQL, -running the SQL statements `create database activerecord_unittest` and -`create database activerecord_unittest2` is sufficient. This is not -necessary for SQLite3. +First, create the databases you'll need. You can find a list of the required +table names, usernames, and passwords in `activerecord/test/config.example.yml`. + +For MySQL and PostgreSQL, running the SQL statements `create database +activerecord_unittest` and `create database activerecord_unittest2` is +sufficient. This is not necessary for SQLite3. This is how you run the Active Record test suite only for SQLite3: diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index df3003a6a8..ba0cdbf3af 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -683,7 +683,7 @@ Ruby instruction to be executed -- in this case, Active Support's `week` method. 51: # 52: # 2.weeks # => 14 days 53: def weeks -=> 54: ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]]) +=> 54: ActiveSupport::Duration.weeks(self) 55: end 56: alias :week :weeks 57: diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md index 16c7e782bc..7ec038eb4d 100644 --- a/guides/source/development_dependencies_install.md +++ b/guides/source/development_dependencies_install.md @@ -46,7 +46,7 @@ $ cd rails The test suite must pass with any submitted code. No matter whether you are writing a new patch, or evaluating someone else's, you need to be able to run the tests. -Install first SQLite3 and its development files for the `sqlite3` gem. Mac OS X +Install first SQLite3 and its development files for the `sqlite3` gem. On macOS users are done with: ```bash diff --git a/guides/source/engines.md b/guides/source/engines.md index 0020112a1c..180a786237 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -59,7 +59,7 @@ only be enhancing it, rather than changing it drastically. To see demonstrations of other engines, check out [Devise](https://github.com/plataformatec/devise), an engine that provides authentication for its parent applications, or -[Forem](https://github.com/radar/forem), an engine that provides forum +[Thredded](https://github.com/thredded/thredded), an engine that provides forum functionality. There's also [Spree](https://github.com/spree/spree) which provides an e-commerce platform, and [RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine. diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 8a451ab793..57b8472462 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -86,7 +86,7 @@ your prompt will look something like `c:\source_code>` ### Installing Rails -Open up a command line prompt. On Mac OS X open Terminal.app, on Windows choose +Open up a command line prompt. On macOS open Terminal.app, on Windows choose "Run" from your Start menu and type 'cmd.exe'. Any commands prefaced with a dollar sign `$` should be run in the command line. Verify that you have a current version of Ruby installed: @@ -98,7 +98,7 @@ ruby 2.3.1p112 TIP: A number of tools exist to help you quickly install Ruby and Ruby on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), -while Mac OS X users can use [Tokaido](https://github.com/tokaido/tokaidoapp). +while macOS users can use [Tokaido](https://github.com/tokaido/tokaidoapp). For more installation methods for most Operating Systems take a look at [ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/). @@ -206,7 +206,7 @@ folder directly to the Ruby interpreter e.g. `ruby bin\rails server`. TIP: Compiling CoffeeScript and JavaScript asset compression requires you have a JavaScript runtime available on your system, in the absence of a runtime you will see an `execjs` error during asset compilation. -Usually Mac OS X and Windows come with a JavaScript runtime installed. +Usually macOS and Windows come with a JavaScript runtime installed. Rails adds the `therubyracer` gem to the generated `Gemfile` in a commented line for new apps and you can uncomment if you need it. `therubyrhino` is the recommended runtime for JRuby users and is added by @@ -221,7 +221,7 @@ your application in action, open a browser window and navigate to TIP: To stop the web server, hit Ctrl+C in the terminal window where it's running. To verify the server has stopped you should see your command prompt -cursor again. For most UNIX-like systems including Mac OS X this will be a +cursor again. For most UNIX-like systems including macOS this will be a dollar sign `$`. In development mode, Rails does not generally require you to restart the server; changes you make in files will be automatically picked up by the server. diff --git a/guides/source/i18n.md b/guides/source/i18n.md index fd54bca4ff..ed8cf8a344 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -72,11 +72,13 @@ I18n.l Time.now There are also attribute readers and writers for the following attributes: ```ruby -load_path # Announce your custom translation files -locale # Get and set the current locale -default_locale # Get and set the default locale -exception_handler # Use a different exception_handler -backend # Use a different backend +load_path # Announce your custom translation files +locale # Get and set the current locale +default_locale # Get and set the default locale +available_locales # Whitelist locales available for the application +enforce_available_locales # Enforce locale whitelisting (true or false) +exception_handler # Use a different exception_handler +backend # Use a different backend ``` So, let's internationalize a simple Rails application from the ground up in the next chapters! @@ -124,6 +126,9 @@ The load path must be specified before any translations are looked up. To change # Where the I18n library should search for translation files I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')] +# Whitelist locales available for the application +I18n.available_locales = [:en, :pt] + # Set default locale to something other than :en I18n.default_locale = :pt ``` @@ -404,6 +409,35 @@ NOTE: You need to restart the server when you add new locale files. You may use YAML (`.yml`) or plain Ruby (`.rb`) files for storing your translations in SimpleStore. YAML is the preferred option among Rails developers. However, it has one big disadvantage. YAML is very sensitive to whitespace and special characters, so the application may not load your dictionary properly. Ruby files will crash your application on first request, so you may easily find what's wrong. (If you encounter any "weird issues" with YAML dictionaries, try putting the relevant portion of your dictionary into a Ruby file.) +If your translations are stored in YAML files, certain keys must be escaped. They are: + +* true, on, yes +* false, off, no + +Examples: + +```erb +# config/locales/en.yml +en: + success: + 'true': 'True!' + 'on': 'On!' + 'false': 'False!' + failure: + true: 'True!' + off: 'Off!' + false: 'False!' +``` + +```ruby +I18n.t 'success.true' # => 'True!' +I18n.t 'success.on' # => 'On!' +I18n.t 'success.false' # => 'False!' +I18n.t 'failure.false' # => Translation Missing +I18n.t 'failure.off' # => Translation Missing +I18n.t 'failure.true' # => Translation Missing +``` + ### Passing Variables to Translations One key consideration for successfully internationalizing an application is to @@ -667,7 +701,7 @@ end ### Pluralization -In English there are only one singular and one plural form for a given string, e.g. "1 message" and "2 messages". Other languages ([Arabic](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ar), [Japanese](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ja), [Russian](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ru) and many more) have different grammars that have additional or fewer [plural forms](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html). Thus, the I18n API provides a flexible pluralization feature. +In English there are only one singular and one plural form for a given string, e.g. "1 message" and "2 messages". Other languages ([Arabic](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ar), [Japanese](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ja), [Russian](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#ru) and many more) have different grammars that have additional or fewer [plural forms](http://cldr.unicode.org/index/cldr-spec/plural-rules). Thus, the I18n API provides a flexible pluralization feature. The `:count` interpolation variable has a special role in that it both is interpolated to the translation and used to pick a pluralization from the translations according to the pluralization rules defined by CLDR: diff --git a/guides/source/initialization.md b/guides/source/initialization.md index aa7bbcc19b..3ea156c6fe 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -168,7 +168,7 @@ module Rails::Command class << self def invoke(namespace, args = [], **config) namespace = namespace.to_s - namespace = "help" if namespace.blank? || Thor::HELP_MAPPINGS.include?(namespace) + namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace) namespace = "version" if %w( -v --version ).include? namespace if command = find_by_namespace(namespace) diff --git a/guides/source/kindle/rails_guides.opf.erb b/guides/source/kindle/rails_guides.opf.erb index 547abcbc19..63eeb007d7 100644 --- a/guides/source/kindle/rails_guides.opf.erb +++ b/guides/source/kindle/rails_guides.opf.erb @@ -5,7 +5,7 @@ <meta name="cover" content="cover" /> <dc-metadata xmlns:dc="http://purl.org/dc/elements/1.1/"> - <dc:title>Ruby on Rails Guides (<%= @version %>)</dc:title> + <dc:title>Ruby on Rails Guides (<%= @version || "master@#{@edge[0, 7]}" %>)</dc:title> <dc:language>en-us</dc:language> <dc:creator>Ruby on Rails</dc:creator> diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index c8702f54fc..48bb3147f3 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -411,6 +411,8 @@ render formats: :xml render formats: [:json, :xml] ``` +If a template with the specified format does not exist an `ActionView::MissingTemplate` error is raised. + #### Finding Layouts To find the current layout, Rails first looks for a file in `app/views/layouts` with the same base name as the controller. For example, rendering actions from the `PhotosController` class will use `app/views/layouts/photos.html.erb` (or `app/views/layouts/photos.builder`). If there is no such controller-specific layout, Rails will use `app/views/layouts/application.html.erb` or `app/views/layouts/application.builder`. If there is no `.erb` layout, Rails will use a `.builder` layout if one exists. Rails also provides several ways to more precisely assign specific layouts to individual controllers and actions. @@ -1155,7 +1157,7 @@ To pass a local variable to a partial in only specific cases use the `local_assi <%= render article, full: true %> ``` -* `_articles.html.erb` +* `_article.html.erb` ```erb <h2><%= article.title %></h2> diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md index 50866350f8..de63e193f4 100644 --- a/guides/source/ruby_on_rails_guides_guidelines.md +++ b/guides/source/ruby_on_rails_guides_guidelines.md @@ -50,6 +50,48 @@ Use the same inline formatting as regular text: ##### The `:content_type` Option ``` +Linking to the API +------------------ + +Links to the API (`api.rubyonrails.org`) are processed by the guides generator in the following manner: + +Links that include a release tag are left untouched. For example + +``` +http://api.rubyonrails.org/v5.0.1/classes/ActiveRecord/Attributes/ClassMethods.html +``` + +is not modified. + +Please use these in release notes, since they should point to the corresponding version no matter the target being generated. + +If the link does not include a release tag and edge guides are being generated, the domain is replaced by `edgeapi.rubyonrails.org`. For example, + +``` +http://api.rubyonrails.org/classes/ActionDispatch/Response.html +``` + +becomes + +``` +http://edgeapi.rubyonrails.org/classes/ActionDispatch/Response.html +``` + +If the link does not include a release tag and release guides are being generated, the Rails version is injected. For example, if we are generating the guides for v5.1.0 the link + +``` +http://api.rubyonrails.org/classes/ActionDispatch/Response.html +``` + +becomes + +``` +http://api.rubyonrails.org/v5.1.0/classes/ActionDispatch/Response.html +``` + +Please don't link to `edgeapi.rubyonrails.org` manually. + + API Documentation Guidelines ---------------------------- @@ -97,8 +139,6 @@ By default, guides that have not been modified are not processed, so `ONLY` is r To force processing all the guides, pass `ALL=1`. -It is also recommended that you work with `WARNINGS=1`. This detects duplicate IDs and warns about broken internal links. - If you want to generate guides in a language other than English, you can keep them in a separate directory under `source` (eg. `source/es`) and use the `GUIDES_LANGUAGE` environment variable: ``` diff --git a/guides/source/testing.md b/guides/source/testing.md index 6f783089a9..652030a733 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -8,7 +8,7 @@ This guide covers built-in mechanisms in Rails for testing your application. After reading this guide, you will know: * Rails testing terminology. -* How to write unit, functional, and integration tests for your application. +* How to write unit, functional, integration, and system tests for your application. * Other popular testing approaches and plugins. -------------------------------------------------------------------------------- @@ -33,18 +33,27 @@ Rails creates a `test` directory for you as soon as you create a Rails project u ```bash $ ls -F test -controllers/ helpers/ mailers/ test_helper.rb -fixtures/ integration/ models/ +controllers/ helpers/ mailers/ system/ test_helper.rb +fixtures/ integration/ models/ application_system_test_case.rb ``` The `helpers`, `mailers`, and `models` directories are meant to hold tests for view helpers, mailers, and models, respectively. The `controllers` directory is meant to hold tests for controllers, routes, and views. The `integration` directory is meant to hold tests for interactions between controllers. +The system test directory holds system tests, which are used for full browser +testing of your application. System tests allow you to test your application +the way your users experience it and help you test your JavaScript as well. +System tests inherit from Capybara and perform in browser tests for your +application. + Fixtures are a way of organizing test data; they reside in the `fixtures` directory. A `jobs` directory will also be created when an associated test is first generated. The `test_helper.rb` file holds the default configuration for your tests. +The `application_system_test_case.rb` holds the default configuration for your system +tests. + ### The Test Environment @@ -358,6 +367,7 @@ All the basic assertions such as `assert_equal` defined in `Minitest::Assertions * [`ActionView::TestCase`](http://api.rubyonrails.org/classes/ActionView/TestCase.html) * [`ActionDispatch::IntegrationTest`](http://api.rubyonrails.org/classes/ActionDispatch/IntegrationTest.html) * [`ActiveJob::TestCase`](http://api.rubyonrails.org/classes/ActiveJob/TestCase.html) +* [`ActionDispatch::SystemTestCase`](http://api.rubyonrails.org/classes/ActionDispatch/SystemTestCase.html) Each of these classes include `Minitest::Assertions`, allowing us to use all of the basic assertions in our tests. @@ -587,6 +597,177 @@ create test/fixtures/articles.yml Model tests don't have their own superclass like `ActionMailer::TestCase` instead they inherit from [`ActiveSupport::TestCase`](http://api.rubyonrails.org/classes/ActiveSupport/TestCase.html). +System Testing +-------------- + +System tests are full-browser tests that can be used to test your application's +JavaScript and user experience. System tests use Capybara as a base. + +System tests allow for running tests in either a real browser or a headless +driver for testing full user interactions with your application. + +For creating Rails system tests, you use the `test/system` directory in your +application. Rails provides a generator to create a system test skeleton for you. + +```bash +$ bin/rails generate system_test users_create_test + invoke test_unit + create test/system/users_create_test.rb +``` + +Here's what a freshly-generated system test looks like: + +```ruby +require "application_system_test_case" + +class UsersCreateTest < ApplicationSystemTestCase + visit users_url + + assert_selector "h1", text: "Users" +end +``` + +By default, system tests are run with the Selenium driver, using the Chrome +browser, and a screen size of 1400x1400. The next section explains how to +change the default settings. + +### Changing the default settings + +Rails makes changing the default settings for system tests very simple. All +the setup is abstracted away so you can focus on writing your tests. + +When you generate a new application or scaffold, an `application_system_test_case.rb` file +is created in the test directory. This is where all the configuration for your +system tests should live. + +If you want to change the default settings you can simply change what the system +tests are "driven by". Say you want to change the driver from Selenium to +Poltergeist. First add the Poltergeist gem to your Gemfile. Then in your +`application_system_test_case.rb` file do the following: + +```ruby +require "test_helper" +require "capybara/poltergeist" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :poltergeist +end +``` + +The driver name is a required argument for `driven_by`. The optional arguments +that can be passed to `driven_by` are `:using` for the browser (this will only +be used for non-headless drivers like Selenium), `:on` for the port Puma should +use, and `:screen_size` to change the size of the screen for screenshots. + +```ruby +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :firefox +end +``` + +If your Capybara configuration requires more setup than provided by Rails, all +of that configuration can be put into the `application_system_test_case.rb` file. + +Please see [Capybara's documentation](https://github.com/teamcapybara/capybara#setup) +for additional settings. + +### Screenshot Helper + +The `ScreenshotHelper` is a helper designed to capture screenshots of your tests. +This can be helpful for viewing the browser at the point a test failed, or +to view screenshots later for debugging. + +Two methods are provided: `take_screenshot` and `take_failed_screenshot`. +`take_failed_screenshot` is automatically included in `after_teardown` inside +Rails. + +The `take_screenshot` helper method can be included anywhere in your tests to +take a screenshot of the browser. + +### Implementing a system test + +Now we're going to add a system test to our blog application. We'll demonstrate +writing a system test by visiting the index page and creating a new blog article. + +If you used the scaffold generator, a system test skeleton is automatically +created for you. If you did not use the generator start by creating a system +test skeleton. + +```bash +$ bin/rails generate system_test articles +``` + +It should have created a test file placeholder for us. With the output of the +previous command you should see: + +```bash + invoke test_unit + create test/system/articles_test.rb +``` + +Now let's open that file and write our first assertion: + +```ruby +require "application_system_test_case" + +class ArticlesTest < ApplicationSystemTestCase + test "viewing the index" do + visit articles_path + assert_selector "h1", text: "Articles" + end +end +``` + +The test should see that there is an h1 on the articles index and pass. + +Run the system tests. + +```bash +bin/rails test:system +``` + +#### Creating articles system test + +Now let's test the flow for creating a new article in our blog. + +```ruby +test "creating an article" do + visit articles_path + + click_on "New Article" + + fill_in "Title", with: "Creating an Article" + fill_in "Body", with: "Created this article successfully!" + + click_on "Create Article" + + assert_text "Creating an Article" +end +``` + +The first step is to call `visit articles_path`. This will take the test to the +articles index page. + +Then the `click_on "New Article"` will find the "New Article" button on the +index page. This will redirect the browser to `/articles/new`. + +Then the test will fill in the title and body of the article with the specified +text. Once the fields are filled in, "Create Article" is clicked on which will +send a POST request to create the new article in the database. + +We will be redirected back to the the articles index page and there we assert +that the text from the article title is on the articles index page. + +#### Taking it further + +The beauty of system testing is that it is similar to integration testing in +that it tests the user's interaction with your controller, model, and view, but +system testing is much more robust and actually tests your application as if +a real user were using it. Going forward, you can test anything that the user +themselves would do in your application such as commenting, deleting articles, +publishing draft articles, etc. Integration Testing ------------------- @@ -868,7 +1049,7 @@ class ArticlesControllerTest < ActionDispatch::IntegrationTest assert_equal "index", @controller.action_name assert_equal "application/x-www-form-urlencoded", @request.media_type - assert_match "Articles", @response.body + assert_match "Articles", @response.body end end ``` diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 8a3b3b84b4..8ba00a2b10 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -140,6 +140,8 @@ See [#19034](https://github.com/rails/rails/pull/19034) for more details. ### Rails Controller Testing +#### Extraction of some helper methods to `rails-controller-testing` + `assigns` and `assert_template` have been extracted to the `rails-controller-testing` gem. To continue using these methods in your controller tests, add `gem 'rails-controller-testing'` to your Gemfile. @@ -147,6 +149,14 @@ your Gemfile. If you are using Rspec for testing, please see the extra configuration required in the gem's documentation. +#### New behavior when uploading files + +If you are using `ActionDispatch::Http::UploadedFile` in your tests to +upload files, you will need to change to use the similar `Rack::Test::UploadedFile` +class instead. + +See [#26404](https://github.com/rails/rails/issues/26404) for more details. + ### Autoloading is Disabled After Booting in the Production Environment Autoloading is now disabled after booting in the production environment by @@ -703,7 +713,7 @@ There are a few major changes related to JSON handling in Rails 4.1. MultiJSON has reached its [end-of-life](https://github.com/rails/rails/pull/10576) and has been removed from Rails. -If your application currently depend on MultiJSON directly, you have a few options: +If your application currently depends on MultiJSON directly, you have a few options: 1. Add 'multi_json' to your Gemfile. Note that this might cease to work in the future diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index d7d1a66863..f9294b6616 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,4 +1,58 @@ -* The `log:clear` task clear all environments log files by default. +* Fix running multiple tests in one `rake` command + + e.g. `bin/rake test:models test:controllers` + + *Dominic Cleal* + +* Add option to configure Ruby's warning behaviour to test runner. + + *Yuji Yaginuma* + +* Initialize git repo when generating new app, if option `--skip-git` + is not provided. + + *Dino Maric* + +* Install Byebug gem as default in Windows (mingw and x64_mingw) platform. + + *Junichi Ito* + +* Make every Rails command work within engines. + + *Sean Collins*, *Yuji Yaginuma* + +* Don't generate HTML/ERB templates for scaffold controller with `--api` flag. + + Fixes #27591. + + *Prathamesh Sonpatki* + +* Make `Rails.env` fall back to `development` when `RAILS_ENV` and `RACK_ENV` is an empty string. + + *Daniel Deng* + +* Remove deprecated `CONTROLLER` environment variable for `routes` task. + + *Rafael Mendonça França* + +* Remove deprecated tasks: `rails:update`, `rails:template`, `rails:template:copy`, + `rails:update:configs` and `rails:update:bin`. + + *Rafael Mendonça França* + +* Remove deprecated file `rails/rack/debugger`. + + *Rafael Mendonça França* + +* Remove deprecated `config.serve_static_files`. + + *Rafael Mendonça França* + +* Remove deprecated `config.static_cache_control`. + + *Rafael Mendonça França* + +* The `log:clear` task clear all environments log files by default. *Yuji Yaginuma* @@ -14,7 +68,7 @@ *DHH* -* Add Yarn support in new apps with a yarn binstub and vendor/package.json. Skippable via --skip-yarn option. +* Add Yarn support in new apps with a yarn binstub and package.json. Skippable via --skip-yarn option. *Liceth Ovalles*, *Guillermo Iguaran*, *DHH* diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index ee48043a50..00add5829d 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -7,6 +7,7 @@ require "active_support/dependencies/autoload" require "active_support/core_ext/kernel/reporting" require "active_support/core_ext/module/delegation" require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/object/blank" require "rails/application" require "rails/version" @@ -67,7 +68,7 @@ module Rails # Rails.env.development? # => true # Rails.env.production? # => false def env - @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development") + @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development") end # Sets the Rails environment. diff --git a/railties/lib/rails/api/generator.rb b/railties/lib/rails/api/generator.rb new file mode 100644 index 0000000000..dcc491783c --- /dev/null +++ b/railties/lib/rails/api/generator.rb @@ -0,0 +1,28 @@ +require "sdoc" + +class RDoc::Generator::API < RDoc::Generator::SDoc # :nodoc: + RDoc::RDoc.add_generator self + + def generate_class_tree_level(classes, visited = {}) + # Only process core extensions on the first visit. + if visited.empty? + core_exts, classes = classes.partition { |klass| core_extension?(klass) } + + super.unshift([ "Core extensions", "", "", build_core_ext_subtree(core_exts, visited) ]) + else + super + end + end + + private + def build_core_ext_subtree(classes, visited) + classes.map do |klass| + [ klass.name, klass.document_self_or_methods ? klass.path : "", "", + generate_class_tree_level(klass.classes_and_modules, visited) ] + end + end + + def core_extension?(klass) + klass.name != "ActiveSupport" && klass.in_files.any? { |file| file.absolute_name.include?("core_ext") } + end +end diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb index bc670b1d75..0c0343114f 100644 --- a/railties/lib/rails/api/task.rb +++ b/railties/lib/rails/api/task.rb @@ -1,4 +1,5 @@ require "rdoc/task" +require "rails/api/generator" module Rails module API @@ -8,8 +9,7 @@ module Rails include: %w( README.rdoc lib/active_support/**/*.rb - ), - exclude: "lib/active_support/vendor/*" + ) }, "activerecord" => { @@ -69,7 +69,11 @@ module Rails README.rdoc lib/**/*.rb ), - exclude: "lib/rails/generators/rails/**/templates/**/*.rb" + exclude: %w( + lib/rails/generators/**/templates/**/*.rb + lib/rails/test_unit/* + lib/rails/api/generator.rb + ) } } @@ -80,7 +84,7 @@ module Rails # Be lazy computing stuff to have as light impact as possible to # the rest of tasks. before_running_rdoc do - load_and_configure_sdoc + configure_sdoc configure_rdoc_files setup_horo_variables end @@ -91,20 +95,15 @@ module Rails # no-op end - def load_and_configure_sdoc - require "sdoc" - + def configure_sdoc self.title = "Ruby on Rails API" self.rdoc_dir = api_dir options << "-m" << api_main options << "-e" << "UTF-8" - options << "-f" << "sdoc" + options << "-f" << "api" options << "-T" << "rails" - rescue LoadError - $stderr.puts %(Unable to load SDoc, please add\n\n gem 'sdoc', require: false\n\nto the Gemfile.) - exit 1 end def configure_rdoc_files @@ -147,7 +146,7 @@ module Rails end class RepoTask < Task - def load_and_configure_sdoc + def configure_sdoc super options << "-g" # link to GitHub, SDoc flag end diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index f96432c89f..1a6aed7ce4 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -72,7 +72,7 @@ module Rails # on one of the applications to create a copy of the application which shares # the configuration. # - # If you decide to define rake tasks, runners, or initializers in an + # If you decide to define Rake tasks, runners, or initializers in an # application other than +Rails.application+, then you must run them manually. class Application < Engine autoload :Bootstrap, "rails/application/bootstrap" @@ -265,8 +265,8 @@ module Rails end end - # If you try to define a set of rake tasks on the instance, these will get - # passed up to the rake tasks defined on the application's class. + # If you try to define a set of Rake tasks on the instance, these will get + # passed up to the Rake tasks defined on the application's class. def rake_tasks(&block) self.class.rake_tasks(&block) end @@ -511,7 +511,7 @@ module Rails def validate_secret_key_config! #:nodoc: if secrets.secret_key_base.blank? - ActiveSupport::Deprecation.warn "You didn't set `secret_key_base`. " + + ActiveSupport::Deprecation.warn "You didn't set `secret_key_base`. " \ "Read the upgrade documentation to learn more about this new config option." if secrets.secret_token.blank? diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 11da271501..6102af3fff 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -48,8 +48,8 @@ INFO logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDERR)) logger.level = ActiveSupport::Logger::WARN logger.warn( - "Rails Error: Unable to access log file. Please ensure that #{path} exists and is writable " + - "(ie, make it writable for user and group: chmod 0664 #{path}). " + + "Rails Error: Unable to access log file. Please ensure that #{path} exists and is writable " \ + "(ie, make it writable for user and group: chmod 0664 #{path}). " \ "The log level has been raised to WARN and the output directed to STDERR until the problem is fixed." ) logger diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 810750ed35..b0d33f87a3 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -3,9 +3,6 @@ require "active_support/file_update_checker" require "rails/engine/configuration" require "rails/source_annotation_extractor" -require "active_support/deprecation" -require "active_support/core_ext/string/strip" # for strip_heredoc - module Rails class Application class Configuration < ::Rails::Engine::Configuration @@ -19,11 +16,11 @@ module Rails :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading attr_writer :log_level - attr_reader :encoding, :api_only, :static_cache_control + attr_reader :encoding, :api_only def initialize(*) super - self.encoding = "utf-8" + self.encoding = Encoding::UTF_8 @allow_concurrency = nil @consider_all_requests_local = false @filter_parameters = [] @@ -56,35 +53,6 @@ module Rails @enable_dependency_loading = false end - def static_cache_control=(value) - ActiveSupport::Deprecation.warn <<-eow.strip_heredoc - `config.static_cache_control` is deprecated and will be removed in Rails 5.1. - Please use - `config.public_file_server.headers = { 'Cache-Control' => '#{value}' }` - instead. - eow - - @static_cache_control = value - end - - def serve_static_files - ActiveSupport::Deprecation.warn <<-eow.strip_heredoc - `config.serve_static_files` is deprecated and will be removed in Rails 5.1. - Please use `config.public_file_server.enabled` instead. - eow - - @public_file_server.enabled - end - - def serve_static_files=(value) - ActiveSupport::Deprecation.warn <<-eow.strip_heredoc - `config.serve_static_files` is deprecated and will be removed in Rails 5.1. - Please use `config.public_file_server.enabled = #{value}` instead. - eow - - @public_file_server.enabled = value - end - def encoding=(value) @encoding = value silence_warnings do @@ -161,7 +129,7 @@ module Rails def colorize_logging=(val) ActiveSupport::LogSubscriber.colorize_logging = val - self.generators.colorize_logging = val + generators.colorize_logging = val end def session_store(new_session_store = nil, **options) diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index d070aca2dd..8fe48feefb 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -19,7 +19,6 @@ module Rails if config.public_file_server.enabled headers = config.public_file_server.headers || {} - headers["Cache-Control".freeze] = config.static_cache_control if config.static_cache_control middleware.use ::ActionDispatch::Static, paths["public"].first, index: config.public_file_server.index_name, headers: headers end diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index a855e8fab0..c027d06663 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -124,6 +124,7 @@ module Rails # the hook are taken into account. initializer :set_routes_reloader_hook do |app| reloader = routes_reloader + reloader.eager_load = app.config.eager_load reloader.execute_if_updated reloaders << reloader app.reloader.to_run do diff --git a/railties/lib/rails/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb index cf0a4e128f..e02ef629f2 100644 --- a/railties/lib/rails/application/routes_reloader.rb +++ b/railties/lib/rails/application/routes_reloader.rb @@ -4,11 +4,13 @@ module Rails class Application class RoutesReloader attr_reader :route_sets, :paths - delegate :execute_if_updated, :execute, :updated?, to: :updater + attr_accessor :eager_load + delegate :updated?, to: :updater def initialize @paths = [] @route_sets = [] + @eager_load = false end def reload! @@ -19,6 +21,19 @@ module Rails revert end + def execute + ret = updater.execute + route_sets.each(&:eager_load!) if eager_load + ret + end + + def execute_if_updated + if updated = updater.execute_if_updated + route_sets.each(&:eager_load!) if eager_load + end + updated + end + private def updater diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb index ddb953543f..13f3b90b6d 100644 --- a/railties/lib/rails/command.rb +++ b/railties/lib/rails/command.rb @@ -15,6 +15,8 @@ module Rails include Behavior + HELP_MAPPINGS = %w(-h -? --help) + class << self def hidden_commands # :nodoc: @hidden_commands ||= [] @@ -27,7 +29,7 @@ module Rails # Receives a namespace, arguments and the behavior to invoke the command. def invoke(namespace, args = [], **config) namespace = namespace.to_s - namespace = "help" if namespace.blank? || Thor::HELP_MAPPINGS.include?(namespace) + namespace = "help" if namespace.blank? || HELP_MAPPINGS.include?(namespace) namespace = "version" if %w( -v --version ).include? namespace if command = find_by_namespace(namespace) @@ -37,7 +39,7 @@ module Rails end end - # Rails finds namespaces similar to thor, it only adds one rule: + # Rails finds namespaces similar to Thor, it only adds one rule: # # Command names must end with "_command.rb". This is required because Rails # looks in load paths and loads the command just before it's going to be used. diff --git a/railties/lib/rails/command/actions.rb b/railties/lib/rails/command/actions.rb index 31b656ec31..8fda1c87c6 100644 --- a/railties/lib/rails/command/actions.rb +++ b/railties/lib/rails/command/actions.rb @@ -8,11 +8,16 @@ module Rails Dir.chdir(File.expand_path("../../", APP_PATH)) unless File.exist?(File.expand_path("config.ru")) end - if defined?(ENGINE_PATH) - def require_application_and_environment! - require ENGINE_PATH + def require_application_and_environment! + require ENGINE_PATH if defined?(ENGINE_PATH) + + if defined?(APP_PATH) + require APP_PATH + Rails.application.require_environment! end + end + if defined?(ENGINE_PATH) def load_tasks Rake.application.init("rails") Rake.application.load_rakefile @@ -24,11 +29,6 @@ module Rails engine.load_generators end else - def require_application_and_environment! - require APP_PATH - Rails.application.require_environment! - end - def load_tasks Rails.application.load_tasks end diff --git a/railties/lib/rails/command/base.rb b/railties/lib/rails/command/base.rb index 7ae190433a..1435792536 100644 --- a/railties/lib/rails/command/base.rb +++ b/railties/lib/rails/command/base.rb @@ -56,7 +56,7 @@ module Rails end def perform(command, args, config) # :nodoc: - command = nil if Thor::HELP_MAPPINGS.include?(args.first) + command = nil if Rails::Command::HELP_MAPPINGS.include?(args.first) dispatch(command, args.dup, nil, config) end diff --git a/railties/lib/rails/commands/help/USAGE b/railties/lib/rails/commands/help/USAGE index 348f41861f..c5f8ab72bb 100644 --- a/railties/lib/rails/commands/help/USAGE +++ b/railties/lib/rails/commands/help/USAGE @@ -1,16 +1,3 @@ -Usage: bin/rails COMMAND [args] [options] -<% if engine? %> -The common Rails commands available for engines are: - generate Generate new code (short-cut alias: "g") - destroy Undo code generated with "generate" (short-cut alias: "d") - test Run tests (short-cut alias: "t") - -All commands can be run with -h for more information. - -If you want to run any commands that need to be run in context -of the application, like `bin/rails server` or `bin/rails console`, -you should do it from the application's directory (typically test/dummy). -<% else %> The most common rails commands are: generate Generate new code (short-cut alias: "g") console Start the Rails console (short-cut alias: "c") @@ -18,10 +5,11 @@ The most common rails commands are: test Run tests (short-cut alias: "t") dbconsole Start a console for the database specified in config/database.yml (short-cut alias: "db") +<% unless engine? %> new Create a new Rails application. "rails new my_app" creates a new application called MyApp in "./my_app" +<% end %> All commands can be run with -h (or --help) for more information. -<% end %> In addition to those commands, there are: diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index 15c636103b..d58721f648 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -95,7 +95,7 @@ module Rails module Command class ServerCommand < Base # :nodoc: - DEFAULT_PID_PATH = File.expand_path("tmp/pids/server.pid").freeze + DEFAULT_PID_PATH = "tmp/pids/server.pid".freeze class_option :port, aliases: "-p", type: :numeric, desc: "Runs Rails on the specified port.", banner: :port, default: 3000 @@ -141,7 +141,7 @@ module Rails config: options[:config], environment: environment, daemonize: options[:daemon], - pid: options[:pid], + pid: pid, caching: options["dev-caching"], restart_cmd: restart_command } @@ -165,6 +165,10 @@ module Rails "bin/rails server #{@server} #{@original_options.join(" ")}" end + def pid + File.expand_path(options[:pid]) + end + def self.banner(*) "rails server [puma, thin etc] [options]" end diff --git a/railties/lib/rails/commands/test/test_command.rb b/railties/lib/rails/commands/test/test_command.rb index 7bf8f61137..629fb5b425 100644 --- a/railties/lib/rails/commands/test/test_command.rb +++ b/railties/lib/rails/commands/test/test_command.rb @@ -11,7 +11,7 @@ module Rails def perform(*) $LOAD_PATH << Rails::Command.root.join("test") - Minitest.run_via[:rails] = true + Minitest.run_via = :rails require "active_support/testing/autorun" end diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 2dd1fb3273..dc0b158bd4 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -109,7 +109,7 @@ module Rails # # == Endpoint # - # An engine can also be a rack application. It can be useful if you have a rack application that + # An engine can also be a Rack application. It can be useful if you have a Rack application that # you would like to wrap with +Engine+ and provide with some of the +Engine+'s features. # # To do that, use the +endpoint+ method: @@ -128,7 +128,7 @@ module Rails # # == Middleware stack # - # As an engine can now be a rack endpoint, it can also have a middleware + # As an engine can now be a Rack endpoint, it can also have a middleware # stack. The usage is exactly the same as in <tt>Application</tt>: # # module MyEngine @@ -380,7 +380,7 @@ module Rails def isolate_namespace(mod) engine_name(generate_railtie_name(mod.name)) - self.routes.default_scope = { module: ActiveSupport::Inflector.underscore(mod.name) } + routes.default_scope = { module: ActiveSupport::Inflector.underscore(mod.name) } self.isolated = true unless mod.respond_to?(:railtie_namespace) @@ -499,7 +499,7 @@ module Rails paths["app/helpers"].existent end - # Returns the underlying rack application for this engine. + # Returns the underlying Rack application for this engine. def app @app || @app_build_lock.synchronize { @app ||= begin @@ -549,7 +549,7 @@ module Rails load(seed_file) if seed_file end - # Add configured load paths to ruby load paths and remove duplicates. + # Add configured load paths to Ruby's load path, and remove duplicate entries. initializer :set_load_path, before: :bootstrap_hook do _all_load_paths.reverse_each do |path| $LOAD_PATH.unshift(path) if File.directory?(path) @@ -573,7 +573,7 @@ module Rails end initializer :add_routing_paths do |app| - routing_paths = self.paths["config/routes.rb"].existent + routing_paths = paths["config/routes.rb"].existent if routes? || routing_paths.any? app.routes_reloader.paths.unshift(*routing_paths) @@ -661,7 +661,6 @@ module Rails end def self.find_root_with_flag(flag, root_path, default = nil) #:nodoc: - while root_path && File.directory?(root_path) && !File.exist?("#{root_path}/#{flag}") parent = File.dirname(root_path) root_path = parent != root_path && parent diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb index a23ae44b0b..b9ef63243a 100644 --- a/railties/lib/rails/engine/commands.rb +++ b/railties/lib/rails/engine/commands.rb @@ -1,12 +1,7 @@ -require "rails/command" +unless defined?(APP_PATH) + if File.exist?(File.expand_path("test/dummy/config/application.rb", ENGINE_ROOT)) + APP_PATH = File.expand_path("test/dummy/config/application", ENGINE_ROOT) + end +end -aliases = { - "g" => "generate", - "d" => "destroy", - "t" => "test" -} - -command = ARGV.shift -command = aliases[command] || command - -Rails::Command.invoke command, ARGV +require "rails/commands" diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index e1980a42ad..85f66cc416 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -62,6 +62,7 @@ module Rails stylesheets: true, stylesheet_engine: :css, scaffold_stylesheet: true, + system_tests: nil, test_framework: false, template_engine: :erb } @@ -151,6 +152,7 @@ module Rails "#{test}:controller", "#{test}:helper", "#{test}:integration", + "#{test}:system", "#{test}:mailer", "#{test}:model", "#{test}:scaffold", @@ -218,7 +220,7 @@ module Rails [[ "rails", rails ]] + groups.sort.to_a end - # Rails finds namespaces similar to thor, it only adds one rule: + # Rails finds namespaces similar to Thor, it only adds one rule: # # Generators names must end with "_generator.rb". This is required because Rails # looks in load paths and loads the generator just before it's going to be used. diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index d058d82cea..0bd0615b7e 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -269,7 +269,7 @@ module Rails if args.size == 1 say args.first.to_s unless options.quiet? else - args << (self.behavior == :invoke ? :green : :red) + args << (behavior == :invoke ? :green : :red) say_status(*args) end end diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb index f677e545e5..d06609e91e 100644 --- a/railties/lib/rails/generators/actions/create_migration.rb +++ b/railties/lib/rails/generators/actions/create_migration.rb @@ -54,8 +54,8 @@ module Rails say_status :skip, :yellow else say_status :conflict, :red - raise Error, "Another migration is already named #{migration_file_name}: " + - "#{existing_migration}. Use --force to replace this migration " + + raise Error, "Another migration is already named #{migration_file_name}: " \ + "#{existing_migration}. Use --force to replace this migration " \ "or --skip to ignore conflicted file." end end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index ef698bd4ac..04f6341471 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -82,6 +82,9 @@ module Rails class_option :skip_test, type: :boolean, aliases: "-T", default: false, desc: "Skip test files" + class_option :skip_system_test, type: :boolean, default: false, + desc: "Skip system test files" + class_option :dev, type: :boolean, default: false, desc: "Setup the #{name} with Gemfile pointing to your Rails checkout" @@ -190,7 +193,7 @@ module Rails def webserver_gemfile_entry # :doc: return [] if options[:skip_puma] comment = "Use Puma as the app server" - GemfileEntry.new("puma", "~> 3.0", comment) + GemfileEntry.new("puma", "~> 3.7", comment) end def include_all_railties? # :doc: @@ -243,7 +246,6 @@ module Rails def rails_gemfile_entry dev_edge_common = [ - GemfileEntry.github("arel", "rails/arel") ] if options.dev? [ diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index cc6ae3860f..a650c52626 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -256,8 +256,8 @@ module Rails last = extract_last_module(nesting) if last && last.const_defined?(last_name.camelize, false) - raise Error, "The name '#{class_name}' is either already used in your application " << - "or reserved by Ruby on Rails. Please choose an alternative and run " << + raise Error, "The name '#{class_name}' is either already used in your application " \ + "or reserved by Ruby on Rails. Please choose an alternative and run " \ "this generator again." end end @@ -331,7 +331,7 @@ module Rails def self.prepare_for_invocation(name, value) #:nodoc: return super unless value.is_a?(String) || value.is_a?(Symbol) - if value && constants = self.hooks[name] + if value && constants = hooks[name] value = name if TrueClass === value Rails::Generators.find_by_namespace(value, *constants) elsif klass = Rails::Generators.find_by_namespace(value) diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb index 677f8041ae..3f1d9932f6 100644 --- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb @@ -9,10 +9,10 @@ module Erb # :nodoc: view_base_path = File.join("app/views", class_path, file_name + "_mailer") empty_directory view_base_path - if self.behavior == :invoke + if behavior == :invoke formats.each do |format| layout_path = File.join("app/views/layouts", class_path, filename_with_extensions("mailer", format)) - template filename_with_extensions(:layout, format), layout_path + template filename_with_extensions(:layout, format), layout_path unless File.exist?(layout_path) end end diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb index 0d63b9a5c9..82481169c3 100644 --- a/railties/lib/rails/generators/migration.rb +++ b/railties/lib/rails/generators/migration.rb @@ -35,7 +35,7 @@ module Rails end def set_migration_assigns!(destination) - destination = File.expand_path(destination, self.destination_root) + destination = File.expand_path(destination, destination_root) migration_dir = File.dirname(destination) @migration_number = self.class.next_migration_number(migration_dir) diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb index e3660b012a..02557b098a 100644 --- a/railties/lib/rails/generators/named_base.rb +++ b/railties/lib/rails/generators/named_base.rb @@ -82,6 +82,10 @@ module Rails !options[:skip_namespace] && namespace end + def namespace_dirs + @namespace_dirs ||= namespace.name.split("::").map(&:underscore) + end + def file_path # :doc: @file_path ||= (class_path + [file_name]).join("/") end @@ -95,11 +99,11 @@ module Rails end def namespaced_class_path # :doc: - @namespaced_class_path ||= [namespaced_path] + @class_path + @namespaced_class_path ||= namespace_dirs + @class_path end def namespaced_path # :doc: - @namespaced_path ||= namespace.name.split("::").first.underscore + @namespaced_path ||= namespace_dirs.join("/") end def class_name # :doc: @@ -219,7 +223,7 @@ module Rails # def self.check_class_collision(options = {}) # :doc: define_method :check_class_collision do - name = if self.respond_to?(:controller_class_name) # for ScaffoldBase + name = if respond_to?(:controller_class_name) # for ScaffoldBase controller_class_name else class_name diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 8efdfdcb44..86326d98ed 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -53,6 +53,12 @@ module Rails template "gitignore", ".gitignore" end + def version_control + if !options[:skip_git] && !options[:pretend] + run "git init" + end + end + def app directory "app" @@ -144,6 +150,12 @@ module Rails template "test/test_helper.rb" end + def system_test + empty_directory_with_keep_file "test/system" + + template "test/application_system_test_case.rb" + end + def tmp empty_directory_with_keep_file "tmp" empty_directory "tmp/cache" @@ -154,7 +166,7 @@ module Rails empty_directory_with_keep_file "vendor" unless options[:skip_yarn] - template "package.json", "vendor/package.json" + template "package.json" end end end @@ -181,10 +193,6 @@ module Rails def initialize(*args) super - unless app_path - raise Error, "Application name should be provided in arguments. For details run: rails --help" - end - if !options[:skip_active_record] && !DATABASES.include?(options[:database]) raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}." end @@ -205,6 +213,7 @@ module Rails build(:configru) build(:gitignore) unless options[:skip_git] build(:gemfile) unless options[:skip_gemfile] + build(:version_control) end def create_app_files @@ -259,6 +268,10 @@ module Rails build(:test) unless options[:skip_test] end + def create_system_test_files + build(:system_test) unless options[:skip_system_test] || options[:skip_test] || options[:api] + end + def create_tmp_files build(:tmp) end @@ -267,7 +280,7 @@ module Rails build(:vendor) if options[:skip_yarn] - remove_file "vendor/package.json" + remove_file "package.json" end end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index f1015b16d5..b082d70cba 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -31,7 +31,12 @@ end <% if RUBY_ENGINE == 'ruby' -%> group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console - gem 'byebug', platform: :mri + gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] + <%- unless options.skip_system_test? || options.api? -%> + # Adds support for Capybara system testing and selenium driver + gem 'capybara', '~> 2.7.0' + gem 'selenium-webdriver' + <%- end -%> end group :development do diff --git a/railties/lib/rails/generators/rails/app/templates/bin/yarn b/railties/lib/rails/generators/rails/app/templates/bin/yarn index 872438cecb..4ae896a8d3 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/yarn +++ b/railties/lib/rails/generators/rails/app/templates/bin/yarn @@ -1,4 +1,4 @@ -VENDOR_PATH = File.expand_path('../vendor', __dir__) +VENDOR_PATH = File.expand_path('..', __dir__) Dir.chdir(VENDOR_PATH) do begin exec "yarnpkg #{ARGV.join(" ")}" diff --git a/railties/lib/rails/generators/rails/app/templates/config/cable.yml b/railties/lib/rails/generators/rails/app/templates/config/cable.yml index 0bbde6f74f..1da4913082 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/cable.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/cable.yml @@ -7,3 +7,4 @@ test: production: adapter: redis url: redis://localhost:6379/1 + channel_prefix: <%= app_name %>_production diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml index c223d6bc62..a21555e573 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml @@ -1,4 +1,4 @@ -# SQL Server (2005 or higher recommended) +# SQL Server (2012 or higher recommended) # # Install the adapters and driver # gem install tiny_tds @@ -8,29 +8,12 @@ # gem 'tiny_tds' # gem 'activerecord-sqlserver-adapter' # -# You should make sure freetds is configured correctly first. -# freetds.conf contains host/port/protocol_versions settings. -# http://freetds.schemamania.org/userguide/freetdsconf.htm -# -# A typical Microsoft server -# [mssql] -# host = mssqlserver.yourdomain.com -# port = 1433 -# tds version = 7.1 - -# If you can connect with "tsql -S servername", your basic FreeTDS installation is working. -# 'man tsql' for more info -# Set timeout to a larger number if valid queries against a live db fail -# default: &default adapter: sqlserver encoding: utf8 - pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> - reconnect: false - username: <%= app_name %> - password: - timeout: 25 - dataserver: from_freetds.conf + username: sa + password: <%= ENV['SA_PASSWORD'] %> + host: localhost development: <<: *default diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 7deab5dbb1..4a39e43e57 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -88,7 +88,7 @@ Rails.application.configure do if ENV["RAILS_LOG_TO_STDOUT"].present? logger = ActiveSupport::Logger.new(STDOUT) logger.formatter = config.log_formatter - config.logger = ActiveSupport::TaggedLogging.new(logger) + config.logger = ActiveSupport::TaggedLogging.new(logger) end <%- unless options.skip_active_record? -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt index f5d03fb117..51196ae743 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt @@ -7,7 +7,7 @@ Rails.application.config.assets.version = '1.0' # Rails.application.config.assets.paths << Emoji.images_path <%- unless options[:skip_yarn] -%> # Add Yarn node_modules folder to the asset load path. -Rails.application.config.assets.paths << Rails.root.join('vendor/node_modules') +Rails.application.config.assets.paths << Rails.root.join('node_modules') <%- end -%> # Precompile additional assets. diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults.rb.tt index 3ad3eba98a..bd844f0503 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults.rb.tt @@ -24,9 +24,6 @@ ActiveSupport.to_time_preserves_timezone = <%= options[:update] ? false : true % # Require `belongs_to` associations by default. Previous versions had false. Rails.application.config.active_record.belongs_to_required_by_default = <%= options[:update] ? false : true %> <%- end -%> - -# Do not halt callback chains when a callback returns false. Previous versions had true. -ActiveSupport.halt_callback_chains_on_return_false = <%= options[:update] ? true : false %> <%- unless options[:update] -%> # Configure SSL options to enable HSTS with subdomains. Previous versions had false. diff --git a/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml b/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml index 0653957166..decc5a8573 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/locales/en.yml @@ -16,6 +16,16 @@ # # This would use the information in config/locales/es.yml. # +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# # To learn more, please read the Rails Internationalization guide # available at http://guides.rubyonrails.org/i18n.html. diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore index 709b341387..7221c26729 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore +++ b/railties/lib/rails/generators/rails/app/templates/gitignore @@ -22,7 +22,8 @@ <% end -%> <% unless options[:skip_yarn] -%> -/vendor/node_modules +/node_modules +/yarn-error.log <% end -%> .byebug_history diff --git a/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb new file mode 100644 index 0000000000..d19212abd5 --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 4cf4f8cd9a..be211e016d 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -91,6 +91,7 @@ task default: :test opts[:skip_bundle] = true opts[:api] = options.api? opts[:skip_listen] = true + opts[:skip_git] = true invoke Rails::Generators::AppGenerator, [ File.expand_path(dummy_path, destination_root) ], opts @@ -112,7 +113,6 @@ task default: :test def test_dummy_clean inside dummy_path do - remove_file ".gitignore" remove_file "db/seeds.rb" remove_file "doc" remove_file "Gemfile" @@ -186,7 +186,7 @@ task default: :test desc: "Skip gemspec file" class_option :skip_gemfile_entry, type: :boolean, default: false, - desc: "If creating plugin in application's directory " + + desc: "If creating plugin in application's directory " \ "skip adding entry to Gemfile" class_option :api, type: :boolean, default: false, @@ -195,10 +195,6 @@ task default: :test def initialize(*args) @dummy_path = nil super - - unless plugin_path - raise Error, "Plugin name should be provided in arguments. For details run: rails plugin new --help" - end end public_task :set_default_accessors! @@ -415,7 +411,6 @@ task default: :test require 'rake/testtask' Rake::TestTask.new(:test) do |t| - t.libs << 'lib' t.libs << 'test' t.pattern = 'test/**/*_test.rb' t.verbose = false diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt index 56e7925c6b..c03d9953d4 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt @@ -3,6 +3,7 @@ ENGINE_ROOT = File.expand_path('../..', __FILE__) ENGINE_PATH = File.expand_path('../../lib/<%= namespaced_name -%>/engine', __FILE__) +APP_PATH = File.expand_path('../../<%= dummy_path -%>/config/application', __FILE__) # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt index c0fbb84a93..35a9bf8c8b 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/test.tt @@ -5,6 +5,6 @@ require 'rails/test_unit/minitest_plugin' Rails::TestUnitReporter.executable = 'bin/test' -Minitest.run_via[:rails] = true +Minitest.run_via = :rails require "active_support/testing/autorun" diff --git a/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb new file mode 100644 index 0000000000..d19212abd5 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin/templates/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb index ed6bf7f7d7..12d6bc85b2 100644 --- a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb @@ -6,6 +6,7 @@ module Rails remove_hook_for :resource_controller remove_class_option :actions + class_option :api, type: :boolean class_option :stylesheets, type: :boolean, desc: "Generate Stylesheets" class_option :stylesheet_engine, desc: "Engine for Stylesheets" class_option :assets, type: :boolean @@ -15,10 +16,13 @@ module Rails def handle_skip @options = @options.merge(stylesheets: false) unless options[:assets] @options = @options.merge(stylesheet_engine: false) unless options[:stylesheets] && options[:scaffold_stylesheet] + @options = @options.merge(system_tests: false) if options[:api] end hook_for :scaffold_controller, required: true + hook_for :system_tests, as: :system + hook_for :assets do |assets| invoke assets, [controller_name] end diff --git a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb index e4f3161ffd..cf97c22160 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb @@ -20,7 +20,11 @@ module Rails template template_file, File.join("app/controllers", controller_class_path, "#{controller_file_name}_controller.rb") end - hook_for :template_engine, :test_framework, as: :scaffold + hook_for :template_engine, as: :scaffold do |template_engine| + invoke template_engine unless options.api? + end + + hook_for :test_framework, as: :scaffold # Invoke the helper using the controller name (pluralized) hook_for :helper, as: :scaffold do |invoked| diff --git a/railties/lib/rails/generators/rails/system_test/USAGE b/railties/lib/rails/generators/rails/system_test/USAGE new file mode 100644 index 0000000000..f11a99e008 --- /dev/null +++ b/railties/lib/rails/generators/rails/system_test/USAGE @@ -0,0 +1,10 @@ +Description: + Stubs out a new system test. Pass the name of the test, either + CamelCased or under_scored, as an argument. + + This generator invokes the current system tool, which defaults to + TestUnit. + +Example: + `rails generate system_test GeneralStories` creates a GeneralStories + system test in test/system/general_stories_test.rb diff --git a/railties/lib/rails/generators/rails/system_test/system_test_generator.rb b/railties/lib/rails/generators/rails/system_test/system_test_generator.rb new file mode 100644 index 0000000000..901120e892 --- /dev/null +++ b/railties/lib/rails/generators/rails/system_test/system_test_generator.rb @@ -0,0 +1,7 @@ +module Rails + module Generators + class SystemTestGenerator < NamedBase # :nodoc: + hook_for :system_tests, as: :system + end + end +end diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb index 978b053308..e7cb722473 100644 --- a/railties/lib/rails/generators/resource_helpers.rb +++ b/railties/lib/rails/generators/resource_helpers.rb @@ -59,7 +59,7 @@ module Rails end # Loads the ORM::Generators::ActiveModel class. This class is responsible - # to tell scaffold entities how to generate an specific method for the + # to tell scaffold entities how to generate a specific method for the # ORM. Check Rails::Generators::ActiveModel for more information. def orm_class @orm_class ||= begin diff --git a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb index dea7e22196..118e0f1271 100644 --- a/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb +++ b/railties/lib/rails/generators/test_unit/integration/templates/integration_test.rb @@ -1,7 +1,9 @@ require 'test_helper' +<% module_namespacing do -%> class <%= class_name %>Test < ActionDispatch::IntegrationTest # test "the truth" do # assert true # end end +<% end -%> diff --git a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb index 8840a86d0d..292db35121 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb @@ -22,7 +22,7 @@ module TestUnit # :nodoc: def fixture_name @fixture_name ||= if mountable_engine? - "%s_%s" % [namespaced_path, table_name] + (namespace_dirs + [table_name]).join("_") else table_name end diff --git a/railties/lib/rails/generators/test_unit/system/system_generator.rb b/railties/lib/rails/generators/test_unit/system/system_generator.rb new file mode 100644 index 0000000000..aec415a4e5 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/system/system_generator.rb @@ -0,0 +1,17 @@ +require "rails/generators/test_unit" + +module TestUnit # :nodoc: + module Generators # :nodoc: + class SystemGenerator < Base # :nodoc: + check_class_collision suffix: "Test" + + def create_test_files + if !File.exist?(File.join("test/application_system_test_case.rb")) + template "application_system_test_case.rb", File.join("test", "application_system_test_case.rb") + end + + template "system_test.rb", File.join("test/system", "#{file_name.pluralize}_test.rb") + end + end + end +end diff --git a/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb new file mode 100644 index 0000000000..d19212abd5 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/system/templates/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/railties/lib/rails/generators/test_unit/system/templates/system_test.rb b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb new file mode 100644 index 0000000000..b5ce2ba5c8 --- /dev/null +++ b/railties/lib/rails/generators/test_unit/system/templates/system_test.rb @@ -0,0 +1,9 @@ +require "application_system_test_case" + +class <%= class_name.pluralize %>Test < ApplicationSystemTestCase + # test "visiting the index" do + # visit <%= plural_table_name %>_url + # + # assert_selector "h1", text: "<%= class_name %>" + # end +end diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb index 5d4acd6f6b..fc064dac32 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -1,9 +1,9 @@ require "cgi" module Rails - # This module helps build the runtime properties used to display in the - # Rails::InfoController responses. Including the active Rails version, Ruby - # version, Rack version, and so on. + # This module helps build the runtime properties that are displayed in + # Rails::InfoController responses. These include the active Rails version, + # Ruby version, Rack version, and so on. module Info mattr_accessor :properties class << (@@properties = []) diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index 10925de8b2..af3be10a31 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -2,12 +2,12 @@ module Rails module Paths # This object is an extended hash that behaves as root of the <tt>Rails::Paths</tt> system. # It allows you to collect information about how you want to structure your application - # paths by a Hash like API. It requires you to give a physical path on initialization. + # paths through a Hash-like API. It requires you to give a physical path on initialization. # # root = Root.new "/rails" # root.add "app/controllers", eager_load: true # - # The command above creates a new root object and adds "app/controllers" as a path. + # The above command creates a new root object and adds "app/controllers" as a path. # This means we can get a <tt>Rails::Paths::Path</tt> object back like below: # # path = root["app/controllers"] diff --git a/railties/lib/rails/rack/debugger.rb b/railties/lib/rails/rack/debugger.rb deleted file mode 100644 index dccfa3e9bb..0000000000 --- a/railties/lib/rails/rack/debugger.rb +++ /dev/null @@ -1,3 +0,0 @@ -require "active_support/deprecation" - -ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.") diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index 3a48c4c496..e9088c44ce 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -13,7 +13,7 @@ # start with the tag optionally followed by a colon. Everything up to the end # of the line (or closing ERB comment tag) is considered to be their text. class SourceAnnotationExtractor - class Annotation < Struct.new(:line, :tag, :text) + Annotation = Struct.new(:line, :tag, :text) do def self.directories @@directories ||= %w(app config db lib test) + (ENV["SOURCE_ANNOTATION_DIRECTORIES"] || "").split(",") end diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb index 48675b3845..c0a50e5bda 100644 --- a/railties/lib/rails/tasks.rb +++ b/railties/lib/rails/tasks.rb @@ -12,6 +12,7 @@ require "rake" restart routes tmp + yarn ).tap { |arr| arr << "statistics" if Rake.application.current_scope.empty? }.each do |task| diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index a2167796eb..f5586b53f0 100644 --- a/railties/lib/rails/tasks/framework.rake +++ b/railties/lib/rails/tasks/framework.rake @@ -1,5 +1,3 @@ -require "active_support/deprecation" - namespace :app do desc "Update configs and some other initially generated files (or use just update:configs or update:bin)" task update: [ "update:configs", "update:bin", "update:upgrade_guide_info" ] @@ -73,15 +71,3 @@ namespace :app do end end end - -namespace :rails do - %i(update template templates:copy update:configs update:bin).each do |task_name| - task "#{task_name}" do - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Running #{task_name} with the rails: namespace is deprecated in favor of app: namespace. - Run bin/rails app:#{task_name} instead. - MSG - Rake.application.invoke_task("app:#{task_name}") - end - end -end diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake index f5e5b9ae87..215fb2ceb5 100644 --- a/railties/lib/rails/tasks/routes.rake +++ b/railties/lib/rails/tasks/routes.rake @@ -1,5 +1,3 @@ -require "active_support/deprecation" -require "active_support/core_ext/string/strip" # for strip_heredoc require "optparse" desc "Print out all defined routes in match order, with names. Target specific controller with -c option, or grep routes using -g option" @@ -7,15 +5,8 @@ task routes: :environment do all_routes = Rails.application.routes.routes require "action_dispatch/routing/inspector" inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes) - if ARGV.any? { |argv| argv.start_with? "CONTROLLER" } - puts <<-eow.strip_heredoc - Passing `CONTROLLER` to `bin/rails routes` is deprecated and will be removed in Rails 5.1. - Please use `bin/rails routes -c controller_name` instead. - eow - end routes_filter = nil - routes_filter = { controller: ENV["CONTROLLER"] } if ENV["CONTROLLER"] OptionParser.new do |opts| opts.banner = "Usage: rails routes [options]" diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake index ba1697186e..cb569be58b 100644 --- a/railties/lib/rails/tasks/statistics.rake +++ b/railties/lib/rails/tasks/statistics.rake @@ -17,6 +17,7 @@ STATS_DIRECTORIES = [ %w(Mailer\ tests test/mailers), %w(Job\ tests test/jobs), %w(Integration\ tests test/integration), + %w(System\ tests test/system), ].collect do |name, dir| [ name, "#{File.dirname(Rake.application.rakefile_location)}/#{dir}" ] end.select { |name, dir| File.directory?(dir) } diff --git a/railties/lib/rails/tasks/yarn.rake b/railties/lib/rails/tasks/yarn.rake new file mode 100644 index 0000000000..87476b1b8c --- /dev/null +++ b/railties/lib/rails/tasks/yarn.rake @@ -0,0 +1,11 @@ +namespace :yarn do + desc "Install all JavaScript dependencies as specified via Yarn" + task :install do + system("./bin/yarn install --no-progress") + end +end + +# Run Yarn prior to Sprockets assets precompilation, so dependencies are available for use. +if Rake::Task.task_defined?("assets:precompile") + Rake::Task["assets:precompile"].enhance [ "yarn:install" ] +end diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index db341dd847..09931c108a 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -11,13 +11,19 @@ require "rails/generators/test_case" require "active_support/testing/autorun" +if defined?(Capbyara) + require "action_dispatch/system_test_case" +end + if defined?(ActiveRecord::Base) ActiveRecord::Migration.maintain_test_schema! - class ActiveSupport::TestCase - include ActiveRecord::TestFixtures - self.fixture_path = "#{Rails.root}/test/fixtures/" - self.file_fixture_path = self.fixture_path + "files" + module ActiveSupport + class TestCase + include ActiveRecord::TestFixtures + self.fixture_path = "#{Rails.root}/test/fixtures/" + self.file_fixture_path = fixture_path + "files" + end end ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path @@ -27,6 +33,8 @@ if defined?(ActiveRecord::Base) end end +# :enddoc: + class ActionController::TestCase def before_setup # :nodoc: @routes = Rails.application.routes @@ -40,3 +48,12 @@ class ActionDispatch::IntegrationTest super end end + +if defined? Capybara + class ActionDispatch::SystemTestCase + def before_setup # :nodoc: + @routes = Rails.application.routes + super + end + end +end diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb index 6e196a32ab..e44fe78bbd 100644 --- a/railties/lib/rails/test_unit/minitest_plugin.rb +++ b/railties/lib/rails/test_unit/minitest_plugin.rb @@ -52,21 +52,25 @@ module Minitest options[:color] = value end + opts.on("-w", "--warnings", + "Enable ruby warnings") do + $VERBOSE = true + end + options[:color] = true options[:output_inline] = true - options[:patterns] = defined?(@rake_patterns) ? @rake_patterns : opts.order! + options[:patterns] = opts.order! unless run_via.rake? end - # Running several Rake tasks in a single command would trip up the runner, - # as the patterns would also contain the other Rake tasks. def self.rake_run(patterns) # :nodoc: - @rake_patterns = patterns + self.run_via = :rake unless run_via.set? + ::Rails::TestRequirer.require_files(patterns) autorun end module RunRespectingRakeTestopts def run(args = []) - if defined?(@rake_patterns) + if run_via.rake? args = Shellwords.split(ENV["TESTOPTS"] || "") end @@ -81,8 +85,9 @@ module Minitest def self.plugin_rails_init(options) ENV["RAILS_ENV"] = options[:environment] || "test" - # If run via `ruby` we've been passed the files to run directly. - unless run_via[:ruby] + # If run via `ruby` we've been passed the files to run directly, or if run + # via `rake` then they have already been eagerly required. + unless run_via.ruby? || run_via.rake? ::Rails::TestRequirer.require_files(options[:patterns]) end @@ -97,7 +102,33 @@ module Minitest reporter << ::Rails::TestUnitReporter.new(options[:io], options) end - mattr_accessor(:run_via) { Hash.new } + def self.run_via=(runner) + if run_via.set? + raise ArgumentError, "run_via already assigned" + else + run_via.runner = runner + end + end + + class RunVia + attr_accessor :runner + alias set? runner + + # Backwardscompatibility with Rails 5.0 generated plugin test scripts. + def []=(runner, *) + @runner = runner + end + + def ruby? + runner == :ruby + end + + def rake? + runner == :rake + end + end + + mattr_reader(:run_via) { RunVia.new } end # Put Rails as the first plugin minitest initializes so other plugins diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb index 746120e6a1..9cc3f73a9c 100644 --- a/railties/lib/rails/test_unit/railtie.rb +++ b/railties/lib/rails/test_unit/railtie.rb @@ -11,6 +11,7 @@ module Rails fixture_replacement: nil c.integration_tool :test_unit + c.system_tests :test_unit end initializer "test_unit.line_filtering" do diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 4c157c1262..4dde3d3c97 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -47,4 +47,9 @@ namespace :test do $: << "test" Minitest.rake_run(["test/controllers", "test/mailers", "test/functional"]) end + + task system: "test:prepare" do + $: << "test" + Minitest.rake_run(["test/system"]) + end end diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index fd1e1b9662..e4b2d0457d 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -17,15 +17,15 @@ module TestApp end end -# Skips the current run on Rubinius using Minitest::Assertions#skip -def rubinius_skip(message = "") - skip message if RUBY_ENGINE == "rbx" -end -# Skips the current run on JRuby using Minitest::Assertions#skip -def jruby_skip(message = "") - skip message if defined?(JRUBY_VERSION) -end - class ActiveSupport::TestCase include ActiveSupport::Testing::Stream + + # Skips the current run on Rubinius using Minitest::Assertions#skip + private def rubinius_skip(message = "") + skip message if RUBY_ENGINE == "rbx" + end + # Skips the current run on JRuby using Minitest::Assertions#skip + private def jruby_skip(message = "") + skip message if defined?(JRUBY_VERSION) + end end diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb index f62313f3e1..0fb995900f 100644 --- a/railties/test/application/bin_setup_test.rb +++ b/railties/test/application/bin_setup_test.rb @@ -38,9 +38,12 @@ module ApplicationTests app_file "db/schema.rb", "" output = `bin/setup 2>&1` + + # Ignore line that's only output by Bundler < 1.14 + output.sub!(/^Resolving dependencies\.\.\.\n/, "") + assert_equal(<<-OUTPUT, output) == Installing dependencies == -Resolving dependencies... The Gemfile's dependencies are satisfied == Preparing database == diff --git a/railties/test/application/configuration/custom_test.rb b/railties/test/application/configuration/custom_test.rb index 3c675eec71..8360b7bf4b 100644 --- a/railties/test/application/configuration/custom_test.rb +++ b/railties/test/application/configuration/custom_test.rb @@ -10,7 +10,6 @@ module ApplicationTests def teardown teardown_app - FileUtils.rm_rf(new_app) if File.directory?(new_app) end test "access custom configuration point" do @@ -30,28 +29,14 @@ module ApplicationTests assert_equal false, x.hyper_debugger assert_nil x.nil_debugger assert_nil x.i_do_not_exist.zomg - end - test "custom configuration responds to all messages" do - x = Rails.configuration.x + # test that custom configuration responds to all messages assert_equal true, x.respond_to?(:i_do_not_exist) assert_kind_of Method, x.method(:i_do_not_exist) assert_kind_of ActiveSupport::OrderedOptions, x.i_do_not_exist end private - def new_app - File.expand_path("#{app_path}/../new_app") - end - - def copy_app - FileUtils.cp_r(app_path, new_app) - end - - def app - @app ||= Rails.application - end - def require_environment require "#{app_path}/config/environment" end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 31c3b5cf52..14433fbba0 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -51,7 +51,7 @@ module ApplicationTests def setup build_app - supress_default_config + suppress_default_config end def teardown @@ -59,7 +59,7 @@ module ApplicationTests FileUtils.rm_rf(new_app) if File.directory?(new_app) end - def supress_default_config + def suppress_default_config FileUtils.mv("#{app_path}/config/environments", "#{app_path}/config/__environments__") end @@ -78,6 +78,18 @@ module ApplicationTests end end + test "Rails.env falls back to development if RAILS_ENV is blank and RACK_ENV is nil" do + with_rails_env("") do + assert_equal "development", Rails.env + end + end + + test "Rails.env falls back to development if RACK_ENV is blank and RAILS_ENV is nil" do + with_rack_env("") do + assert_equal "development", Rails.env + end + end + test "By default logs tags are not set in development" do restore_default_config @@ -369,26 +381,6 @@ module ApplicationTests end end - test "config.serve_static_files is deprecated" do - make_basic_app do |application| - assert_deprecated do - application.config.serve_static_files = true - end - - assert application.config.public_file_server.enabled - end - end - - test "config.static_cache_control is deprecated" do - make_basic_app do |application| - assert_deprecated do - application.config.static_cache_control = "public, max-age=60" - end - - assert_equal application.config.static_cache_control, "public, max-age=60" - end - end - test "Use key_generator when secret_key_base is set" do make_basic_app do |application| application.secrets.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" @@ -631,9 +623,10 @@ module ApplicationTests assert_equal "", app.config.secret_token assert_nil app.secrets.secret_key_base - assert_raise ArgumentError, /\AA secret is required/ do + e = assert_raise ArgumentError do app.key_generator end + assert_match(/\AA secret is required/, e.message) end test "that nested keys are symbolized the same as parents for hashes more than one level deep" do @@ -992,7 +985,7 @@ module ApplicationTests class ::OmgController < ActionController::Base def index - render plain: env["action_dispatch.show_exceptions"] + render plain: request.env["action_dispatch.show_exceptions"] end end @@ -1192,11 +1185,12 @@ module ApplicationTests end test "config.session_store with :active_record_store without activerecord-session_store gem" do - assert_raise RuntimeError, /activerecord-session_store/ do + e = assert_raise RuntimeError do make_basic_app do |application| application.config.session_store :active_record_store end end + assert_match(/activerecord-session_store/, e.message) end test "default session store initializer does not overwrite the user defined session store even if it is disabled" do diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb index 790aca2aa4..c3a360e5d4 100644 --- a/railties/test/application/mailer_previews_test.rb +++ b/railties/test/application/mailer_previews_test.rb @@ -671,6 +671,40 @@ module ApplicationTests assert_match %r[<p>Hello, World!</p>], last_response.body end + test "multipart mailer preview with empty parts" do + mailer "notifier", <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template "notifier/foo", <<-RUBY + RUBY + + html_template "notifier/foo", <<-RUBY + RUBY + + mailer_preview "notifier", <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app("development") + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + + get "/rails/mailers/notifier/foo?part=text/html" + assert_equal 200, last_response.status + end + private def build_app super diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 51db634b75..c63f23fa0a 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -1,5 +1,4 @@ require "isolation/abstract_unit" -require "active_support/core_ext/string/strip" module ApplicationTests module RakeTests @@ -175,7 +174,7 @@ module ApplicationTests `bin/rails generate model book title:string; bin/rails db:migrate db:structure:dump` structure_dump = File.read("db/structure.sql") - assert_match(/CREATE TABLE \"books\"/, structure_dump) + assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"books\"/, structure_dump) `bin/rails environment db:drop db:structure:load` assert_match expected_database, ActiveRecord::Base.connection_config[:database] require "#{app_path}/app/models/book" @@ -203,7 +202,7 @@ module ApplicationTests stderr_output = capture(:stderr) { `bin/rails db:structure:dump` } assert_empty stderr_output structure_dump = File.read("db/structure.sql") - assert_match(/CREATE TABLE \"posts\"/, structure_dump) + assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"posts\"/, structure_dump) end end diff --git a/railties/test/application/rake/framework_test.rb b/railties/test/application/rake/framework_test.rb index 7ac37b7700..40488a6aab 100644 --- a/railties/test/application/rake/framework_test.rb +++ b/railties/test/application/rake/framework_test.rb @@ -1,5 +1,4 @@ require "isolation/abstract_unit" -require "active_support/core_ext/string/strip" module ApplicationTests module RakeTests diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index d80a45a83f..1b64a0a1ca 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -132,48 +132,6 @@ module ApplicationTests assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output end - def test_rails_routes_with_controller_environment - app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - get '/cart', to: 'cart#show' - get '/basketball', to: 'basketball#index' - end - RUBY - - output = Dir.chdir(app_path) { `bin/rails routes CONTROLLER=cart` } - assert_equal ["Passing `CONTROLLER` to `bin/rails routes` is deprecated and will be removed in Rails 5.1.", - "Please use `bin/rails routes -c controller_name` instead.", - "Prefix Verb URI Pattern Controller#Action", - " cart GET /cart(.:format) cart#show\n"].join("\n"), output - - output = Dir.chdir(app_path) { `bin/rails routes -c cart` } - assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output - end - - def test_rails_routes_with_namespaced_controller_environment - app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - namespace :admin do - resource :post - end - end - RUBY - expected_output = [" Prefix Verb URI Pattern Controller#Action", - " new_admin_post GET /admin/post/new(.:format) admin/posts#new", - "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit", - " admin_post GET /admin/post(.:format) admin/posts#show", - " PATCH /admin/post(.:format) admin/posts#update", - " PUT /admin/post(.:format) admin/posts#update", - " DELETE /admin/post(.:format) admin/posts#destroy", - " POST /admin/post(.:format) admin/posts#create\n"].join("\n") - - output = Dir.chdir(app_path) { `bin/rails routes -c Admin::PostController` } - assert_equal expected_output, output - - output = Dir.chdir(app_path) { `bin/rails routes -c PostController` } - assert_equal expected_output, output - end - def test_singular_resource_output_in_rake_routes app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do @@ -232,6 +190,30 @@ module ApplicationTests assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output end + def test_rails_routes_with_namespaced_controller_search_key + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + namespace :admin do + resource :post + end + end + RUBY + expected_output = [" Prefix Verb URI Pattern Controller#Action", + " new_admin_post GET /admin/post/new(.:format) admin/posts#new", + "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit", + " admin_post GET /admin/post(.:format) admin/posts#show", + " PATCH /admin/post(.:format) admin/posts#update", + " PUT /admin/post(.:format) admin/posts#update", + " DELETE /admin/post(.:format) admin/posts#destroy", + " POST /admin/post(.:format) admin/posts#create\n"].join("\n") + + output = Dir.chdir(app_path) { `bin/rails routes -c Admin::PostController` } + assert_equal expected_output, output + + output = Dir.chdir(app_path) { `bin/rails routes -c PostController` } + assert_equal expected_output, output + end + def test_rails_routes_displays_message_when_no_routes_are_defined app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do @@ -341,16 +323,6 @@ module ApplicationTests assert_no_match(/Errors running/, output) end - def test_db_test_clone_when_using_sql_format - add_to_config "config.active_record.schema_format = :sql" - output = Dir.chdir(app_path) do - `bin/rails generate scaffold user username:string; - bin/rails db:migrate; - bin/rails db:test:clone 2>&1 --trace` - end - assert_match(/Execute db:test:clone_structure/, output) - end - def test_db_test_prepare_when_using_sql_format add_to_config "config.active_record.schema_format = :sql" output = Dir.chdir(app_path) do diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index c515e2b270..6742da20cc 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -263,7 +263,10 @@ module ApplicationTests assert_equal "WIN", last_response.body end - { "development" => "baz", "production" => "bar" }.each do |mode, expected| + { + "development" => ["baz", "http://www.apple.com", "/dashboard"], + "production" => ["bar", "http://www.microsoft.com", "/profile"] + }.each do |mode, (expected_action, expected_url, expected_mapping)| test "reloads routes when configuration is changed in #{mode}" do controller :foo, <<-RUBY class FooController < ApplicationController @@ -274,12 +277,40 @@ module ApplicationTests def baz render plain: "baz" end + + def custom + render plain: custom_url + end + + def mapping + render plain: url_for(User.new) + end + end + RUBY + + app_file "app/models/user.rb", <<-RUBY + class User + extend ActiveModel::Naming + include ActiveModel::Conversion + + def model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, "User") + end + + def persisted? + false + end end RUBY app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#bar' + get 'custom', to: 'foo#custom' + get 'mapping', to: 'foo#mapping' + + direct(:custom) { "http://www.microsoft.com" } + resolve("User") { "/profile" } end RUBY @@ -288,16 +319,33 @@ module ApplicationTests get "/foo" assert_equal "bar", last_response.body + get "/custom" + assert_equal "http://www.microsoft.com", last_response.body + + get "/mapping" + assert_equal "/profile", last_response.body + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#baz' + get 'custom', to: 'foo#custom' + get 'mapping', to: 'foo#mapping' + + direct(:custom) { "http://www.apple.com" } + resolve("User") { "/dashboard" } end RUBY sleep 0.1 get "/foo" - assert_equal expected, last_response.body + assert_equal expected_action, last_response.body + + get "/custom" + assert_equal expected_url, last_response.body + + get "/mapping" + assert_equal expected_mapping, last_response.body end end @@ -358,6 +406,14 @@ module ApplicationTests def index render plain: "foo" end + + def custom + render plain: custom_url + end + + def mapping + render plain: url_for(User.new) + end end RUBY @@ -369,6 +425,21 @@ module ApplicationTests end RUBY + app_file "app/models/user.rb", <<-RUBY + class User + extend ActiveModel::Naming + include ActiveModel::Conversion + + def model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, "User") + end + + def persisted? + false + end + end + RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#index' @@ -389,6 +460,12 @@ module ApplicationTests Rails.application.routes.draw do get 'foo', to: 'foo#index' get 'bar', to: 'bar#index' + + get 'custom', to: 'foo#custom' + direct(:custom) { 'http://www.apple.com' } + + get 'mapping', to: 'foo#mapping' + resolve('User') { '/profile' } end RUBY @@ -402,6 +479,14 @@ module ApplicationTests assert_equal "bar", last_response.body assert_equal "/bar", Rails.application.routes.url_helpers.bar_path + get "/custom" + assert_equal "http://www.apple.com", last_response.body + assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url + + get "/mapping" + assert_equal "/profile", last_response.body + assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new) + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get 'foo', to: 'foo#index' @@ -419,6 +504,18 @@ module ApplicationTests assert_raises NoMethodError do assert_equal "/bar", Rails.application.routes.url_helpers.bar_path end + + get "/custom" + assert_equal 404, last_response.status + assert_raises NoMethodError do + assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.custom_url + end + + get "/mapping" + assert_equal 404, last_response.status + assert_raises NoMethodError do + assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new) + end end test "named routes are cleared when reloading" do @@ -440,19 +537,41 @@ module ApplicationTests end RUBY + app_file "app/models/user.rb", <<-RUBY + class User + extend ActiveModel::Naming + include ActiveModel::Conversion + + def model_name + @_model_name ||= ActiveModel::Name.new(self.class, nil, "User") + end + + def persisted? + false + end + end + RUBY + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':locale/foo', to: 'foo#index', as: 'foo' + get 'users', to: 'foo#users', as: 'users' + direct(:microsoft) { 'http://www.microsoft.com' } + resolve('User') { '/profile' } end RUBY get "/en/foo" assert_equal "foo", last_response.body assert_equal "/en/foo", Rails.application.routes.url_helpers.foo_path(locale: "en") + assert_equal "http://www.microsoft.com", Rails.application.routes.url_helpers.microsoft_url + assert_equal "/profile", Rails.application.routes.url_helpers.polymorphic_path(User.new) app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get ':locale/bar', to: 'bar#index', as: 'foo' + get 'users', to: 'foo#users', as: 'users' + direct(:apple) { 'http://www.apple.com' } end RUBY @@ -464,6 +583,12 @@ module ApplicationTests get "/en/bar" assert_equal "bar", last_response.body assert_equal "/en/bar", Rails.application.routes.url_helpers.foo_path(locale: "en") + assert_equal "http://www.apple.com", Rails.application.routes.url_helpers.apple_url + assert_equal "/users", Rails.application.routes.url_helpers.polymorphic_path(User.new) + + assert_raises NoMethodError do + assert_equal "http://www.microsoft.com", Rails.application.routes.url_helpers.microsoft_url + end end test "resource routing with irregular inflection" do @@ -493,5 +618,63 @@ module ApplicationTests get "/yazilar" assert_equal 200, last_response.status end + + test "reloading routes removes methods and doesn't undefine them" do + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/url', to: 'url#index' + end + RUBY + + app_file "app/models/url_helpers.rb", <<-RUBY + module UrlHelpers + def foo_path + "/foo" + end + end + RUBY + + app_file "app/models/context.rb", <<-RUBY + class Context + include UrlHelpers + include Rails.application.routes.url_helpers + end + RUBY + + controller "url", <<-RUBY + class UrlController < ApplicationController + def index + context = Context.new + render plain: context.foo_path + end + end + RUBY + + get "/url" + assert_equal "/foo", last_response.body + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/url', to: 'url#index' + get '/bar', to: 'foo#index', as: 'foo' + end + RUBY + + Rails.application.reload_routes! + + get "/url" + assert_equal "/bar", last_response.body + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '/url', to: 'url#index' + end + RUBY + + Rails.application.reload_routes! + + get "/url" + assert_equal "/foo", last_response.body + end end end diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index 0939587960..ee03d8b86c 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -15,6 +15,16 @@ module ApplicationTests teardown_app end + def test_run_via_backwardscompatibility + require "rails/test_unit/minitest_plugin" + + assert_nothing_raised do + Minitest.run_via[:ruby] = true + end + + assert_predicate Minitest.run_via, :ruby? + end + def test_run_single_file create_test_file :models, "foo" create_test_file :models, "bar" @@ -536,6 +546,32 @@ module ApplicationTests assert_match "seed=1234", output, "passing TEST= should run selected test" end + def test_rake_runs_multiple_test_tasks + create_test_file :models, "account" + create_test_file :controllers, "accounts_controller" + output = Dir.chdir(app_path) { `bin/rake test:models test:controllers TESTOPTS='-v'` } + assert_match "AccountTest#test_truth", output + assert_match "AccountsControllerTest#test_truth", output + end + + def test_rake_db_and_test_tasks_parses_args_correctly + create_test_file :models, "account" + output = Dir.chdir(app_path) { `bin/rake db:migrate test:models TESTOPTS='-v' && echo ".tables" | rails dbconsole` } + assert_match "AccountTest#test_truth", output + assert_match "ar_internal_metadata", output + end + + def test_warnings_option + app_file "test/models/warnings_test.rb", <<-RUBY + require 'test_helper' + def test_warnings + a = 1 + end + RUBY + assert_match(/warning: assigned but unused variable/, + capture(:stderr) { run_test_command("test/models/warnings_test.rb -w") }) + end + private def run_test_command(arguments = "test/unit/test_test.rb") Dir.chdir(app_path) { `bin/rails t #{arguments}` } diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index 527529aa52..e3dfc3e82b 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -122,11 +122,11 @@ class Rails::ServerTest < ActiveSupport::TestCase end def test_default_options - old_default_options = parse_arguments + server = Rails::Server.new + old_default_options = server.default_options Dir.chdir("..") do - default_options = parse_arguments - assert_equal old_default_options, default_options + assert_equal old_default_options, server.default_options end end diff --git a/railties/test/engine/commands_tasks_test.rb b/railties/test/engine/commands_tasks_test.rb deleted file mode 100644 index 817175b9ef..0000000000 --- a/railties/test/engine/commands_tasks_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require "abstract_unit" - -class Rails::Engine::CommandsTasksTest < ActiveSupport::TestCase - def setup - @destination_root = Dir.mktmpdir("bukkits") - Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --mountable` } - end - - def teardown - FileUtils.rm_rf(@destination_root) - end - - def test_help_command_work_inside_engine - output = capture(:stderr) do - Dir.chdir(plugin_path) { `bin/rails --help` } - end - assert_no_match "NameError", output - end - - private - def plugin_path - "#{@destination_root}/bukkits" - end -end diff --git a/railties/test/engine/commands_test.rb b/railties/test/engine/commands_test.rb new file mode 100644 index 0000000000..b1c937143f --- /dev/null +++ b/railties/test/engine/commands_test.rb @@ -0,0 +1,96 @@ +require "abstract_unit" +begin + require "pty" +rescue LoadError +end + +class Rails::Engine::CommandsTest < ActiveSupport::TestCase + def setup + @destination_root = Dir.mktmpdir("bukkits") + Dir.chdir(@destination_root) { `bundle exec rails plugin new bukkits --mountable` } + end + + def teardown + FileUtils.rm_rf(@destination_root) + end + + def test_help_command_work_inside_engine + output = capture(:stderr) do + Dir.chdir(plugin_path) { `bin/rails --help` } + end + assert_no_match "NameError", output + end + + def test_runner_command_work_inside_engine + output = capture(:stdout) do + Dir.chdir(plugin_path) { system("bin/rails runner 'puts Rails.env'") } + end + + assert_equal "test", output.strip + end + + def test_console_command_work_inside_engine + skip "PTY unavailable" unless available_pty? + + master, slave = PTY.open + spawn_command("console", slave) + assert_output(">", master) + ensure + master.puts "quit" + end + + def test_dbconsole_command_work_inside_engine + skip "PTY unavailable" unless available_pty? + + master, slave = PTY.open + spawn_command("dbconsole", slave) + assert_output("sqlite>", master) + ensure + master.puts ".exit" + end + + def test_server_command_work_inside_engine + skip "PTY unavailable" unless available_pty? + + master, slave = PTY.open + pid = spawn_command("server", slave) + assert_output("Listening on", master) + ensure + kill(pid) + end + + private + def plugin_path + "#{@destination_root}/bukkits" + end + + def assert_output(expected, io, timeout = 10) + timeout = Time.now + timeout + + output = "" + until output.include?(expected) || Time.now > timeout + if IO.select([io], [], [], 0.1) + output << io.read(1) + end + end + + assert_includes output, expected, "#{expected.inspect} expected, but got:\n\n#{output}" + end + + def spawn_command(command, fd) + Process.spawn( + "#{plugin_path}/bin/rails #{command}", + in: fd, out: fd, err: fd + ) + end + + def available_pty? + defined?(PTY) && PTY.respond_to?(:open) + end + + def kill(pid) + Process.kill("TERM", pid) + Process.wait(pid) + rescue Errno::ESRCH + end +end diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb index c54d9cc599..bdef1798f8 100644 --- a/railties/test/generators/api_app_generator_test.rb +++ b/railties/test/generators/api_app_generator_test.rb @@ -117,7 +117,7 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase public/apple-touch-icon-precomposed.png public/apple-touch-icon.png public/favicon.icon - vendor/package.json + package.json ) end end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 20de2258c5..1ac2b4cde0 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -42,6 +42,7 @@ DEFAULT_APP_FILES = %w( test/helpers test/mailers test/integration + test/system vendor tmp tmp/cache @@ -208,7 +209,6 @@ class AppGeneratorTest < Rails::Generators::TestCase quietly { generator.send(:update_config_files) } assert_file "#{app_root}/config/initializers/new_framework_defaults.rb" do |content| - assert_match(/ActiveSupport\.halt_callback_chains_on_return_false = true/, content) assert_match(/Rails\.application\.config.active_record\.belongs_to_required_by_default = false/, content) assert_no_match(/Rails\.application\.config\.ssl_options/, content) end @@ -340,7 +340,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generator_defaults_to_puma_version run_generator [destination_root] - assert_gem "puma", "'~> 3.0'" + assert_gem "puma", "'~> 3.7'" end def test_generator_if_skip_puma_is_given @@ -422,7 +422,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generator_if_skip_yarn_is_given run_generator [destination_root, "--skip-yarn"] - assert_no_file "vendor/package.json" + assert_no_file "package.json" assert_no_file "bin/yarn" end @@ -497,17 +497,22 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generator_for_yarn run_generator([destination_root]) - assert_file "vendor/package.json", /dependencies/ + assert_file "package.json", /dependencies/ assert_file "config/initializers/assets.rb", /node_modules/ end def test_generator_for_yarn_skipped run_generator([destination_root, "--skip-yarn"]) - assert_no_file "vendor/package.json" + assert_no_file "package.json" assert_file "config/initializers/assets.rb" do |content| assert_no_match(/node_modules/, content) end + + assert_file ".gitignore" do |content| + assert_no_match(/node_modules/, content) + assert_no_match(/yarn-error\.log/, content) + end end def test_inclusion_of_jbuilder @@ -592,6 +597,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_pretend_option output = run_generator [File.join(destination_root, "myapp"), "--pretend"] assert_no_match(/run bundle install/, output) + assert_no_match(/run git init/, output) end def test_application_name_with_spaces @@ -731,6 +737,11 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_version_control_initializes_git_repo + run_generator [destination_root] + assert_directory ".git" + end + def test_create_keeps run_generator folders_with_keep = %w( @@ -777,7 +788,7 @@ class AppGeneratorTest < Rails::Generators::TestCase template end - sequence = ["install", "exec spring binstub --all", "echo ran after_bundle"] + sequence = ["git init", "install", "exec spring binstub --all", "echo ran after_bundle"] @sequence_step ||= 0 ensure_bundler_first = -> command do assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}" @@ -792,11 +803,29 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - assert_equal 3, @sequence_step + assert_equal 4, @sequence_step end - private + def test_system_tests_directory_generated + run_generator + assert_file("test/system/.keep") + assert_directory("test/system") + end + + def test_system_tests_are_not_generated_on_system_test_skip + run_generator [destination_root, "--skip-system-test"] + + assert_no_directory("test/system") + end + + def test_system_tests_are_not_generated_on_test_skip + run_generator [destination_root, "--skip-test"] + + assert_no_directory("test/system") + end + + private def stub_rails_application(root) Rails.application.config.root = root Rails.application.class.stub(:name, "Myapp") do diff --git a/railties/test/generators/integration_test_generator_test.rb b/railties/test/generators/integration_test_generator_test.rb index 8bcc02440a..9358b63bd4 100644 --- a/railties/test/generators/integration_test_generator_test.rb +++ b/railties/test/generators/integration_test_generator_test.rb @@ -3,10 +3,14 @@ require "rails/generators/rails/integration_test/integration_test_generator" class IntegrationTestGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper - arguments %w(integration) def test_integration_test_skeleton_is_created - run_generator + run_generator %w(integration) assert_file "test/integration/integration_test.rb", /class IntegrationTest < ActionDispatch::IntegrationTest/ end + + def test_namespaced_integration_test_skeleton_is_created + run_generator %w(iguchi/integration) + assert_file "test/integration/iguchi/integration_test.rb", /class Iguchi::IntegrationTest < ActionDispatch::IntegrationTest/ + end end diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 6e1d1b70a9..f46278cefe 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -309,6 +309,17 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_add_migration_to_configured_path + old_paths = Rails.application.config.paths["db/migrate"] + Rails.application.config.paths.add "db/migrate", with: "db2/migrate" + + migration = "migration_in_custom_path" + run_generator [migration] + assert_migration "db2/migrate/#{migration}.rb", /.*/ + ensure + Rails.application.config.paths["db/migrate"] = old_paths + end + private def with_singular_table_name diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index 2b9f3ed7f2..99490af3a9 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -220,6 +220,17 @@ class ModelGeneratorTest < Rails::Generators::TestCase ActiveRecord::Base.timestamped_migrations = true end + def test_migration_with_configured_path + old_paths = Rails.application.config.paths["db/migrate"] + Rails.application.config.paths.add "db/migrate", with: "db2/migrate" + + run_generator + + assert_migration "db2/migrate/create_accounts.rb", /class CreateAccounts < ActiveRecord::Migration\[[0-9.]+\]/ + ensure + Rails.application.config.paths["db/migrate"] = old_paths + end + def test_model_with_references_attribute_generates_belongs_to_associations run_generator ["product", "name:string", "supplier:references"] assert_file "app/models/product.rb", /belongs_to :supplier/ diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 9921a80342..eaf1199601 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -421,6 +421,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "--full"] assert_file "bin/rails", /ENGINE_PATH = File.expand_path\('..\/..\/lib\/bukkits\/engine', __FILE__\)/ assert_file "bin/rails", /ENGINE_ROOT = File.expand_path\('..\/..', __FILE__\)/ + assert_file "bin/rails", %r|APP_PATH = File.expand_path\('../../test/dummy/config/application', __FILE__\)| assert_file "bin/rails", /require 'rails\/all'/ assert_file "bin/rails", /require 'rails\/engine\/commands'/ end @@ -490,6 +491,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_no_directory "test/dummy/doc" assert_no_directory "test/dummy/test" assert_no_directory "test/dummy/vendor" + assert_no_directory "test/dummy/.git" end def test_skipping_test_files diff --git a/railties/test/generators/plugin_test_runner_test.rb b/railties/test/generators/plugin_test_runner_test.rb index 0bdf3b2726..0bdd2a77d2 100644 --- a/railties/test/generators/plugin_test_runner_test.rb +++ b/railties/test/generators/plugin_test_runner_test.rb @@ -92,6 +92,17 @@ class PluginTestRunnerTest < ActiveSupport::TestCase assert_equal 1, result.scan(/1 runs, 1 assertions, 0 failures/).length end + def test_warnings_option + plugin_file "test/models/warnings_test.rb", <<-RUBY + require 'test_helper' + def test_warnings + a = 1 + end + RUBY + assert_match(/warning: assigned but unused variable/, + capture(:stderr) { run_test_command("test/models/warnings_test.rb -w") }) + end + private def plugin_path "#{@destination_root}/bukkits" diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index bd23faf268..9971626f9f 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -230,6 +230,12 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_match(/@user\.destroy/, m) end end + + assert_no_file "app/views/users/index.html.erb" + assert_no_file "app/views/users/edit.html.erb" + assert_no_file "app/views/users/show.html.erb" + assert_no_file "app/views/users/new.html.erb" + assert_no_file "app/views/users/_form.html.erb" end def test_api_controller_tests diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index 6b7e2c91d7..436fbd5d73 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -62,6 +62,11 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase assert_match(/patch product_line_url\(@product_line\), params: \{ product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \} \}/, test) end + # System tests + assert_file "test/system/product_lines_test.rb" do |test| + assert_match(/class ProductLinesTest < ApplicationSystemTestCase/, test) + end + # Views assert_no_file "app/views/layouts/product_lines.html.erb" @@ -492,6 +497,26 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase end end + def test_scaffold_tests_pass_by_default_inside_namespaced_mountable_engine + Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits-admin --mountable` } + + engine_path = File.join(destination_root, "bukkits-admin") + + Dir.chdir(engine_path) do + quietly do + `bin/rails g scaffold User name:string age:integer; + bin/rails db:migrate` + end + + assert_file "bukkits-admin/app/controllers/bukkits/admin/users_controller.rb" do |content| + assert_match(/module Bukkits::Admin/, content) + assert_match(/class UsersController < ApplicationController/, content) + end + + assert_match(/8 runs, 10 assertions, 0 failures, 0 errors/, `bin/rails test 2>&1`) + end + end + def test_scaffold_tests_pass_by_default_inside_full_engine Dir.chdir(destination_root) { `bundle exec rails plugin new bukkits --full` } diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 08b0e34fe2..cc9d3629e9 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -109,6 +109,7 @@ module SharedGeneratorTests def test_skip_git run_generator [destination_root, "--skip-git", "--full"] assert_no_file(".gitignore") + assert_no_directory(".git") end def test_skip_keeps diff --git a/railties/test/generators/system_test_generator_test.rb b/railties/test/generators/system_test_generator_test.rb new file mode 100644 index 0000000000..e8e561ec49 --- /dev/null +++ b/railties/test/generators/system_test_generator_test.rb @@ -0,0 +1,12 @@ +require "generators/generators_test_helper" +require "rails/generators/rails/system_test/system_test_generator" + +class SystemTestGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + arguments %w(user) + + def test_system_test_skeleton_is_created + run_generator + assert_file "test/system/users_test.rb", /class UsersTest < ApplicationSystemTestCase/ + end +end diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb index 7dd91a2465..33b4bc6a3a 100644 --- a/railties/test/rack_logger_test.rb +++ b/railties/test/rack_logger_test.rb @@ -20,7 +20,7 @@ module Rails def development?; false; end end - class Subscriber < Struct.new(:starts, :finishes) + Subscriber = Struct.new(:starts, :finishes) do def initialize(starts = [], finishes = []) super end diff --git a/tools/test.rb b/tools/test.rb index ce546b382d..71349a5974 100644 --- a/tools/test.rb +++ b/tools/test.rb @@ -16,5 +16,5 @@ end ActiveSupport::TestCase.extend Rails::LineFiltering Rails::TestUnitReporter.executable = "bin/test" -Minitest.run_via[:rails] = true +Minitest.run_via = :rails require "active_support/testing/autorun" |