diff options
726 files changed, 12684 insertions, 10369 deletions
diff --git a/.travis.yml b/.travis.yml index 8eee7b129b..4c45265db0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,8 @@ rvm: - jruby env: - "GEM=railties" - - "GEM=ap,am,amo,as,av" + - "GEM=ap" + - "GEM=am,amo,as,av,aj" - "GEM=ar:mysql" - "GEM=ar:mysql2" - "GEM=ar:sqlite3" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 19b7b638b6..9ba2e53ef2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,5 +12,6 @@ Ruby on Rails is a volunteer effort. We encourage you to pitch in. [Join the tea * If you have a change or new feature in mind, please [suggest it on the rubyonrails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core) and start writing code. -Thanks! :heart: :heart: :heart: <br /> +Thanks! :heart: :heart: :heart: + Rails Team @@ -7,11 +7,14 @@ gemspec # ensure correct loading order gem 'mocha', '~> 0.14', require: false +gem 'rack', github: 'rack/rack', branch: 'master' gem 'rack-cache', '~> 1.2' gem 'jquery-rails', '~> 3.1.0' -gem 'turbolinks' +gem 'turbolinks', github: 'rails/turbolinks', branch: 'master' gem 'coffee-rails', '~> 4.0.0' -gem 'arel', github: 'rails/arel', branch: 'master' +gem 'rails-html-sanitizer', github: 'rails/rails-html-sanitizer' +#temporary gem until a new version of loofah is released +gem 'loofah', github: 'kaspth/loofah', branch: 'single-scrub' gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master' gem 'i18n', github: 'svenfuchs/i18n', branch: 'master' @@ -34,6 +37,20 @@ end # AS gem 'dalli', '>= 2.2.1' +# ActiveJob +gem 'globalid', github: 'rails/globalid' +gem 'resque', require: false +gem 'resque-scheduler', require: false +gem 'sidekiq', require: false +gem 'sucker_punch', require: false +gem 'delayed_job', require: false +gem 'queue_classic', require: false +gem 'sneakers', '0.1.1.pre', require: false +gem 'que', require: false +gem 'backburner', require: false +gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false +gem 'qu-redis', require: false + # Add your own local bundler stuff local_gemfile = File.dirname(__FILE__) + "/.Gemfile" instance_eval File.read local_gemfile if File.exist? local_gemfile @@ -86,6 +103,12 @@ platforms :jruby do end end +platforms :rbx do + # The rubysl-yaml gem doesn't ship with Psych by default + # as it needs libyaml that isn't always available. + gem 'psych', '~> 2.0' +end + # gems that are necessary for ActiveRecord tests with Oracle database if ENV['ORACLE_ENHANCED'] platforms :ruby do @@ -67,7 +67,7 @@ independently outside Rails. * [Getting Started with Rails](http://guides.rubyonrails.org/getting_started.html) * [Ruby on Rails Guides](http://guides.rubyonrails.org) * [The API Documentation](http://api.rubyonrails.org) - * [Ruby on Rails Tutorial](http://ruby.railstutorial.org/ruby-on-rails-tutorial-book) + * [Ruby on Rails Tutorial](http://www.railstutorial.org/book) ## Contributing @@ -11,7 +11,7 @@ task :build => "all:build" desc "Release all gems to rubygems and create a tag" task :release => "all:release" -PROJECTS = %w(activesupport activemodel actionpack actionview actionmailer activerecord railties) +PROJECTS = %w(activesupport activemodel actionpack actionview actionmailer activerecord railties activejob) desc 'Run all tests by default' task :default => %w(test test:isolated) diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 97ff62fa87..e6952a4b3b 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,38 @@ +* Added #deliver_later in addition to #deliver, which will enqueue a job to render and + deliver the mail instead of delivering it right at that moment. The job is enqueued + using the new Active Job framework in Rails, and will use whatever queue is configured for Rails. + + *DHH/Abdelkader Boudih/Cristian Bica* + + +* Make ActionMailer::Previews methods class methods. Previously they were + instance methods and ActionMailer tries to render a message when they + are called. + + *Cristian Bica* + +* Deprecate `*_path` helpers in email views. When used they generate + non-working links and are not the intention of most developers. Instead + we recommend to use `*_url` helper. + + *Richard Schneeman* + +* Raise an exception when attachments are added after `mail` was called. + This is a safeguard to prevent invalid emails. + + Fixes #16163. + + *Yves Senn* + +* Add `config.action_mailer.show_previews` configuration option. + + This config option can be used to enable the mail preview in environments + other than development (such as staging). + + Defaults to `true` in development and false elsewhere. + + *Leonard Garvey* + * Allow preview interceptors to be registered through `config.action_mailer.preview_interceptors`. diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index 01d97b7213..8452348e11 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -23,4 +23,5 @@ Gem::Specification.new do |s| s.add_dependency 'actionview', version s.add_dependency 'mail', ['~> 2.5', '>= 2.5.4'] + s.add_dependency 'rails-dom-testing' end diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 83969d4074..b994ef3182 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -45,4 +45,6 @@ module ActionMailer autoload :Previews, 'action_mailer/preview' autoload :TestCase autoload :TestHelper + autoload :MessageDelivery + autoload :DeliveryJob end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 135cf35ac0..9aae14ec8c 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -394,7 +394,7 @@ module ActionMailer # implement for a custom delivery agent. # # * <tt>perform_deliveries</tt> - Determines whether emails are actually sent from Action Mailer when you - # call <tt>.deliver</tt> on an mail message or on an Action Mailer method. This is on by default but can + # call <tt>.deliver</tt> on an email message or on an Action Mailer method. This is on by default but can # be turned off to aid in functional testing. # # * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with @@ -548,8 +548,8 @@ module ActionMailer end def method_missing(method_name, *args) # :nodoc: - if respond_to?(method_name) - new(method_name, *args).message + if action_methods.include?(method_name.to_s) + MessageDelivery.new(self, method_name, *args) else super end @@ -586,6 +586,10 @@ module ActionMailer class NullMail #:nodoc: def body; '' end + def respond_to?(string, include_all=false) + true + end + def method_missing(*args) nil end @@ -649,7 +653,22 @@ module ActionMailer # mail.attachments[0] # => Mail::Part (first attachment) # def attachments - @_message.attachments + if @_mail_was_called + LateAttachmentsProxy.new(@_message.attachments) + else + @_message.attachments + end + end + + class LateAttachmentsProxy < SimpleDelegator + def inline; _raise_error end + def []=(_name, _content); _raise_error end + + private + def _raise_error + raise RuntimeError, "Can't add attachments after `mail` was called.\n" \ + "Make sure to use `attachments[]=` before calling `mail`." + end end # The main method that creates the message and renders the email templates. There are @@ -882,6 +901,11 @@ module ActionMailer container.add_part(part) end + # Emails do not support relative path links. + def self.supports_path? + false + end + ActiveSupport.run_load_hooks(:action_mailer, self) end end diff --git a/actionmailer/lib/action_mailer/delivery_job.rb b/actionmailer/lib/action_mailer/delivery_job.rb new file mode 100644 index 0000000000..b2cfa245fd --- /dev/null +++ b/actionmailer/lib/action_mailer/delivery_job.rb @@ -0,0 +1,11 @@ +require 'active_job' + +module ActionMailer + class DeliveryJob < ActiveJob::Base + queue_as :mailers + + def perform(mailer, mail_method, delivery_method, *args) + mailer.constantize.public_send(mail_method, *args).send(delivery_method) + end + end +end diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb index e4aaab34b1..5b57c75ec3 100644 --- a/actionmailer/lib/action_mailer/log_subscriber.rb +++ b/actionmailer/lib/action_mailer/log_subscriber.rb @@ -6,25 +6,27 @@ module ActionMailer class LogSubscriber < ActiveSupport::LogSubscriber # An email was delivered. def deliver(event) - return unless logger.info? - recipients = Array(event.payload[:to]).join(', ') - info("\nSent mail to #{recipients} (#{event.duration.round(1)}ms)") - debug(event.payload[:mail]) + info do + recipients = Array(event.payload[:to]).join(', ') + "\nSent mail to #{recipients} (#{event.duration.round(1)}ms)" + end + + debug { event.payload[:mail] } end # An email was received. def receive(event) - return unless logger.info? - info("\nReceived mail (#{event.duration.round(1)}ms)") - debug(event.payload[:mail]) + info { "\nReceived mail (#{event.duration.round(1)}ms)" } + debug { event.payload[:mail] } end # An email was generated. def process(event) - return unless logger.debug? - mailer = event.payload[:mailer] - action = event.payload[:action] - debug("\n#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms") + debug do + mailer = event.payload[:mailer] + action = event.payload[:action] + "\n#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms" + end end # Use the logger configured for ActionMailer::Base diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb new file mode 100644 index 0000000000..80a0517bff --- /dev/null +++ b/actionmailer/lib/action_mailer/message_delivery.rb @@ -0,0 +1,45 @@ +require 'delegate' + +module ActionMailer + class MessageDelivery < Delegator + def initialize(mailer, mail_method, *args) + @mailer = mailer + @mail_method = mail_method + @args = args + end + + def __getobj__ + @obj ||= @mailer.send(:new, @mail_method, *@args).message + end + + def __setobj__(obj) + @obj = obj + end + + def message #:nodoc: + __getobj__ + end + + def deliver_later!(options={}) + enqueue_delivery :deliver!, options + end + + def deliver_later(options={}) + enqueue_delivery :deliver, options + end + + private + def enqueue_delivery(delivery_method, options={}) + args = @mailer.name, @mail_method.to_s, delivery_method.to_s, *@args + enqueue_method = :enqueue + if options[:at] + enqueue_method = :enqueue_at + args.unshift options[:at] + elsif options[:in] + enqueue_method = :enqueue_in + args.unshift options[:in] + end + ActionMailer::DeliveryJob.send enqueue_method, *args + end + end +end diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb index 33a9faa7e7..44cf6665ba 100644 --- a/actionmailer/lib/action_mailer/preview.rb +++ b/actionmailer/lib/action_mailer/preview.rb @@ -11,10 +11,20 @@ module ActionMailer # mattr_accessor :preview_path, instance_writer: false + # Enable or disable mailer previews through app configuration: + # + # config.action_mailer.show_previews = true + # + # Defaults to true for development environment + # + mattr_accessor :show_previews, instance_writer: false + # :nodoc: mattr_accessor :preview_interceptors, instance_writer: false self.preview_interceptors = [] + end + module ClassMethods # Register one or more Interceptors which will be called before mail is previewed. def register_preview_interceptors(*interceptors) interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) } @@ -94,6 +104,10 @@ module ActionMailer Base.preview_path end + def show_previews #:nodoc: + Base.show_previews + end + def inform_preview_interceptors(message) #:nodoc: Base.preview_interceptors.each do |interceptor| interceptor.previewing_email(message) diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index 671551fa53..c62d4b5082 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -18,8 +18,9 @@ module ActionMailer options.assets_dir ||= paths["public"].first options.javascripts_dir ||= paths["public/javascripts"].first options.stylesheets_dir ||= paths["public/stylesheets"].first + options.show_previews = Rails.env.development? if options.show_previews.nil? - if Rails.env.development? + if options.show_previews options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil end @@ -29,7 +30,7 @@ module ActionMailer ActiveSupport.on_load(:action_mailer) do include AbstractController::UrlFor - extend ::AbstractController::Railties::RoutesHelpers.with(app.routes) + extend ::AbstractController::Railties::RoutesHelpers.with(app.routes, false) include app.routes.mounted_helpers register_interceptors(options.delete(:interceptors)) @@ -37,6 +38,13 @@ module ActionMailer register_observers(options.delete(:observers)) options.each { |k,v| send("#{k}=", v) } + + if options.show_previews + app.routes.append do + get '/rails/mailers' => "rails/mailers#index" + get '/rails/mailers/*path' => "rails/mailers#preview" + end + end end end diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index a5442c0316..d507032838 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -1,4 +1,5 @@ require 'active_support/test_case' +require 'rails-dom-testing' module ActionMailer class NonInferrableMailerError < ::StandardError @@ -15,6 +16,7 @@ module ActionMailer include ActiveSupport::Testing::ConstantLookup include TestHelper + include Rails::Dom::Testing::Assertions::SelectorAssertions included do class_attribute :_mailer_class @@ -55,14 +57,13 @@ module ActionMailer protected def initialize_test_deliveries - @old_delivery_method = ActionMailer::Base.delivery_method + set_delivery_method :test @old_perform_deliveries = ActionMailer::Base.perform_deliveries - ActionMailer::Base.delivery_method = :test ActionMailer::Base.perform_deliveries = true end def restore_test_deliveries - ActionMailer::Base.delivery_method = @old_delivery_method + restore_delivery_method ActionMailer::Base.perform_deliveries = @old_perform_deliveries ActionMailer::Base.deliveries.clear end diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index 4e5a9c3715..c549545674 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -26,27 +26,9 @@ I18n.enforce_available_locales = false FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__)) ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH -class MockSMTP - def self.deliveries - @@deliveries - end - - def initialize - @@deliveries = [] - end - - def sendmail(mail, from, to) - @@deliveries << [mail, from, to] - end - - def start(*args) - yield self - end -end - -class Net::SMTP - def self.new(*args) - MockSMTP.new +module Rails + def self.root + File.expand_path('../', File.dirname(__FILE__)) end end @@ -67,3 +49,5 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +require 'mocha/setup' # FIXME: stop using mocha diff --git a/actionmailer/test/assert_select_email_test.rb b/actionmailer/test/assert_select_email_test.rb new file mode 100644 index 0000000000..57ae3436e1 --- /dev/null +++ b/actionmailer/test/assert_select_email_test.rb @@ -0,0 +1,47 @@ +require 'abstract_unit' + +class AssertSelectEmailTest < ActionMailer::TestCase + class AssertSelectMailer < ActionMailer::Base + def test(html) + mail body: html, content_type: "text/html", + subject: "Test e-mail", from: "test@test.host", to: "test <test@test.host>" + end + end + + class AssertMultipartSelectMailer < ActionMailer::Base + def test(options) + mail subject: "Test e-mail", from: "test@test.host", to: "test <test@test.host>" do |format| + format.text { render text: options[:text] } + format.html { render text: options[:html] } + end + end + end + + # + # Test assert_select_email + # + + def test_assert_select_email + assert_raise ActiveSupport::TestCase::Assertion do + assert_select_email {} + end + + AssertSelectMailer.test("<div><p>foo</p><p>bar</p></div>").deliver + assert_select_email do + assert_select "div:root" do + assert_select "p:first-child", "foo" + assert_select "p:last-child", "bar" + end + end + end + + def test_assert_select_email_multipart + AssertMultipartSelectMailer.test(html: "<div><p>foo</p><p>bar</p></div>", text: 'foo bar').deliver + assert_select_email do + assert_select "div:root" do + assert_select "p:first-child", "foo" + assert_select "p:last-child", "bar" + end + end + end +end diff --git a/actionmailer/test/asset_host_test.rb b/actionmailer/test/asset_host_test.rb index 00f1348a53..9ba67c2842 100644 --- a/actionmailer/test/asset_host_test.rb +++ b/actionmailer/test/asset_host_test.rb @@ -31,25 +31,10 @@ class AssetHostTest < ActiveSupport::TestCase def test_asset_host_as_one_argument_proc AssetHostMailer.config.asset_host = Proc.new { |source| if source.starts_with?('/images') - "http://images.example.com" - else - "http://assets.example.com" + 'http://images.example.com' end } mail = AssetHostMailer.email_with_asset assert_equal %Q{<img alt="Somelogo" src="http://images.example.com/images/somelogo.png" />}, mail.body.to_s.strip end - - def test_asset_host_as_two_argument_proc - ActionController::Base.config.asset_host = Proc.new {|source,request| - if request && request.ssl? - "https://www.example.com" - else - "http://www.example.com" - end - } - mail = nil - assert_nothing_raised { mail = AssetHostMailer.email_with_asset } - assert_equal %Q{<img alt="Somelogo" src="http://www.example.com/images/somelogo.png" />}, mail.body.to_s.strip - end end diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 229ded8e04..fc24639bf4 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -11,6 +11,8 @@ require 'mailers/asset_mailer' class BaseTest < ActiveSupport::TestCase setup do + @original_delivery_method = ActionMailer::Base.delivery_method + ActionMailer::Base.delivery_method = :test @original_asset_host = ActionMailer::Base.asset_host @original_assets_dir = ActionMailer::Base.assets_dir end @@ -19,6 +21,7 @@ class BaseTest < ActiveSupport::TestCase ActionMailer::Base.asset_host = @original_asset_host ActionMailer::Base.assets_dir = @original_assets_dir BaseMailer.deliveries.clear + ActionMailer::Base.delivery_method = @original_delivery_method end test "method call to mail does not raise error" do @@ -232,6 +235,45 @@ class BaseTest < ActiveSupport::TestCase end end + test "adding attachments after mail was called raises exception" do + class LateAttachmentMailer < ActionMailer::Base + def welcome + mail body: "yay", from: "welcome@example.com", to: "to@example.com" + attachments['invoice.pdf'] = 'This is test File content' + end + end + + e = assert_raises(RuntimeError) { LateAttachmentMailer.welcome.message } + assert_match(/Can't add attachments after `mail` was called./, e.message) + end + + test "adding inline attachments after mail was called raises exception" do + class LateInlineAttachmentMailer < ActionMailer::Base + def welcome + mail body: "yay", from: "welcome@example.com", to: "to@example.com" + attachments.inline['invoice.pdf'] = 'This is test File content' + end + end + + e = assert_raises(RuntimeError) { LateInlineAttachmentMailer.welcome.message } + assert_match(/Can't add attachments after `mail` was called./, e.message) + end + + test "accessing attachments works after mail was called" do + class LateAttachmentAccessorMailer < ActionMailer::Base + def welcome + attachments['invoice.pdf'] = 'This is test File content' + mail body: "yay", from: "welcome@example.com", to: "to@example.com" + + unless attachments.map(&:filename) == ["invoice.pdf"] + raise Minitest::Assertion, "Should allow access to attachments" + end + end + end + + assert_nothing_raised { LateAttachmentAccessorMailer.welcome } + end + # Implicit multipart test "implicit multipart" do email = BaseMailer.implicit_multipart @@ -429,7 +471,6 @@ class BaseTest < ActiveSupport::TestCase end test "calling deliver on the action should increment the deliveries collection if using the test mailer" do - BaseMailer.delivery_method = :test BaseMailer.welcome.deliver assert_equal(1, BaseMailer.deliveries.length) end diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb index 16e8638542..a76ac6d295 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -108,6 +108,7 @@ class MailDeliveryTest < ActiveSupport::TestCase end test "delivery method can be customized per instance" do + Mail::SMTP.any_instance.expects(:deliver!) email = DeliveryMailer.welcome.deliver assert_instance_of Mail::SMTP, email.delivery_method email = DeliveryMailer.welcome(delivery_method: :test).deliver @@ -117,7 +118,6 @@ class MailDeliveryTest < ActiveSupport::TestCase test "delivery method can be customized in subclasses not changing the parent" do DeliveryMailer.delivery_method = :test assert_equal :smtp, ActionMailer::Base.delivery_method - $BREAK = true email = DeliveryMailer.welcome.deliver assert_instance_of Mail::TestMailer, email.delivery_method end diff --git a/actionmailer/test/fixtures/test_helper_mailer/welcome b/actionmailer/test/fixtures/test_helper_mailer/welcome new file mode 100644 index 0000000000..61ce70d578 --- /dev/null +++ b/actionmailer/test/fixtures/test_helper_mailer/welcome @@ -0,0 +1 @@ +Welcome!
\ No newline at end of file diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb index d9d588a950..ee36b89dd6 100644 --- a/actionmailer/test/i18n_with_controller_test.rb +++ b/actionmailer/test/i18n_with_controller_test.rb @@ -28,8 +28,23 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest get ':controller(/:action(/:id))' end + class RoutedRackApp + attr_reader :routes + + def initialize(routes, &blk) + @routes = routes + @stack = ActionDispatch::MiddlewareStack.new(&blk).build(@routes) + end + + def call(env) + @stack.call(env) + end + end + + APP = RoutedRackApp.new(Routes) + def app - Routes + APP end teardown do @@ -37,6 +52,7 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest end def test_send_mail + Mail::SMTP.any_instance.expects(:deliver!) with_translation 'de', email_subject: '[Anmeldung] Willkommen' do get '/test/send_mail' assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body diff --git a/actionmailer/test/mailers/delayed_mailer.rb b/actionmailer/test/mailers/delayed_mailer.rb new file mode 100644 index 0000000000..62d4baa434 --- /dev/null +++ b/actionmailer/test/mailers/delayed_mailer.rb @@ -0,0 +1,6 @@ +class DelayedMailer < ActionMailer::Base + + def test_message(*) + mail(from: 'test-sender@test.com', to: 'test-receiver@test.com', subject: 'Test Subject', body: 'Test Body') + end +end diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb new file mode 100644 index 0000000000..a097d0e84d --- /dev/null +++ b/actionmailer/test/message_delivery_test.rb @@ -0,0 +1,82 @@ +# encoding: utf-8 +gem 'activejob' +require 'active_job' +require 'abstract_unit' +require 'minitest/mock' +require_relative 'mailers/delayed_mailer' + +class MessageDeliveryTest < ActiveSupport::TestCase + + setup do + @previous_logger = ActiveJob::Base.logger + @previous_delivery_method = ActionMailer::Base.delivery_method + ActionMailer::Base.delivery_method = :test + ActiveJob::Base.logger = Logger.new(nil) + @mail = DelayedMailer.test_message(1, 2, 3) + ActionMailer::Base.deliveries.clear + end + + teardown do + ActiveJob::Base.logger = @previous_logger + ActionMailer::Base.delivery_method = @previous_delivery_method + end + + test 'should have a message' do + assert @mail.message + end + + test 'its message should be a Mail::Message' do + assert_equal Mail::Message , @mail.message.class + end + + test 'should respond to .deliver' do + assert_respond_to @mail, :deliver + end + + test 'should respond to .deliver!' do + assert_respond_to @mail, :deliver! + end + + test 'should respond to .deliver_later' do + assert_respond_to @mail, :deliver_later + end + + test 'should respond to .deliver_later!' do + assert_respond_to @mail, :deliver_later! + end + + test 'should enqueue and run correctly in activejob' do + @mail.deliver_later! + assert_equal 1 , ActionMailer::Base.deliveries.size + end + + test 'should enqueue the email with :deliver delivery method' do + ret = ActionMailer::DeliveryJob.stub :enqueue, ->(*args){ args } do + @mail.deliver_later + end + assert_equal ['DelayedMailer', 'test_message', 'deliver', 1, 2, 3], ret + end + + test 'should enqueue the email with :deliver! delivery method' do + ret = ActionMailer::DeliveryJob.stub :enqueue, ->(*args){ args } do + @mail.deliver_later! + end + assert_equal ['DelayedMailer', 'test_message', 'deliver!', 1, 2, 3], ret + end + + test 'should enqueue a delivery with a delay' do + ret = ActionMailer::DeliveryJob.stub :enqueue_in, ->(*args){ args } do + @mail.deliver_later in: 600 + end + assert_equal [600, 'DelayedMailer', 'test_message', 'deliver', 1, 2, 3], ret + end + + test 'should enqueue a delivery at a specific time' do + later_time = Time.now.to_i + 3600 + ret = ActionMailer::DeliveryJob.stub :enqueue_at, ->(*args){ args } do + @mail.deliver_later at: later_time + end + assert_equal [later_time, 'DelayedMailer', 'test_message', 'deliver', 1, 2, 3], ret + end + +end diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index 7c7f0b6fdc..1ff08a3b6e 100644 --- a/actionmailer/test/test_helper_test.rb +++ b/actionmailer/test/test_helper_test.rb @@ -1,3 +1,4 @@ +# encoding: utf-8 require 'abstract_unit' class TestHelperMailer < ActionMailer::Base @@ -36,6 +37,14 @@ class TestHelperMailerTest < ActionMailer::TestCase assert_equal "UTF-8", charset end + def test_encode + assert_equal '=?UTF-8?Q?This_is_=E3=81=82_string?=', encode('This is あ string') + end + + def test_read_fixture + assert_equal ['Welcome!'], read_fixture('welcome') + end + def test_assert_emails assert_nothing_raised do assert_emails 1 do diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index c38b31903b..ddacbe2aa2 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,125 @@ +* Deleted the deprecated TagAssertions. + + *Kasper Timm Hansen* + +* Use the Active Support JSON encoder for cookie jars using the `:json` or + `:hybrid` serializer. This allows you to serialize custom Ruby objects into + cookies by defining the `#as_json` hook on such objects. + + Fixes #16520. + + *Godfrey Chan* + +* Add `config.action_dispatch.cookies_digest` option for setting custom + digest. The default remains the same - 'SHA1'. + + *Łukasz Strzałkowski* + +* Move `respond_with` (and the class-level `respond_to`) to + the `responders` gem. + + *José Valim* + +* When your templates change, browser caches bust automatically. + + New default: the template digest is automatically included in your ETags. + When you call `fresh_when @post`, the digest for `posts/show.html.erb` + is mixed in so future changes to the HTML will blow HTTP caches for you. + This makes it easy to HTTP-cache many more of your actions. + + If you render a different template, you can now pass the `:template` + option to include its digest instead: + + fresh_when @post, template: 'widgets/show' + + Pass `template: false` to skip the lookup. To turn this off entirely, set: + + config.action_controller.etag_with_template_digest = false + + *Jeremy Kemper* + +* Remove deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError` + in favor of `AbstractController::Helpers::MissingHelperError`. + + *Yves Senn* + +* Fix `assert_template` not being able to assert that no files were rendered. + + *Guo Xiang Tan* + +* Extract source code for the entire exception stack trace for + better debugging and diagnosis. + + *Ryan Dao* + +* Allows ActionDispatch::Request::LOCALHOST to match any IPv4 127.0.0.0/8 + loopback address. + + *Earl St Sauver*, *Sven Riedel* + +* Preserve original path in `ShowExceptions` middleware by stashing it as + `env["action_dispatch.original_path"]` + + `ActionDispatch::ShowExceptions` overwrites `PATH_INFO` with the status code + for the exception defined in `ExceptionWrapper`, so the path + the user was visiting when an exception occurred was not previously + available to any custom exceptions_app. The original `PATH_INFO` is now + stashed in `env["action_dispatch.original_path"]`. + + *Grey Baker* + +* Use `String#bytesize` instead of `String#size` when checking for cookie + overflow. + + *Agis Anastasopoulos* + +* `render nothing: true` or rendering a `nil` body no longer add a single + space to the response body. + + The old behavior was added as a workaround for a bug in an early version of + Safari, where the HTTP headers are not returned correctly if the response + body has a 0-length. This is been fixed since and the workaround is no + longer necessary. + + Use `render body: ' '` if the old behavior is desired. + + See #14883 for details. + + *Godfrey Chan* + +* Prepend a JS comment to JSONP callbacks. Addresses CVE-2014-4671 + ("Rosetta Flash") + + *Greg Campbell* + +* Because URI paths may contain non US-ASCII characters we need to force + the encoding of any unescaped URIs to UTF-8 if they are US-ASCII. + This essentially replicates the functionality of the monkey patch to + URI.parser.unescape in active_support/core_ext/uri.rb. + + Fixes #16104. + + *Karl Entwistle* + +* Generate shallow paths for all children of shallow resources. + + Fixes #15783. + + *Seb Jacobs* + +* JSONP responses are now rendered with the `text/javascript` content type + when rendering through a `respond_to` block. + + Fixes #15081. + + *Lucas Mazza* + +* Add `config.action_controller.always_permitted_parameters` to configure which + parameters are permitted globally. The default value of this configuration is + `['controller', 'action']`. + + *Gary S. Weaver*, *Rafael Chacon* + * Fix env['PATH_INFO'] missing leading slash when a rack app mounted at '/'. Fixes #15511. @@ -26,6 +148,8 @@ application. Use of a symbol should be replaced with `action: symbol`. Use of a string without a "#" should be replaced with `controller: string`. + *Aaron Patterson* + * Fix URL generation with `:trailing_slash` such that it does not add a trailing slash after `.:format` @@ -164,5 +288,4 @@ *Tony Wooster* - Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/actionpack/CHANGELOG.md) for previous changes. diff --git a/actionpack/Rakefile b/actionpack/Rakefile index 7eab972595..d12213a2d8 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -40,27 +40,9 @@ task :release => :package do end task :lines do - lines, codelines, total_lines, total_codelines = 0, 0, 0, 0 - - FileList["lib/**/*.rb"].each do |file_name| - next if file_name =~ /vendor/ - File.open(file_name, 'r') do |f| - while line = f.gets - lines += 1 - next if line =~ /^\s*$/ - next if line =~ /^\s*#/ - codelines += 1 - end - end - puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}" - - total_lines += lines - total_codelines += codelines - - lines, codelines = 0, 0 - end - - puts "Total: Lines #{total_lines}, LOC #{total_codelines}" + load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics' + files = FileList["lib/**/*.rb"] + CodeTools::LineStatistics.new(files).print_loc end rule '.rb' => '.y' do |t| diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 1d6009bab8..5834e79668 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -21,8 +21,9 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version - s.add_dependency 'rack', '~> 1.5.2' + s.add_dependency 'rack', '~> 1.6.0.alpha' s.add_dependency 'rack-test', '~> 0.6.2' + s.add_dependency 'rails-deprecated_sanitizer' s.add_dependency 'actionview', version s.add_development_dependency 'activemodel', version diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index 15faabf977..4026dab2ce 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -164,6 +164,14 @@ module AbstractController _find_action_name(action_name).present? end + # Returns true if the given controller is capable of rendering + # a path. A subclass of +AbstractController::Base+ + # may return false. An Email controller for example does not + # support paths, only full URLs. + def self.supports_path? + true + end + private # Returns true if the name can be considered an action because diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index e77e4e01e9..95c67d482b 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -27,9 +27,6 @@ module AbstractController end module ClassMethods - MissingHelperError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('AbstractController::Helpers::ClassMethods::MissingHelperError', - 'AbstractController::Helpers::MissingHelperError') - # When a class is inherited, wrap its helper module in a new module. # This ensures that the parent class's module can be changed # independently of the child class's. diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb index 6684f46f64..568c47e43a 100644 --- a/actionpack/lib/abstract_controller/railties/routes_helpers.rb +++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb @@ -1,14 +1,14 @@ module AbstractController module Railties module RoutesHelpers - def self.with(routes) + def self.with(routes, include_path_helpers = true) Module.new do define_method(:inherited) do |klass| super(klass) if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) } - klass.send(:include, namespace.railtie_routes_url_helpers) + klass.send(:include, namespace.railtie_routes_url_helpers(include_path_helpers)) else - klass.send(:include, routes.url_helpers) + klass.send(:include, routes.url_helpers(include_path_helpers)) end end end diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 50bc26a80f..7f1aeafe8b 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -17,6 +17,7 @@ module ActionController autoload :ConditionalGet autoload :Cookies autoload :DataStreaming + autoload :EtagWithTemplateDigest autoload :Flash autoload :ForceSSL autoload :Head diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index e6fe6b0b00..7bbf938987 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -213,6 +213,7 @@ module ActionController Rendering, Renderers::All, ConditionalGet, + EtagWithTemplateDigest, RackDelegation, Caching, MimeResponds, diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 12d798d0c1..de85e0c1a7 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -16,7 +16,7 @@ module ActionController # All the caching stores from ActiveSupport::Cache are available to be used as backends # for Action Controller caching. # - # Configuration examples (MemoryStore is the default): + # Configuration examples (FileStore is the default): # # config.action_controller.cache_store = :memory_store # config.action_controller.cache_store = :file_store, '/path/to/cache/directory' diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index b1acca2435..89fa75f025 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -16,50 +16,51 @@ module ActionController end def process_action(event) - return unless logger.info? - - payload = event.payload - additions = ActionController::Base.log_process_action(payload) - - status = payload[:status] - if status.nil? && payload[:exception].present? - exception_class_name = payload[:exception].first - status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) + info do + payload = event.payload + additions = ActionController::Base.log_process_action(payload) + + status = payload[:status] + if status.nil? && payload[:exception].present? + exception_class_name = payload[:exception].first + status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name) + end + message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" + message << " (#{additions.join(" | ")})" unless additions.blank? + message end - message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" - message << " (#{additions.join(" | ")})" unless additions.blank? - - info(message) end def halted_callback(event) - info("Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected") + info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" } end def send_file(event) - info("Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)") + info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" } end def redirect_to(event) - info("Redirected to #{event.payload[:location]}") + info { "Redirected to #{event.payload[:location]}" } end def send_data(event) - info("Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)") + info { "Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)" } end def unpermitted_parameters(event) - unpermitted_keys = event.payload[:keys] - debug("Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}") + debug do + unpermitted_keys = event.payload[:keys] + "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}" + end end def deep_munge(event) - message = "Value for params[:#{event.payload[:keys].join('][:')}] was set "\ - "to nil, because it was one of [], [null] or [null, null, ...]. "\ - "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\ - "for more information."\ - - debug(message) + debug do + "Value for params[:#{event.payload[:keys].join('][:')}] was set "\ + "to nil, because it was one of [], [null] or [null, null, ...]. "\ + "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\ + "for more information."\ + end end %w(write_fragment read_fragment exist_fragment? diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 9a427ebfdb..bfbc15a901 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -182,7 +182,8 @@ module ActionController body = [body] unless body.nil? || body.respond_to?(:each) super end - + + # Tests if render or redirect has already happened. def performed? response_body || (response && response.committed?) end diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 6e0cd51d8b..a93727df90 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -41,6 +41,11 @@ module ActionController # * <tt>:last_modified</tt>. # * <tt>:public</tt> By default the Cache-Control header is private, set this to # +true+ if you want your application to be cachable by other devices (proxy caches). + # * <tt>:template</tt> By default, the template digest for the current + # controller/action is included in ETags. If the action renders a + # different template, you can include its digest instead. If the action + # doesn't render a template at all, you can pass <tt>template: false</tt> + # to skip any attempt to check for a template digest. # # === Example: # @@ -66,18 +71,24 @@ module ActionController # @article = Article.find(params[:id]) # fresh_when(@article, public: true) # end + # + # When rendering a different template than the default controller/action + # style, you can indicate which digest to include in the ETag: + # + # before_action { fresh_when @article, template: 'widgets/show' } + # def fresh_when(record_or_options, additional_options = {}) if record_or_options.is_a? Hash options = record_or_options - options.assert_valid_keys(:etag, :last_modified, :public) + options.assert_valid_keys(:etag, :last_modified, :public, :template) else record = record_or_options options = { etag: record, last_modified: record.try(:updated_at) }.merge!(additional_options) end - response.etag = combine_etags(options[:etag]) if options[:etag] - response.last_modified = options[:last_modified] if options[:last_modified] - response.cache_control[:public] = true if options[:public] + response.etag = combine_etags(options) if options[:etag] || options[:template] + response.last_modified = options[:last_modified] if options[:last_modified] + response.cache_control[:public] = true if options[:public] head :not_modified if request.fresh?(response) end @@ -93,6 +104,11 @@ module ActionController # * <tt>:last_modified</tt>. # * <tt>:public</tt> By default the Cache-Control header is private, set this to # +true+ if you want your application to be cachable by other devices (proxy caches). + # * <tt>:template</tt> By default, the template digest for the current + # controller/action is included in ETags. If the action renders a + # different template, you can include its digest instead. If the action + # doesn't render a template at all, you can pass <tt>template: false</tt> + # to skip any attempt to check for a template digest. # # === Example: # @@ -133,6 +149,14 @@ module ActionController # end # end # end + # + # When rendering a different template than the default controller/action + # style, you can indicate which digest to include in the ETag: + # + # def show + # super if stale? @article, template: 'widgets/show' + # end + # def stale?(record_or_options, additional_options = {}) fresh_when(record_or_options, additional_options) !request.fresh?(response) @@ -168,8 +192,9 @@ module ActionController end private - def combine_etags(etag) - [ etag, *etaggers.map { |etagger| instance_exec(&etagger) }.compact ] + def combine_etags(options) + etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact + etags.unshift options[:etag] end end end diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb new file mode 100644 index 0000000000..3ca0c6837a --- /dev/null +++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb @@ -0,0 +1,50 @@ +module ActionController + # When our views change, they should bubble up into HTTP cache freshness + # and bust browser caches. So the template digest for the current action + # is automatically included in the ETag. + # + # Enabled by default for apps that use Action View. Disable by setting + # + # config.action_controller.etag_with_template_digest = false + # + # Override the template to digest by passing `:template` to `fresh_when` + # and `stale?` calls. For example: + # + # # We're going to render widgets/show, not posts/show + # fresh_when @post, template: 'widgets/show' + # + # # We're not going to render a template, so omit it from the ETag. + # fresh_when @post, template: false + # + module EtagWithTemplateDigest + extend ActiveSupport::Concern + + include ActionController::ConditionalGet + + included do + class_attribute :etag_with_template_digest + self.etag_with_template_digest = true + + ActiveSupport.on_load :action_view, yield: true do |action_view_base| + etag do |options| + determine_template_etag(options) if etag_with_template_digest + end + end + end + + private + def determine_template_etag(options) + if template = pick_template_for_etag(options) + lookup_and_digest_template(template) + end + end + + def pick_template_for_etag(options) + options.fetch(:template) { "#{controller_name}/#{action_name}" } + end + + def lookup_and_digest_template(template) + ActionView::Digestor.digest name: template, finder: lookup_context + end + end +end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index a2cb6d1e66..d920668184 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -85,7 +85,7 @@ module ActionController if host_or_options.is_a?(Hash) options.merge!(host_or_options) elsif host_or_options - options.merge!(:host => host_or_options) + options[:host] = host_or_options end secure_url = ActionDispatch::Http::URL.url_for(options.slice(*URL_OPTIONS)) diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index 84a9112144..3d2badf9c2 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -14,6 +14,8 @@ module ActionController # return head(:method_not_allowed) unless request.post? # return head(:bad_request) unless valid_request? # render + # + # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols. def head(status, options = {}) options, status = status, nil if status.is_a?(Hash) status ||= options.delete(:status) || :ok diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 5b52c19802..25c123edf7 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -471,7 +471,7 @@ module ActionController # pairs by the standardized `:`, `;`, or `\t` delimiters defined in # `AUTHN_PAIR_DELIMITERS`. def raw_params(auth) - auth.sub(TOKEN_REGEX, '').split(/"\s*#{AUTHN_PAIR_DELIMITERS}\s*/) + auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/) end # Encodes the given token and options into an Authorization header value. diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb index 706ce04062..c9ef3a3dad 100644 --- a/actionpack/lib/action_controller/metal/live.rb +++ b/actionpack/lib/action_controller/metal/live.rb @@ -303,10 +303,12 @@ module ActionController logger = ActionController::Base.logger return unless logger - message = "\n#{exception.class} (#{exception.message}):\n" - message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) - message << " " << exception.backtrace.join("\n ") - logger.fatal("#{message}\n\n") + logger.fatal do + message = "\n#{exception.class} (#{exception.message}):\n" + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) + message << " " << exception.backtrace.join("\n ") + "#{message}\n\n" + end end def response_body=(body) diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 00e7e980f8..dc572f13d2 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -5,56 +5,22 @@ module ActionController #:nodoc: module MimeResponds extend ActiveSupport::Concern - included do - class_attribute :responder, :mimes_for_respond_to - self.responder = ActionController::Responder - clear_respond_to - end - module ClassMethods - # Defines mime types that are rendered by default when invoking - # <tt>respond_with</tt>. - # - # respond_to :html, :xml, :json - # - # Specifies that all actions in the controller respond to requests - # for <tt>:html</tt>, <tt>:xml</tt> and <tt>:json</tt>. - # - # To specify on per-action basis, use <tt>:only</tt> and - # <tt>:except</tt> with an array of actions or a single action: - # - # respond_to :html - # respond_to :xml, :json, except: [ :edit ] - # - # This specifies that all actions respond to <tt>:html</tt> - # and all actions except <tt>:edit</tt> respond to <tt>:xml</tt> and - # <tt>:json</tt>. - # - # respond_to :json, only: :create - # - # This specifies that the <tt>:create</tt> action and no other responds - # to <tt>:json</tt>. - def respond_to(*mimes) - options = mimes.extract_options! - - only_actions = Array(options.delete(:only)).map(&:to_s) - except_actions = Array(options.delete(:except)).map(&:to_s) - - new = mimes_for_respond_to.dup - mimes.each do |mime| - mime = mime.to_sym - new[mime] = {} - new[mime][:only] = only_actions unless only_actions.empty? - new[mime][:except] = except_actions unless except_actions.empty? - end - self.mimes_for_respond_to = new.freeze + def respond_to(*) + raise NoMethodError, "The controller-level `respond_to' feature has " \ + "been extracted to the `responders` gem. Add it to your Gemfile to " \ + "continue using this feature:\n" \ + " gem 'responders', '~> 2.0'\n" \ + "Consult the Rails upgrade guide for details." end + end - # Clear all mime types in <tt>respond_to</tt>. - # - def clear_respond_to - self.mimes_for_respond_to = Hash.new.freeze - end + def respond_with(*) + raise NoMethodError, "The `respond_with' feature has been extracted " \ + "to the `responders` gem. Add it to your Gemfile to continue using " \ + "this feature:\n" \ + " gem 'responders', '~> 2.0'\n" \ + "Consult the Rails upgrade guide for details." end # Without web-service support, an action which collects the data for displaying a list of people @@ -217,7 +183,7 @@ module ActionController #:nodoc: # format.html.phone { redirect_to progress_path } # format.html.none { render "trash" } # end - # + # # Variants also support common `any`/`all` block that formats have. # # It works for both inline: @@ -253,189 +219,13 @@ module ActionController #:nodoc: def respond_to(*mimes, &block) raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? - if collector = retrieve_collector_from_mimes(mimes, &block) - response = collector.response - response ? response.call : render({}) - end - end - - # For a given controller action, respond_with generates an appropriate - # response based on the mime-type requested by the client. - # - # If the method is called with just a resource, as in this example - - # - # class PeopleController < ApplicationController - # respond_to :html, :xml, :json - # - # def index - # @people = Person.all - # respond_with @people - # end - # end - # - # then the mime-type of the response is typically selected based on the - # request's Accept header and the set of available formats declared - # by previous calls to the controller's class method +respond_to+. Alternatively - # the mime-type can be selected by explicitly setting <tt>request.format</tt> in - # the controller. - # - # If an acceptable format is not identified, the application returns a - # '406 - not acceptable' status. Otherwise, the default response is to render - # a template named after the current action and the selected format, - # e.g. <tt>index.html.erb</tt>. If no template is available, the behavior - # depends on the selected format: - # - # * for an html response - if the request method is +get+, an exception - # is raised but for other requests such as +post+ the response - # depends on whether the resource has any validation errors (i.e. - # assuming that an attempt has been made to save the resource, - # e.g. by a +create+ action) - - # 1. If there are no errors, i.e. the resource - # was saved successfully, the response +redirect+'s to the resource - # i.e. its +show+ action. - # 2. If there are validation errors, the response - # renders a default action, which is <tt>:new</tt> for a - # +post+ request or <tt>:edit</tt> for +patch+ or +put+. - # Thus an example like this - - # - # respond_to :html, :xml - # - # def create - # @user = User.new(params[:user]) - # flash[:notice] = 'User was successfully created.' if @user.save - # respond_with(@user) - # end - # - # is equivalent, in the absence of <tt>create.html.erb</tt>, to - - # - # def create - # @user = User.new(params[:user]) - # respond_to do |format| - # if @user.save - # flash[:notice] = 'User was successfully created.' - # format.html { redirect_to(@user) } - # format.xml { render xml: @user } - # else - # format.html { render action: "new" } - # format.xml { render xml: @user } - # end - # end - # end - # - # * for a JavaScript request - if the template isn't found, an exception is - # raised. - # * for other requests - i.e. data formats such as xml, json, csv etc, if - # the resource passed to +respond_with+ responds to <code>to_<format></code>, - # the method attempts to render the resource in the requested format - # directly, e.g. for an xml request, the response is equivalent to calling - # <code>render xml: resource</code>. - # - # === Nested resources - # - # As outlined above, the +resources+ argument passed to +respond_with+ - # can play two roles. It can be used to generate the redirect url - # for successful html requests (e.g. for +create+ actions when - # no template exists), while for formats other than html and JavaScript - # it is the object that gets rendered, by being converted directly to the - # required format (again assuming no template exists). - # - # For redirecting successful html requests, +respond_with+ also supports - # the use of nested resources, which are supplied in the same way as - # in <code>form_for</code> and <code>polymorphic_url</code>. For example - - # - # def create - # @project = Project.find(params[:project_id]) - # @task = @project.comments.build(params[:task]) - # flash[:notice] = 'Task was successfully created.' if @task.save - # respond_with(@project, @task) - # end - # - # This would cause +respond_with+ to redirect to <code>project_task_url</code> - # instead of <code>task_url</code>. For request formats other than html or - # JavaScript, if multiple resources are passed in this way, it is the last - # one specified that is rendered. - # - # === Customizing response behavior - # - # Like +respond_to+, +respond_with+ may also be called with a block that - # can be used to overwrite any of the default responses, e.g. - - # - # def create - # @user = User.new(params[:user]) - # flash[:notice] = "User was successfully created." if @user.save - # - # respond_with(@user) do |format| - # format.html { render } - # end - # end - # - # The argument passed to the block is an ActionController::MimeResponds::Collector - # object which stores the responses for the formats defined within the - # block. Note that formats with responses defined explicitly in this way - # do not have to first be declared using the class method +respond_to+. - # - # Also, a hash passed to +respond_with+ immediately after the specified - # resource(s) is interpreted as a set of options relevant to all - # formats. Any option accepted by +render+ can be used, e.g. - # respond_with @people, status: 200 - # However, note that these options are ignored after an unsuccessful attempt - # to save a resource, e.g. when automatically rendering <tt>:new</tt> - # after a post request. - # - # Two additional options are relevant specifically to +respond_with+ - - # 1. <tt>:location</tt> - overwrites the default redirect location used after - # a successful html +post+ request. - # 2. <tt>:action</tt> - overwrites the default render action used after an - # unsuccessful html +post+ request. - def respond_with(*resources, &block) - if self.class.mimes_for_respond_to.empty? - raise "In order to use respond_with, first you need to declare the " \ - "formats your controller responds to in the class level." - end - - if collector = retrieve_collector_from_mimes(&block) - options = resources.size == 1 ? {} : resources.extract_options! - options = options.clone - options[:default_response] = collector.response - (options.delete(:responder) || self.class.responder).call(self, resources, options) - end - end - - protected - - # Collect mimes declared in the class method respond_to valid for the - # current action. - def collect_mimes_from_class_level #:nodoc: - action = action_name.to_s - - self.class.mimes_for_respond_to.keys.select do |mime| - config = self.class.mimes_for_respond_to[mime] - - if config[:except] - !config[:except].include?(action) - elsif config[:only] - config[:only].include?(action) - else - true - end - end - end - - # Returns a Collector object containing the appropriate mime-type response - # for the current request, based on the available responses defined by a block. - # In typical usage this is the block passed to +respond_with+ or +respond_to+. - # - # Sends :not_acceptable to the client and returns nil if no suitable format - # is available. - def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc: - mimes ||= collect_mimes_from_class_level collector = Collector.new(mimes, request.variant) block.call(collector) if block_given? - format = collector.negotiate_format(request) - if format + if format = collector.negotiate_format(request) _process_format(format) - collector + response = collector.response + response ? response.call : render({}) else raise ActionController::UnknownFormat end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 3feb737277..acaa8227c9 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -68,14 +68,15 @@ module ActionController # <tt>ActionController::RedirectBackError</tt>. def redirect_to(options = {}, response_status = {}) #:doc: raise ActionControllerError.new("Cannot redirect to nil!") unless options + raise ActionControllerError.new("Cannot redirect to a parameter hash!") if options.is_a?(ActionController::Parameters) raise AbstractController::DoubleRenderError if response_body self.status = _extract_redirect_to_status(options, response_status) - self.location = _compute_redirect_to_location(options) + self.location = _compute_redirect_to_location(request, options) self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>" end - def _compute_redirect_to_location(options) #:nodoc: + def _compute_redirect_to_location(request, options) #:nodoc: case options # The scheme name consist of a letter followed by any combination of # letters, digits, and the plus ("+"), period ("."), or hyphen ("-") @@ -89,11 +90,13 @@ module ActionController when :back request.headers["Referer"] or raise RedirectBackError when Proc - _compute_redirect_to_location options.call + _compute_redirect_to_location request, options.call else url_for(options) end.delete("\0\r\n") end + module_function :_compute_redirect_to_location + public :_compute_redirect_to_location private def _extract_redirect_to_status(options, response_status) diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 46405cef55..02c4e563f5 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -112,8 +112,11 @@ module ActionController json = json.to_json(options) unless json.kind_of?(String) if options[:callback].present? - self.content_type ||= Mime::JS - "#{options[:callback]}(#{json})" + if self.content_type.nil? || self.content_type == Mime::JSON + self.content_type = Mime::JS + end + + "/**/#{options[:callback]}(#{json})" else self.content_type ||= Mime::JSON json diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 93e7d6954c..7bbff0450a 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -67,8 +67,8 @@ module ActionController options[:html] = ERB::Util.html_escape(options[:html]) end - if options.delete(:nothing) || _any_render_format_is_nil?(options) - options[:body] = " " + if options.delete(:nothing) + options[:body] = nil end if options[:status] @@ -86,10 +86,6 @@ module ActionController end end - def _any_render_format_is_nil?(options) - RENDER_FORMATS_IN_PRIORITY.any? { |format| options.key?(format) && options[format].nil? } - end - # Process controller specific options, as status, content-type and location. def _process_options(options) #:nodoc: status, content_type, location = options.values_at(:status, :content_type, :location) diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 1355fe87d0..0efa0fb259 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -77,7 +77,7 @@ module ActionController #:nodoc: end module ClassMethods - # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked. + # Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked. # # class ApplicationController < ActionController::Base # protect_from_forgery diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb deleted file mode 100644 index 5096558c67..0000000000 --- a/actionpack/lib/action_controller/metal/responder.rb +++ /dev/null @@ -1,297 +0,0 @@ -require 'active_support/json' - -module ActionController #:nodoc: - # Responsible for exposing a resource to different mime requests, - # usually depending on the HTTP verb. The responder is triggered when - # <code>respond_with</code> is called. The simplest case to study is a GET request: - # - # class PeopleController < ApplicationController - # respond_to :html, :xml, :json - # - # def index - # @people = Person.all - # respond_with(@people) - # end - # end - # - # When a request comes in, for example for an XML response, three steps happen: - # - # 1) the responder searches for a template at people/index.xml; - # - # 2) if the template is not available, it will invoke <code>#to_xml</code> on the given resource; - # - # 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it. - # - # === Built-in HTTP verb semantics - # - # The default \Rails responder holds semantics for each HTTP verb. Depending on the - # content type, verb and the resource status, it will behave differently. - # - # Using \Rails default responder, a POST request for creating an object could - # be written as: - # - # def create - # @user = User.new(params[:user]) - # flash[:notice] = 'User was successfully created.' if @user.save - # respond_with(@user) - # end - # - # Which is exactly the same as: - # - # def create - # @user = User.new(params[:user]) - # - # respond_to do |format| - # if @user.save - # flash[:notice] = 'User was successfully created.' - # format.html { redirect_to(@user) } - # format.xml { render xml: @user, status: :created, location: @user } - # else - # format.html { render action: "new" } - # format.xml { render xml: @user.errors, status: :unprocessable_entity } - # end - # end - # end - # - # The same happens for PATCH/PUT and DELETE requests. - # - # === Nested resources - # - # You can supply nested resources as you do in <code>form_for</code> and <code>polymorphic_url</code>. - # Consider the project has many tasks example. The create action for - # TasksController would be like: - # - # def create - # @project = Project.find(params[:project_id]) - # @task = @project.tasks.build(params[:task]) - # flash[:notice] = 'Task was successfully created.' if @task.save - # respond_with(@project, @task) - # end - # - # Giving several resources ensures that the responder will redirect to - # <code>project_task_url</code> instead of <code>task_url</code>. - # - # Namespaced and singleton resources require a symbol to be given, as in - # polymorphic urls. If a project has one manager which has many tasks, it - # should be invoked as: - # - # respond_with(@project, :manager, @task) - # - # Note that if you give an array, it will be treated as a collection, - # so the following is not equivalent: - # - # respond_with [@project, :manager, @task] - # - # === Custom options - # - # <code>respond_with</code> also allows you to pass options that are forwarded - # to the underlying render call. Those options are only applied for success - # scenarios. For instance, you can do the following in the create method above: - # - # def create - # @project = Project.find(params[:project_id]) - # @task = @project.tasks.build(params[:task]) - # flash[:notice] = 'Task was successfully created.' if @task.save - # respond_with(@project, @task, status: 201) - # end - # - # This will return status 201 if the task was saved successfully. If not, - # it will simply ignore the given options and return status 422 and the - # resource errors. You can also override the location to redirect to: - # - # respond_with(@project, location: root_path) - # - # To customize the failure scenario, you can pass a block to - # <code>respond_with</code>: - # - # def create - # @project = Project.find(params[:project_id]) - # @task = @project.tasks.build(params[:task]) - # respond_with(@project, @task, status: 201) do |format| - # if @task.save - # flash[:notice] = 'Task was successfully created.' - # else - # format.html { render "some_special_template" } - # end - # end - # end - # - # Using <code>respond_with</code> with a block follows the same syntax as <code>respond_to</code>. - class Responder - attr_reader :controller, :request, :format, :resource, :resources, :options - - DEFAULT_ACTIONS_FOR_VERBS = { - :post => :new, - :patch => :edit, - :put => :edit - } - - def initialize(controller, resources, options={}) - @controller = controller - @request = @controller.request - @format = @controller.formats.first - @resource = resources.last - @resources = resources - @options = options - @action = options.delete(:action) - @default_response = options.delete(:default_response) - end - - delegate :head, :render, :redirect_to, :to => :controller - delegate :get?, :post?, :patch?, :put?, :delete?, :to => :request - - # Undefine :to_json and :to_yaml since it's defined on Object - undef_method(:to_json) if method_defined?(:to_json) - undef_method(:to_yaml) if method_defined?(:to_yaml) - - # Initializes a new responder and invokes the proper format. If the format is - # not defined, call to_format. - # - def self.call(*args) - new(*args).respond - end - - # Main entry point for responder responsible to dispatch to the proper format. - # - def respond - method = "to_#{format}" - respond_to?(method) ? send(method) : to_format - end - - # HTML format does not render the resource, it always attempt to render a - # template. - # - def to_html - default_render - rescue ActionView::MissingTemplate => e - navigation_behavior(e) - end - - # to_js simply tries to render a template. If no template is found, raises the error. - def to_js - default_render - end - - # All other formats follow the procedure below. First we try to render a - # template, if the template is not available, we verify if the resource - # responds to :to_format and display it. - # - def to_format - if get? || !has_errors? || response_overridden? - default_render - else - display_errors - end - rescue ActionView::MissingTemplate => e - api_behavior(e) - end - - protected - - # This is the common behavior for formats associated with browsing, like :html, :iphone and so forth. - def navigation_behavior(error) - if get? - raise error - elsif has_errors? && default_action - render :action => default_action - else - redirect_to navigation_location - end - end - - # This is the common behavior for formats associated with APIs, such as :xml and :json. - def api_behavior(error) - raise error unless resourceful? - raise MissingRenderer.new(format) unless has_renderer? - - if get? - display resource - elsif post? - display resource, :status => :created, :location => api_location - else - head :no_content - end - end - - # Checks whether the resource responds to the current format or not. - # - def resourceful? - resource.respond_to?("to_#{format}") - end - - # Returns the resource location by retrieving it from the options or - # returning the resources array. - # - def resource_location - options[:location] || resources - end - alias :navigation_location :resource_location - alias :api_location :resource_location - - # If a response block was given, use it, otherwise call render on - # controller. - # - def default_render - if @default_response - @default_response.call(options) - else - controller.default_render(options) - end - end - - # Display is just a shortcut to render a resource with the current format. - # - # display @user, status: :ok - # - # For XML requests it's equivalent to: - # - # render xml: @user, status: :ok - # - # Options sent by the user are also used: - # - # respond_with(@user, status: :created) - # display(@user, status: :ok) - # - # Results in: - # - # render xml: @user, status: :created - # - def display(resource, given_options={}) - controller.render given_options.merge!(options).merge!(format => resource) - end - - def display_errors - controller.render format => resource_errors, :status => :unprocessable_entity - end - - # Check whether the resource has errors. - # - def has_errors? - resource.respond_to?(:errors) && !resource.errors.empty? - end - - # Check whether the necessary Renderer is available - def has_renderer? - Renderers::RENDERERS.include?(format) - end - - # By default, render the <code>:edit</code> action for HTML requests with errors, unless - # the verb was POST. - # - def default_action - @action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol] - end - - def resource_errors - respond_to?("#{format}_resource_errors", true) ? send("#{format}_resource_errors") : resource.errors - end - - def json_resource_errors - {:errors => resource.errors} - end - - def response_overridden? - @default_response.present? - end - end -end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index b70962cf44..bc27ecaa20 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/array/wrap' +require 'active_support/deprecation' require 'active_support/rescuable' require 'action_dispatch/http/upload' require 'stringio' @@ -39,7 +40,7 @@ module ActionController # == Action Controller \Parameters # # Allows to choose which attributes should be whitelisted for mass updating - # and thus prevent accidentally exposing that which shouldn’t be exposed. + # and thus prevent accidentally exposing that which shouldn't be exposed. # Provides two methods for this purpose: #require and #permit. The former is # used to mark parameters as required. The latter is used to set the parameter # as permitted and limit which attributes should be allowed for mass updating. @@ -101,9 +102,23 @@ module ActionController cattr_accessor :permit_all_parameters, instance_accessor: false cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false - # Never raise an UnpermittedParameters exception because of these params - # are present. They are added by Rails and it's of no concern. - NEVER_UNPERMITTED_PARAMS = %w( controller action ) + # By default, never raise an UnpermittedParameters exception if these + # params are present. The default includes both 'controller' and 'action' + # because they are added by Rails and should be of no concern. One way + # to change these is to specify `always_permitted_parameters` in your + # config. For instance: + # + # config.always_permitted_parameters = %w( controller action format ) + cattr_accessor :always_permitted_parameters + self.always_permitted_parameters = %w( controller action ) + + def self.const_missing(const_name) + super unless const_name == :NEVER_UNPERMITTED_PARAMS + ActiveSupport::Deprecation.warn "`ActionController::Parameters::NEVER_UNPERMITTED_PARAMS`"\ + " has been deprecated. Use "\ + "`ActionController::Parameters.always_permitted_parameters` instead." + self.always_permitted_parameters + end # Returns a new instance of <tt>ActionController::Parameters</tt>. # Also, sets the +permitted+ attribute to the default value of @@ -389,7 +404,7 @@ module ActionController end def unpermitted_keys(params) - self.keys - params.keys - NEVER_UNPERMITTED_PARAMS + self.keys - params.keys - self.always_permitted_parameters end # diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 07265be3fe..0f2fa5fb08 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -28,20 +28,19 @@ module ActionController :port => request.optional_port, :protocol => request.protocol, :_recall => request.path_parameters - }.merge(super).freeze + }.merge!(super).freeze if (same_origin = _routes.equal?(env["action_dispatch.routes".freeze])) || (script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) || (original_script_name = env['ORIGINAL_SCRIPT_NAME'.freeze]) - @_url_options.dup.tap do |options| - if original_script_name - options[:original_script_name] = original_script_name - else - options[:script_name] = same_origin ? request.script_name.dup : script_name - end - options.freeze + options = @_url_options.dup + if original_script_name + options[:original_script_name] = original_script_name + else + options[:script_name] = same_origin ? request.script_name.dup : script_name end + options.freeze else @_url_options end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index a2fc814221..28b20052b5 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -23,6 +23,10 @@ module ActionController options = app.config.action_controller ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false } + if app.config.action_controller[:always_permitted_parameters] + ActionController::Parameters.always_permitted_parameters = + app.config.action_controller.delete(:always_permitted_parameters) + end ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do (Rails.env.test? || Rails.env.development?) ? :log : false end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index b117170514..8c10c3e7b0 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -3,6 +3,8 @@ require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/module/anonymous' require 'active_support/core_ext/hash/keys' +require 'rails-dom-testing' + module ActionController module TemplateAssertions extend ActiveSupport::Concern @@ -12,11 +14,13 @@ module ActionController teardown :teardown_subscriptions end + RENDER_TEMPLATE_INSTANCE_VARIABLES = %w{partials templates layouts files}.freeze + def setup_subscriptions - @_partials = Hash.new(0) - @_templates = Hash.new(0) - @_layouts = Hash.new(0) - @_files = Hash.new(0) + RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| + instance_variable_set("@_#{instance_variable}", Hash.new(0)) + end + @_subscribers = [] @_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| @@ -30,25 +34,21 @@ module ActionController end @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| - path = payload[:virtual_path] - next unless path - partial = path =~ /^.*\/_[^\/]*$/ + if virtual_path = payload[:virtual_path] + partial = virtual_path =~ /^.*\/_[^\/]*$/ - if partial - @_partials[path] += 1 - @_partials[path.split("/").last] += 1 - end - - @_templates[path] += 1 - end - - @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| - next if payload[:virtual_path] # files don't have virtual path + if partial + @_partials[virtual_path] += 1 + @_partials[virtual_path.split("/").last] += 1 + end - path = payload[:identifier] - if path - @_files[path] += 1 - @_files[path.split("/").last] += 1 + @_templates[virtual_path] += 1 + else + path = payload[:identifier] + if path + @_files[path] += 1 + @_files[path.split("/").last] += 1 + end end end end @@ -60,12 +60,16 @@ module ActionController end def process(*args) - @_partials = Hash.new(0) - @_templates = Hash.new(0) - @_layouts = Hash.new(0) + reset_template_assertion super end + def reset_template_assertion + RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| + instance_variable_get("@_#{instance_variable}").clear + end + end + # Asserts that the request was rendered with the appropriate template file or partials. # # # assert that the "new" view template was rendered @@ -89,6 +93,13 @@ module ActionController # # assert that no partials were rendered # assert_template partial: false # + # # assert that a file was rendered + # assert_template file: "README.rdoc" + # + # # assert that no file was rendered + # assert_template file: nil + # assert_template file: false + # # In a view test case, you can also assert that specific locals are passed # to partials: # @@ -138,6 +149,8 @@ module ActionController if options[:file] assert_includes @_files.keys, options[:file] + elsif options.key?(:file) + assert @_files.blank?, "expected no files but #{@_files.keys} was rendered" end if expected_partial = options[:partial] @@ -233,7 +246,6 @@ module ActionController @formats = nil @env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } - @symbolized_path_params = nil @method = @request_method = nil @fullpath = @ip = @remote_ip = @protocol = nil @env['action_dispatch.request.query_parameters'] = {} @@ -432,6 +444,7 @@ module ActionController extend ActiveSupport::Concern include ActionDispatch::TestProcess include ActiveSupport::Testing::ConstantLookup + include Rails::Dom::Testing::Assertions attr_reader :response, :request @@ -455,7 +468,6 @@ module ActionController end def controller_class=(new_class) - prepare_controller_class(new_class) if new_class self._controller_class = new_class end @@ -472,11 +484,6 @@ module ActionController Class === constant && constant < ActionController::Metal end end - - def prepare_controller_class(new_class) - new_class.send :include, ActionController::TestCase::RaiseActionExceptions - end - end # Simulate a GET request with the given parameters. @@ -680,6 +687,11 @@ module ActionController end private + + def document_root_element + html_document.root + end + def check_required_ivars # Sanity check for required instance variables so we can give an # understandable error message. @@ -694,12 +706,11 @@ module ActionController unless @request.env["PATH_INFO"] options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters options.update( - :only_path => true, :action => action, :relative_url_root => nil, :_recall => @request.path_parameters) - url, query_string = @routes.url_for(options).split("?", 2) + url, query_string = @routes.path_for(options).split("?", 2) @request.env["SCRIPT_NAME"] = @controller.config.relative_url_root @request.env["PATH_INFO"] = url @@ -713,34 +724,6 @@ module ActionController end end - # When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline - # (skipping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular - # rescue_action process takes place. This means you can test your rescue_action code by setting remote_addr to something else - # than 0.0.0.0. - # - # The exception is stored in the exception accessor for further inspection. - module RaiseActionExceptions - def self.included(base) #:nodoc: - unless base.method_defined?(:exception) && base.method_defined?(:exception=) - base.class_eval do - attr_accessor :exception - protected :exception, :exception= - end - end - end - - protected - def rescue_action_without_handler(e) - self.exception = e - - if request.remote_addr == "0.0.0.0" - raise(e) - else - super(e) - end - end - end - include Behavior end end diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 0b2b60d2e4..9c8f65deac 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -54,8 +54,14 @@ module ActionDispatch end def formats - @env["action_dispatch.request.formats"] ||= - if parameters[:format] + @env["action_dispatch.request.formats"] ||= begin + params_readable = begin + parameters[:format] + rescue ActionController::BadRequest + false + end + + if params_readable Array(Mime[parameters[:format]]) elsif use_accept_header && valid_accept_header accepts @@ -64,8 +70,8 @@ module ActionDispatch else [Mime::HTML] end + end end - # Sets the \variant for template. def variant=(variant) if variant.is_a?(Symbol) diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index 5f7627cf96..20ae48d458 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/deprecation' module ActionDispatch module Http @@ -24,8 +25,10 @@ module ActionDispatch @env[PARAMETERS_KEY] = parameters end - # The same as <tt>path_parameters</tt> with explicitly symbolized keys. def symbolized_path_parameters + ActiveSupport::Deprecation.warn( + "`symbolized_path_parameters` is deprecated. Please use `path_parameters`" + ) path_parameters end @@ -33,31 +36,22 @@ module ActionDispatch # Returned hash keys are strings: # # {'action' => 'my_action', 'controller' => 'my_controller'} - # - # See <tt>symbolized_path_parameters</tt> for symbolized keys. def path_parameters @env[PARAMETERS_KEY] ||= {} end private - # Convert nested Hash to HashWithIndifferentAccess - # and UTF-8 encode both keys and values in nested Hash. + # Convert nested Hash to HashWithIndifferentAccess. # - # TODO: Validate that the characters are UTF-8. If they aren't, - # you'll get a weird error down the road, but our form handling - # should really prevent that from happening def normalize_encode_params(params) case params - when String - params.force_encoding(Encoding::UTF_8).encode! when Hash if params.has_key?(:tempfile) UploadedFile.new(params) else params.each_with_object({}) do |(key, val), new_hash| - new_key = key.is_a?(String) ? key.dup.force_encoding(Encoding::UTF_8).encode! : key - new_hash[new_key] = if val.is_a?(Array) + new_hash[key] = if val.is_a?(Array) val.map! { |el| normalize_encode_params(el) } else normalize_encode_params(val) diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 4d4b443fb4..8c035c3c6c 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -23,7 +23,7 @@ module ActionDispatch autoload :Session, 'action_dispatch/request/session' autoload :Utils, 'action_dispatch/request/utils' - LOCALHOST = Regexp.union [/^127\.0\.0\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/] + LOCALHOST = Regexp.union [/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/] ENV_METHODS = %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_TRANSLATED REMOTE_HOST @@ -209,8 +209,8 @@ module ActionDispatch end # Returns true if the "X-Requested-With" header contains "XMLHttpRequest" - # (case-insensitive). All major JavaScript libraries send this header with - # every Ajax request. + # (case-insensitive), which may need to be manually added depending on the + # choice of JavaScript libraries and frameworks. def xml_http_request? @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i end @@ -225,7 +225,7 @@ module ActionDispatch @remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s end - # Returns the unique request id, which is based off either the X-Request-Id header that can + # Returns the unique request id, which is based on either the X-Request-Id header that can # be generated by a firewall, load balancer, or web server or by the RequestId middleware # (which sets the action_dispatch.request_id environment variable). # diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 45bf751d09..540e11a4a0 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -27,7 +27,8 @@ module ActionDispatch @tempfile = hash[:tempfile] raise(ArgumentError, ':tempfile is required') unless @tempfile - @original_filename = encode_filename(hash[:filename]) + @original_filename = hash[:filename] + @original_filename &&= @original_filename.encode "UTF-8" @content_type = hash[:type] @headers = hash[:head] end @@ -66,13 +67,6 @@ module ActionDispatch def eof? @tempfile.eof? end - - private - - def encode_filename(filename) - # Encode the filename in the utf8 encoding, unless it is nil - filename.force_encoding(Encoding::UTF_8).encode! if filename - end end end end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 3997c6ee98..6b8dcaf497 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -29,36 +29,48 @@ module ActionDispatch end def url_for(options) - unless options[:host] || options[:only_path] - raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' + if options[:only_path] + path_for options + else + full_url_for options end + end - path = options[:script_name].to_s.chomp("/") - path << options[:path].to_s + def full_url_for(options) + host = options[:host] + protocol = options[:protocol] + port = options[:port] - path = add_trailing_slash(path) if options[:trailing_slash] + unless host + raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' + end - result = if options[:only_path] - path - else - build_host_url(options).concat path - end + build_host_url(host, port, protocol, options, path_for(options)) + end - if options.key? :params - params = options[:params].is_a?(Hash) ? - options[:params] : - { params: options[:params] } + def path_for(options) + path = options[:script_name].to_s.chomp("/") + path << options[:path] if options.key?(:path) - params.reject! { |_,v| v.to_param.nil? } - result << "?#{params.to_query}" unless params.empty? - end + add_trailing_slash(path) if options[:trailing_slash] + add_params(path, options[:params]) if options.key?(:params) + add_anchor(path, options[:anchor]) if options.key?(:anchor) - result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor] - result + path end private + def add_params(path, params) + params = { params: params } unless params.is_a?(Hash) + params.reject! { |_,v| v.to_param.nil? } + path << "?#{params.to_query}" unless params.empty? + end + + def add_anchor(path, anchor) + path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param.to_s)}" + end + def extract_domain_from(host, tld_length) host.split('.').last(1 + tld_length).join('.') end @@ -76,22 +88,17 @@ module ActionDispatch elsif !path.include?(".") path.sub!(/[^\/]\z|\A\z/, '\&/') end - - path end - def build_host_url(options) - protocol = options[:protocol] - host = options[:host] - port = options[:port] + def build_host_url(host, port, protocol, options, path) if match = host.match(HOST_REGEXP) - protocol ||= match[1] unless protocol == false - host = match[2] - port = match[3] unless options.key? :port + protocol ||= match[1] unless protocol == false + host = match[2] + port = match[3] unless options.key? :port end - protocol = normalize_protocol protocol - host = normalize_host(host, options) + protocol = normalize_protocol protocol + host = normalize_host(host, options) result = protocol.dup @@ -104,7 +111,7 @@ module ActionDispatch result << ":#{normalized_port}" } - result + result.concat path end def named_host?(host) diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb index 6d58323789..59b353b1b7 100644 --- a/actionpack/lib/action_dispatch/journey/formatter.rb +++ b/actionpack/lib/action_dispatch/journey/formatter.rb @@ -12,12 +12,12 @@ module ActionDispatch @cache = nil end - def generate(name, options, recall = {}, parameterize = nil) - constraints = recall.merge(options) + def generate(name, options, path_parameters, parameterize = nil) + constraints = path_parameters.merge(options) missing_keys = [] match_route(name, constraints) do |route| - parameterized_parts = extract_parameterized_parts(route, options, recall, parameterize) + parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize) # Skip this route unless a name has been provided or it is a # standard Rails route since we can't determine whether an options diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb index d129ba7e16..9012297400 100644 --- a/actionpack/lib/action_dispatch/journey/parser.rb +++ b/actionpack/lib/action_dispatch/journey/parser.rb @@ -86,7 +86,7 @@ racc_token_table = { racc_nt_base = 10 -racc_use_result_var = true +racc_use_result_var = false Racc_arg = [ racc_action_table, @@ -133,14 +133,12 @@ Racc_debug_parser = false # reduce 0 omitted -def _reduce_1(val, _values, result) - result = Cat.new(val.first, val.last) - result +def _reduce_1(val, _values) + Cat.new(val.first, val.last) end -def _reduce_2(val, _values, result) - result = val.first - result +def _reduce_2(val, _values) + val.first end # reduce 3 omitted @@ -151,24 +149,20 @@ end # reduce 6 omitted -def _reduce_7(val, _values, result) - result = Group.new(val[1]) - result +def _reduce_7(val, _values) + Group.new(val[1]) end -def _reduce_8(val, _values, result) - result = Or.new([val.first, val.last]) - result +def _reduce_8(val, _values) + Or.new([val.first, val.last]) end -def _reduce_9(val, _values, result) - result = Or.new([val.first, val.last]) - result +def _reduce_9(val, _values) + Or.new([val.first, val.last]) end -def _reduce_10(val, _values, result) - result = Star.new(Symbol.new(val.last)) - result +def _reduce_10(val, _values) + Star.new(Symbol.new(val.last)) end # reduce 11 omitted @@ -179,27 +173,23 @@ end # reduce 14 omitted -def _reduce_15(val, _values, result) - result = Slash.new('/') - result +def _reduce_15(val, _values) + Slash.new('/') end -def _reduce_16(val, _values, result) - result = Symbol.new(val.first) - result +def _reduce_16(val, _values) + Symbol.new(val.first) end -def _reduce_17(val, _values, result) - result = Literal.new(val.first) - result +def _reduce_17(val, _values) + Literal.new(val.first) end -def _reduce_18(val, _values, result) - result = Dot.new(val.first) - result +def _reduce_18(val, _values) + Dot.new(val.first) end -def _reduce_none(val, _values, result) +def _reduce_none(val, _values) val[0] end diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y index 0ead222551..d3f7c4d765 100644 --- a/actionpack/lib/action_dispatch/journey/parser.y +++ b/actionpack/lib/action_dispatch/journey/parser.y @@ -1,11 +1,11 @@ class ActionDispatch::Journey::Parser - + options no_result_var token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR rule expressions - : expression expressions { result = Cat.new(val.first, val.last) } - | expression { result = val.first } + : expression expressions { Cat.new(val.first, val.last) } + | expression { val.first } | or ; expression @@ -14,14 +14,14 @@ rule | star ; group - : LPAREN expressions RPAREN { result = Group.new(val[1]) } + : LPAREN expressions RPAREN { Group.new(val[1]) } ; or - : expression OR expression { result = Or.new([val.first, val.last]) } - | expression OR or { result = Or.new([val.first, val.last]) } + : expression OR expression { Or.new([val.first, val.last]) } + | expression OR or { Or.new([val.first, val.last]) } ; star - : STAR { result = Star.new(Symbol.new(val.last)) } + : STAR { Star.new(Symbol.new(val.last)) } ; terminal : symbol @@ -30,16 +30,16 @@ rule | dot ; slash - : SLASH { result = Slash.new('/') } + : SLASH { Slash.new('/') } ; symbol - : SYMBOL { result = Symbol.new(val.first) } + : SYMBOL { Symbol.new(val.first) } ; literal - : LITERAL { result = Literal.new(val.first) } + : LITERAL { Literal.new(val.first) } ; dot - : DOT { result = Dot.new(val.first) } + : DOT { Dot.new(val.first) } ; end diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb index ac4ecb1e65..2b0a6575d4 100644 --- a/actionpack/lib/action_dispatch/journey/router/utils.rb +++ b/actionpack/lib/action_dispatch/journey/router/utils.rb @@ -25,9 +25,10 @@ module ActionDispatch # http://tools.ietf.org/html/rfc3986 class UriEncoder # :nodoc: ENCODE = "%%%02X".freeze - ENCODING = Encoding::US_ASCII - EMPTY = "".force_encoding(ENCODING).freeze - DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(ENCODING) } + US_ASCII = Encoding::US_ASCII + UTF_8 = Encoding::UTF_8 + EMPTY = "".force_encoding(US_ASCII).freeze + DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(US_ASCII) } ALPHA = "a-zA-Z".freeze DIGIT = "0-9".freeze @@ -53,12 +54,13 @@ module ActionDispatch end def unescape_uri(uri) - uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(uri.encoding) + encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding + uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(encoding) end protected def escape(component, pattern) - component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(ENCODING) + component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII) end def percent_encode(unsafe) diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 22b16b628d..83ac62a83d 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/object/blank' require 'active_support/key_generator' require 'active_support/message_verifier' +require 'active_support/json' module ActionDispatch class Request < Rack::Request @@ -90,6 +91,7 @@ module ActionDispatch SECRET_TOKEN = "action_dispatch.secret_token".freeze SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze + COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -173,10 +175,14 @@ module ActionDispatch end end + # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream + # to the Message{Encryptor,Verifier} allows us to handle the + # (de)serialization step within the cookie jar, which gives us the + # opportunity to detect and migrate legacy cookies. module VerifyAndUpgradeLegacySignedMessage def initialize(*args) super - @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: NullSerializer) + @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def verify_and_upgrade_legacy_signed_message(name, signed_message) @@ -212,7 +218,8 @@ module ActionDispatch secret_token: env[SECRET_TOKEN], secret_key_base: env[SECRET_KEY_BASE], upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, - serializer: env[COOKIES_SERIALIZER] + serializer: env[COOKIES_SERIALIZER], + digest: env[COOKIES_DIGEST] } end @@ -289,8 +296,8 @@ module ActionDispatch end end - # Sets the cookie named +name+. The second argument may be the very cookie - # value, or a hash of options as documented above. + # Sets the cookie named +name+. The second argument may be the cookie's + # value or a hash of options as documented above. def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! @@ -385,24 +392,11 @@ module ActionDispatch class JsonSerializer def self.load(value) - JSON.parse(value, quirks_mode: true) + ActiveSupport::JSON.decode(value) end def self.dump(value) - JSON.generate(value, quirks_mode: true) - end - end - - # Passing the NullSerializer downstream to the Message{Encryptor,Verifier} - # allows us to handle the (de)serialization step within the cookie jar, - # which gives us the opportunity to detect and migrate legacy cookies. - class NullSerializer - def self.load(value) - value - end - - def self.dump(value) - value + ActiveSupport::JSON.encode(value) end end @@ -441,6 +435,10 @@ module ActionDispatch serializer end end + + def digest + @options[:digest] || 'SHA1' + end end class SignedCookieJar #:nodoc: @@ -451,7 +449,7 @@ module ActionDispatch @parent_jar = parent_jar @options = options secret = key_generator.generate_key(@options[:signed_cookie_salt]) - @verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer) + @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def [](name) @@ -468,7 +466,7 @@ module ActionDispatch options = { :value => @verifier.generate(serialize(name, options)) } end - raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE @parent_jar[name] = options end @@ -508,7 +506,7 @@ module ActionDispatch @options = options secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) - @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: NullSerializer) + @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end def [](name) @@ -526,7 +524,7 @@ module ActionDispatch options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value])) - raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE @parent_jar[name] = options end diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 0ca1a87645..274f6f2f22 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -38,9 +38,7 @@ module ActionDispatch template = ActionView::Base.new([RESCUES_TEMPLATE_PATH], request: request, exception: wrapper.exception, - application_trace: wrapper.application_trace, - framework_trace: wrapper.framework_trace, - full_trace: wrapper.full_trace, + traces: traces_from_wrapper(wrapper), routes_inspector: routes_inspector(exception), source_extract: wrapper.source_extract, line_number: wrapper.line_number, @@ -95,5 +93,36 @@ module ActionDispatch ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes) end end + + # Augment the exception traces by providing ids for all unique stack frame + def traces_from_wrapper(wrapper) + application_trace = wrapper.application_trace + framework_trace = wrapper.framework_trace + full_trace = wrapper.full_trace + + if application_trace && framework_trace + id_counter = 0 + + application_trace = application_trace.map do |trace| + prev = id_counter + id_counter += 1 + { id: prev, trace: trace } + end + + framework_trace = framework_trace.map do |trace| + prev = id_counter + id_counter += 1 + { id: prev, trace: trace } + end + + full_trace = application_trace + framework_trace + end + + { + "Application Trace" => application_trace, + "Framework Trace" => framework_trace, + "Full Trace" => full_trace + } + end end end diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 2326bb043a..b98b553c38 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -61,12 +61,15 @@ module ActionDispatch end def source_extract - if application_trace && trace = application_trace.first - file, line, _ = trace.split(":") - @file = file - @line_number = line.to_i - source_fragment(@file, @line_number) - end + exception.backtrace.map do |trace| + file, line = trace.split(":") + line_number = line.to_i + { + code: source_fragment(file, line_number), + file: file, + line_number: line_number + } + end if exception.backtrace end private @@ -110,7 +113,7 @@ module ActionDispatch def expand_backtrace @exception.backtrace.unshift( @exception.to_s.split("\n") - ).flatten! + ).flatten! end end end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 4821d2a899..e90f8b9ce6 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -10,7 +10,7 @@ module ActionDispatch end end - # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed + # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create # action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can # then expose the flash to its template. Actually, that exposure is automatically done. @@ -37,8 +37,11 @@ module ActionDispatch # flash.alert = "You must be logged in" # flash.notice = "Post successfully created" # - # This example just places a string in the flash, but you can put any object in there. And of course, you can put as - # many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed. + # This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass + # non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to + # use sanitize helper. + # + # Just remember: They'll be gone by the time the next action has been performed. # # See docs on the FlashHash class for more details about the flash. class Flash diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 5d1740d0d4..25658bac3d 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -5,7 +5,7 @@ module ActionDispatch # Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through # ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header. # - # The unique request id is either based off the X-Request-Id header in the request, which would typically be generated + # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated # by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the # header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only. # diff --git a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb index 1db6194271..625050dc4b 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb @@ -16,9 +16,9 @@ module ActionDispatch # Get a session from the cache. def get_session(env, sid) - sid ||= generate_sid - session = @cache.read(cache_key(sid)) - session ||= {} + unless sid and session = @cache.read(cache_key(sid)) + sid, session = generate_sid, {} + end [sid, session] end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index 0864e7ef2a..ed25c67ae5 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -49,7 +49,7 @@ module ActionDispatch # reasonably sure that your upgrade is otherwise complete. Additionally, # you should take care to make sure you are not relying on the ability to # decode signed cookies generated by your app in external applications or - # Javascript before upgrading. + # JavaScript before upgrading. # # Note that changing the secret key will invalidate all existing sessions! class CookieStore < Rack::Session::Abstract::ID diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 1d4f0f89a6..f0779279c1 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -42,6 +42,7 @@ module ActionDispatch wrapper = ExceptionWrapper.new(env, exception) status = wrapper.status_code env["action_dispatch.exception"] = wrapper.exception + env["action_dispatch.original_path"] = env["PATH_INFO"] env["PATH_INFO"] = "/#{status}" response = @exceptions_app.call(env) response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb index 38429cb78e..51660a619b 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb @@ -1,25 +1,29 @@ <% if @source_extract %> -<div class="source"> -<div class="info"> - Extracted source (around line <strong>#<%= @line_number %></strong>): -</div> -<div class="data"> - <table cellpadding="0" cellspacing="0" class="lines"> - <tr> - <td> - <pre class="line_numbers"> - <% @source_extract.keys.each do |line_number| %> + <% @source_extract.each_with_index do |extract_source, index| %> + <% if extract_source[:code] %> + <div class="source <%="hidden" if index != 0%>" id="frame-source-<%=index%>"> + <div class="info"> + Extracted source (around line <strong>#<%= extract_source[:line_number] %></strong>): + </div> + <div class="data"> + <table cellpadding="0" cellspacing="0" class="lines"> + <tr> + <td> + <pre class="line_numbers"> + <% extract_source[:code].keys.each do |line_number| %> <span><%= line_number -%></span> - <% end %> - </pre> - </td> + <% end %> + </pre> + </td> <td width="100%"> <pre> -<% @source_extract.each do |line, source| -%><div class="line<%= " active" if line == @line_number -%>"><%= source -%></div><% end -%> +<% extract_source[:code].each do |line, source| -%><div class="line<%= " active" if line == extract_source[:line_number] -%>"><%= source -%></div><% end -%> </pre> </td> - </tr> - </table> -</div> -</div> + </tr> + </table> + </div> + </div> + <% end %> + <% end %> <% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb index b181909bff..f62caf51d7 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb @@ -1,9 +1,4 @@ -<% - traces = { "Application Trace" => @application_trace, - "Framework Trace" => @framework_trace, - "Full Trace" => @full_trace } - names = traces.keys -%> +<% names = @traces.keys %> <p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p> @@ -16,9 +11,42 @@ <a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %> <% end %> - <% traces.each do |name, trace| %> + <% @traces.each do |name, trace| %> <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == "Application Trace") ? 'block' : 'none' %>;"> - <pre><code><%= trace.join "\n" %></code></pre> + <pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre> </div> <% end %> + + <script type="text/javascript"> + var traceFrames = document.getElementsByClassName('trace-frames'); + var selectedFrame, currentSource = document.getElementById('frame-source-0'); + + // Add click listeners for all stack frames + for (var i = 0; i < traceFrames.length; i++) { + traceFrames[i].addEventListener('click', function(e) { + e.preventDefault(); + var target = e.target; + var frame_id = target.dataset.frameId; + + if (selectedFrame) { + selectedFrame.className = selectedFrame.className.replace("selected", ""); + } + + target.className += " selected"; + selectedFrame = target; + + // Change the extracted source code + changeSourceExtract(frame_id); + }); + + function changeSourceExtract(frame_id) { + var el = document.getElementById('frame-source-' + frame_id); + if (currentSource && el) { + currentSource.className += " hidden"; + el.className = el.className.replace(" hidden", ""); + currentSource = el; + } + } + } + </script> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb index d4af5c9b06..c0b53068f7 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb @@ -1,15 +1,9 @@ -<% - traces = { "Application Trace" => @application_trace, - "Framework Trace" => @framework_trace, - "Full Trace" => @full_trace } -%> - Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %> -<% traces.each do |name, trace| %> +<% @traces.each do |name, trace| %> <% if trace.any? %> <%= name %> -<%= trace.join("\n") %> +<%= trace.map { |t| t[:trace] }.join("\n") %> <% end %> <% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb index bc5d03dc10..e0509f56f4 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb @@ -116,9 +116,15 @@ background-color: #FFCCCC; } + .hidden { + display: none; + } + a { color: #980905; } a:visited { color: #666; } + a.trace-frames { color: #666; } a:hover { color: #C52F24; } + a.trace-frames.selected { color: #C52F24 } <%= yield :style %> </style> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb index 027a0f5b3e..c1e8b6cae3 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb @@ -1,4 +1,3 @@ -<% @source_extract = @exception.source_extract(0, :html) %> <header> <h1> <%= @exception.original_exception.class.to_s %> in @@ -12,29 +11,7 @@ </p> <pre><code><%= h @exception.message %></code></pre> - <div class="source"> - <div class="info"> - <p>Extracted source (around line <strong>#<%= @exception.line_number %></strong>):</p> - </div> - <div class="data"> - <table cellpadding="0" cellspacing="0" class="lines"> - <tr> - <td> - <pre class="line_numbers"> - <% @source_extract.keys.each do |line_number| %> -<span><%= line_number -%></span> - <% end %> - </pre> - </td> -<td width="100%"> -<pre> -<% @source_extract.each do |line, source| -%><div class="line<%= " active" if line == @exception.line_number -%>"><%= source -%></div><% end -%> -</pre> -</td> - </tr> - </table> -</div> -</div> + <%= render template: "rescues/_source" %> <p><%= @exception.sub_template_message %></p> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb index 5da21d9784..77bcd26726 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb @@ -1,4 +1,3 @@ -<% @source_extract = @exception.source_extract(0, :html) %> <%= @exception.original_exception.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %> Showing <%= @exception.file_name %> where line #<%= @exception.line_number %> raised: diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index aac5546aa1..e92baa5aa7 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -13,9 +13,6 @@ module ActionDispatch module Routing class Mapper URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port] - SCOPE_OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, - :controller, :action, :path_names, :constraints, - :shallow, :blocks, :defaults, :options] class Constraints < Endpoint #:nodoc: attr_reader :app, :constraints @@ -66,7 +63,7 @@ module ActionDispatch attr_reader :requirements, :conditions, :defaults attr_reader :to, :default_controller, :default_action, :as, :anchor - def self.build(scope, path, options) + def self.build(scope, set, path, as, options) options = scope[:options].merge(options) if scope[:options] options.delete :only @@ -77,17 +74,18 @@ module ActionDispatch defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {} - new scope, path, defaults, options + new scope, set, path, defaults, as, options end - def initialize(scope, path, defaults, options) + def initialize(scope, set, path, defaults, as, options) @requirements, @conditions = {}, {} @defaults = defaults + @set = set @to = options.delete :to @default_controller = options.delete(:controller) || scope[:controller] @default_action = options.delete(:action) || scope[:action] - @as = options.delete :as + @as = as @anchor = options.delete :anchor formatted = options.delete :format @@ -249,9 +247,9 @@ module ActionDispatch Constraints.new(to, blocks, false) else if blocks.any? - Constraints.new(dispatcher, blocks, true) + Constraints.new(dispatcher(defaults), blocks, true) else - dispatcher + dispatcher(defaults) end end end @@ -348,8 +346,8 @@ module ActionDispatch parser.parse path end - def dispatcher - Routing::RouteSet::Dispatcher.new(defaults) + def dispatcher(defaults) + @set.dispatcher defaults end end @@ -576,13 +574,21 @@ module ActionDispatch raise "A rack application must be specified" unless path - options[:as] ||= app_name(app) + rails_app = rails_app? app + + if rails_app + options[:as] ||= app.railtie_name + else + # non rails apps can't have an :as + options[:as] = nil + end + target_as = name_for_action(options[:as], path) options[:via] ||= :all match(path, options.merge(:to => app, :anchor => false, :format => false)) - define_generate_prefix(app, target_as) + define_generate_prefix(app, target_as) if rails_app self end @@ -603,31 +609,24 @@ module ActionDispatch end private - def app_name(app) - return unless app.respond_to?(:routes) - - if app.respond_to?(:railtie_name) - app.railtie_name - else - class_name = app.class.is_a?(Class) ? app.name : app.class.name - ActiveSupport::Inflector.underscore(class_name).tr("/", "_") - end + def rails_app?(app) + app.is_a?(Class) && app < Rails::Railtie end def define_generate_prefix(app, name) - return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper) - - _route = @set.named_routes.routes[name.to_sym] + _route = @set.named_routes.get name _routes = @set app.routes.define_mounted_helper(name) app.routes.extend Module.new { - def mounted?; true; end + def optimize_routes_generation?; false; end define_method :find_script_name do |options| - super(options) || begin - prefix_options = options.slice(*_route.segment_keys) - # we must actually delete prefix segment keys to avoid passing them to next url_for - _route.segment_keys.each { |k| options.delete(k) } - _routes.url_helpers.send("#{name}_path", prefix_options) + if options.key? :script_name + super(options) + else + prefix_options = options.slice(*_route.segment_keys) + # we must actually delete prefix segment keys to avoid passing them to next url_for + _route.segment_keys.each { |k| options.delete(k) } + _routes.url_helpers.send("#{name}_path", prefix_options) end end } @@ -717,7 +716,7 @@ module ActionDispatch # resources :posts, module: "admin" # # If you want to route /admin/posts to +PostsController+ - # (without the Admin:: module prefix), you could use + # (without the <tt>Admin::</tt> module prefix), you could use # # scope "/admin" do # resources :posts @@ -771,7 +770,7 @@ module ActionDispatch # end def scope(*args) options = args.extract_options!.dup - recover = {} + scope = {} options[:path] = args.flatten.join('/') if args.any? options[:constraints] ||= {} @@ -791,7 +790,7 @@ module ActionDispatch block, options[:constraints] = options[:constraints], {} end - SCOPE_OPTIONS.each do |option| + @scope.options.each do |option| if option == :blocks value = block elsif option == :options @@ -801,15 +800,15 @@ module ActionDispatch end if value - recover[option] = @scope[option] - @scope[option] = send("merge_#{option}_scope", @scope[option], value) + scope[option] = send("merge_#{option}_scope", @scope[option], value) end end + @scope = @scope.new scope yield self ensure - @scope.merge!(recover) + @scope = @scope.parent end # Scopes routes to a specific controller @@ -1047,8 +1046,6 @@ module ActionDispatch VALID_ON_OPTIONS = [:new, :collection, :member] RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except, :param, :concerns] CANONICAL_ACTIONS = %w(index create new show update destroy) - RESOURCE_METHOD_SCOPES = [:collection, :member, :new] - RESOURCE_SCOPES = [:resource, :resources] class Resource #:nodoc: attr_reader :controller, :path, :options, :param @@ -1434,7 +1431,7 @@ module ActionDispatch end with_scope_level(:nested) do - if shallow? && shallow_nesting_depth > 1 + if shallow? && shallow_nesting_depth >= 1 shallow_scope(parent_resource.nested_scope, nested_options) { yield } else scope(parent_resource.nested_scope, nested_options) { yield } @@ -1522,7 +1519,7 @@ module ActionDispatch if on = options.delete(:on) send(on) { decomposed_match(path, options) } else - case @scope[:scope_level] + case @scope.scope_level when :resources nested { decomposed_match(path, options) } when :resource @@ -1545,13 +1542,13 @@ module ActionDispatch action = nil end - if !options.fetch(:as, true) - options.delete(:as) - else - options[:as] = name_for_action(options[:as], action) - end + as = if !options.fetch(:as, true) # if it's set to nil or false + options.delete(:as) + else + name_for_action(options.delete(:as), action) + end - mapping = Mapping.build(@scope, URI.parser.escape(path), options) + mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options) app, conditions, requirements, defaults, as, anchor = mapping.to_route @set.add_route(app, conditions, requirements, defaults, as, anchor) end @@ -1565,7 +1562,7 @@ module ActionDispatch raise ArgumentError, "must be called with a path and/or options" end - if @scope[:scope_level] == :resources + if @scope.resources? with_scope_level(:root) do scope(parent_resource.path) do super(options) @@ -1632,40 +1629,39 @@ module ActionDispatch end def resource_scope? #:nodoc: - RESOURCE_SCOPES.include? @scope[:scope_level] + @scope.resource_scope? end def resource_method_scope? #:nodoc: - RESOURCE_METHOD_SCOPES.include? @scope[:scope_level] + @scope.resource_method_scope? end def nested_scope? #:nodoc: - @scope[:scope_level] == :nested + @scope.nested? end def with_exclusive_scope begin - old_name_prefix, old_path = @scope[:as], @scope[:path] - @scope[:as], @scope[:path] = nil, nil + @scope = @scope.new(:as => nil, :path => nil) with_scope_level(:exclusive) do yield end ensure - @scope[:as], @scope[:path] = old_name_prefix, old_path + @scope = @scope.parent end end def with_scope_level(kind) - old, @scope[:scope_level] = @scope[:scope_level], kind + @scope = @scope.new_level(kind) yield ensure - @scope[:scope_level] = old + @scope = @scope.parent end def resource_scope(kind, resource) #:nodoc: resource.shallow = @scope[:shallow] - old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource + @scope = @scope.new(:scope_level_resource => resource) @nesting.push(resource) with_scope_level(kind) do @@ -1673,7 +1669,7 @@ module ActionDispatch end ensure @nesting.pop - @scope[:scope_level_resource] = old_resource + @scope = @scope.parent end def nested_options #:nodoc: @@ -1701,21 +1697,22 @@ module ActionDispatch @scope[:constraints][parent_resource.param] end - def canonical_action?(action, flag) #:nodoc: - flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) + def canonical_action?(action) #:nodoc: + resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end def shallow_scope(path, options = {}) #:nodoc: - old_name_prefix, old_path = @scope[:as], @scope[:path] - @scope[:as], @scope[:path] = @scope[:shallow_prefix], @scope[:shallow_path] + scope = { :as => @scope[:shallow_prefix], + :path => @scope[:shallow_path] } + @scope = @scope.new scope scope(path, options) { yield } ensure - @scope[:as], @scope[:path] = old_name_prefix, old_path + @scope = @scope.parent end def path_for_action(action, path) #:nodoc: - if canonical_action?(action, path.blank?) + if path.blank? && canonical_action?(action) @scope[:path].to_s else "#{@scope[:path]}/#{action_path(action, path)}" @@ -1730,15 +1727,17 @@ module ActionDispatch def prefix_name_for_action(as, action) #:nodoc: if as prefix = as - elsif !canonical_action?(action, @scope[:scope_level]) + elsif !canonical_action?(action) prefix = action end - prefix.to_s.tr('-', '_') if prefix + + if prefix && prefix != '/' && !prefix.empty? + Mapper.normalize_name prefix.to_s.tr('-', '_') + end end def name_for_action(as, action) #:nodoc: prefix = prefix_name_for_action(as, action) - prefix = Mapper.normalize_name(prefix) if prefix name_prefix = @scope[:as] if parent_resource @@ -1748,27 +1747,14 @@ module ActionDispatch member_name = parent_resource.member_name end - name = case @scope[:scope_level] - when :nested - [name_prefix, prefix] - when :collection - [prefix, name_prefix, collection_name] - when :new - [prefix, :new, name_prefix, member_name] - when :member - [prefix, name_prefix, member_name] - when :root - [name_prefix, collection_name, prefix] - else - [name_prefix, member_name, prefix] - end + name = @scope.action_name(name_prefix, prefix, collection_name, member_name) - if candidate = name.select(&:present?).join("_").presence + if candidate = name.compact.join("_").presence # If a name was not explicitly given, we check if it is valid # and return nil in case it isn't. Otherwise, we pass the invalid name # forward so the underlying router engine treats it and raises an exception. if as.nil? - candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i + candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate) else candidate end @@ -1893,9 +1879,83 @@ module ActionDispatch end end + class Scope # :nodoc: + OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, + :controller, :action, :path_names, :constraints, + :shallow, :blocks, :defaults, :options] + + RESOURCE_SCOPES = [:resource, :resources] + RESOURCE_METHOD_SCOPES = [:collection, :member, :new] + + attr_reader :parent, :scope_level + + def initialize(hash, parent = {}, scope_level = nil) + @hash = hash + @parent = parent + @scope_level = scope_level + end + + def nested? + scope_level == :nested + end + + def resources? + scope_level == :resources + end + + def resource_method_scope? + RESOURCE_METHOD_SCOPES.include? scope_level + end + + def action_name(name_prefix, prefix, collection_name, member_name) + case scope_level + when :nested + [name_prefix, prefix] + when :collection + [prefix, name_prefix, collection_name] + when :new + [prefix, :new, name_prefix, member_name] + when :member + [prefix, name_prefix, member_name] + when :root + [name_prefix, collection_name, prefix] + else + [name_prefix, member_name, prefix] + end + end + + def resource_scope? + RESOURCE_SCOPES.include? scope_level + end + + def options + OPTIONS + end + + def new(hash) + self.class.new hash, self, scope_level + end + + def new_level(level) + self.class.new(self, self, level) + end + + def fetch(key, &block) + @hash.fetch(key, &block) + end + + def [](key) + @hash.fetch(key) { @parent[key] } + end + + def []=(k,v) + @hash[k] = v + end + end + def initialize(set) #:nodoc: @set = set - @scope = { :path_names => @set.resources_path_names } + @scope = Scope.new({ :path_names => @set.resources_path_names }) @concerns = {} @nesting = [] end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 746e4cd245..427a5674bd 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -142,22 +142,27 @@ module ActionDispatch %w(edit new).each do |action| module_eval <<-EOT, __FILE__, __LINE__ + 1 - def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {}) - polymorphic_url( # polymorphic_url( - record_or_hash, # record_or_hash, - options.merge(:action => "#{action}")) # options.merge(:action => "edit")) - end # end - # - def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {}) - polymorphic_url( # polymorphic_url( - record_or_hash, # record_or_hash, - options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path)) - end # end + def #{action}_polymorphic_url(record_or_hash, options = {}) + polymorphic_url_for_action("#{action}", record_or_hash, options) + end + + def #{action}_polymorphic_path(record_or_hash, options = {}) + polymorphic_path_for_action("#{action}", record_or_hash, options) + end EOT end private + def polymorphic_url_for_action(action, record_or_hash, options) + polymorphic_url(record_or_hash, options.merge(:action => action)) + end + + def polymorphic_path_for_action(action, record_or_hash, options) + options = options.merge(:action => action, :routing_type => :path) + polymorphic_path(record_or_hash, options) + end + class HelperMethodBuilder # :nodoc: CACHE = { 'path' => {}, 'url' => {} } diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 69535faabd..f51bee3b31 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -86,36 +86,69 @@ module ActionDispatch # named routes. class NamedRouteCollection #:nodoc: include Enumerable - attr_reader :routes, :helpers, :module + attr_reader :routes, :url_helpers_module def initialize @routes = {} - @helpers = [] - @module = Module.new + @path_helpers = Set.new + @url_helpers = Set.new + @url_helpers_module = Module.new + @path_helpers_module = Module.new + end + + def route_defined?(name) + key = name.to_sym + @path_helpers.include?(key) || @url_helpers.include?(key) + end + + def helpers + ActiveSupport::Deprecation.warn("`named_routes.helpers` is deprecated, please use `route_defined?(route_name)` to see if a named route was defined.") + @path_helpers + @url_helpers end def helper_names - @helpers.map(&:to_s) + @path_helpers.map(&:to_s) + @url_helpers.map(&:to_s) end def clear! - @helpers.each do |helper| - @module.remove_possible_method helper + @path_helpers.each do |helper| + @path_helpers_module.send :undef_method, helper + end + + @url_helpers.each do |helper| + @url_helpers_module.send :undef_method, helper end @routes.clear - @helpers.clear + @path_helpers.clear + @url_helpers.clear end def add(name, route) - routes[name.to_sym] = route - define_named_route_methods(name, route) + key = name.to_sym + path_name = :"#{name}_path" + url_name = :"#{name}_url" + + if routes.key? key + @path_helpers_module.send :undef_method, path_name + @url_helpers_module.send :undef_method, url_name + end + routes[key] = route + define_url_helper @path_helpers_module, route, path_name, route.defaults, name, PATH + define_url_helper @url_helpers_module, route, url_name, route.defaults, name, FULL + + @path_helpers << path_name + @url_helpers << url_name end def get(name) routes[name.to_sym] end + def key?(name) + routes.key? name.to_sym + end + alias []= add alias [] get alias clear clear! @@ -133,12 +166,31 @@ module ActionDispatch routes.length end + def path_helpers_module(warn = false) + if warn + mod = @path_helpers_module + helpers = @path_helpers + Module.new do + include mod + + helpers.each do |meth| + define_method(meth) do |*args, &block| + ActiveSupport::Deprecation.warn("The method `#{meth}` cannot be used here as a full URL is required. Use `#{meth.to_s.sub(/_path$/, '_url')}` instead") + super(*args, &block) + end + end + end + else + @path_helpers_module + end + end + class UrlHelper # :nodoc: - def self.create(route, options) + def self.create(route, options, route_name, url_strategy) if optimize_helper?(route) - OptimizedUrlHelper.new(route, options) + OptimizedUrlHelper.new(route, options, route_name, url_strategy) else - new route, options + new route, options, route_name, url_strategy end end @@ -146,20 +198,22 @@ module ActionDispatch !route.glob? && route.path.requirements.empty? end + attr_reader :url_strategy, :route_name + class OptimizedUrlHelper < UrlHelper # :nodoc: attr_reader :arg_size - def initialize(route, options) + def initialize(route, options, route_name, url_strategy) super @required_parts = @route.required_parts @arg_size = @required_parts.size end - def call(t, args) - if args.size == arg_size && !args.last.is_a?(Hash) && optimize_routes_generation?(t) + def call(t, args, inner_options) + if args.size == arg_size && !inner_options && optimize_routes_generation?(t) options = t.url_options.merge @options options[:path] = optimized_helper(args) - ActionDispatch::Http::URL.url_for(options) + url_strategy.call options else super end @@ -201,21 +255,27 @@ module ActionDispatch end end - def initialize(route, options) + def initialize(route, options, route_name, url_strategy) @options = options @segment_keys = route.segment_keys.uniq @route = route + @url_strategy = url_strategy + @route_name = route_name end - def call(t, args) + def call(t, args, inner_options) controller_options = t.url_options options = controller_options.merge @options - hash = handle_positional_args(controller_options, args, options, @segment_keys) - t._routes.url_for(hash) + hash = handle_positional_args(controller_options, + inner_options || {}, + args, + options, + @segment_keys) + + t._routes.url_for(hash, route_name, url_strategy) end - def handle_positional_args(controller_options, args, result, path_params) - inner_options = args.extract_options! + def handle_positional_args(controller_options, inner_options, args, result, path_params) if args.size > 0 if args.size < path_params.size - 1 # take format into account @@ -245,27 +305,25 @@ module ActionDispatch # # foo_url(bar, baz, bang, sort_by: 'baz') # - def define_url_helper(route, name, options) - helper = UrlHelper.create(route, options.dup) - - @module.remove_possible_method name - @module.module_eval do + def define_url_helper(mod, route, name, opts, route_key, url_strategy) + helper = UrlHelper.create(route, opts, route_key, url_strategy) + mod.module_eval do define_method(name) do |*args| - helper.call self, args + options = nil + options = args.pop if args.last.is_a? Hash + helper.call self, args, options end end - - helpers << name - end - - def define_named_route_methods(name, route) - define_url_helper route, :"#{name}_path", - route.defaults.merge(:use_route => name, :only_path => true) - define_url_helper route, :"#{name}_url", - route.defaults.merge(:use_route => name, :only_path => false) end end + # :stopdoc: + # strategy for building urls to send to the client + PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) } + FULL = ->(options) { ActionDispatch::Http::URL.full_url_for(options) } + UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) } + # :startdoc: + attr_accessor :formatter, :set, :named_routes, :default_scope, :router attr_accessor :disable_clear_and_finalize, :resources_path_names attr_accessor :default_url_options, :request_class @@ -278,7 +336,7 @@ module ActionDispatch def initialize(request_class = ActionDispatch::Request) self.named_routes = NamedRouteCollection.new - self.resources_path_names = self.class.default_resources_path_names.dup + self.resources_path_names = self.class.default_resources_path_names self.default_url_options = {} self.request_class = request_class @@ -319,6 +377,7 @@ module ActionDispatch mapper.instance_exec(&block) end end + private :eval_block def finalize! return if @finalized @@ -334,6 +393,10 @@ module ActionDispatch @prepend.each { |blk| eval_block(blk) } end + def dispatcher(defaults) + Routing::RouteSet::Dispatcher.new(defaults) + end + module MountedHelpers #:nodoc: extend ActiveSupport::Concern include UrlFor @@ -364,42 +427,51 @@ module ActionDispatch RUBY end - def url_helpers - @url_helpers ||= begin - routes = self - - Module.new do - extend ActiveSupport::Concern - include UrlFor - - # Define url_for in the singleton level so one can do: - # Rails.application.routes.url_helpers.url_for(args) - @_routes = routes - class << self - delegate :url_for, :optimize_routes_generation?, :to => '@_routes' - attr_reader :_routes - def url_options; {}; end - end + def url_helpers(include_path_helpers = true) + routes = self - # Make named_routes available in the module singleton - # as well, so one can do: - # Rails.application.routes.url_helpers.posts_path - extend routes.named_routes.module + Module.new do + extend ActiveSupport::Concern + include UrlFor + + # Define url_for in the singleton level so one can do: + # Rails.application.routes.url_helpers.url_for(args) + @_routes = routes + class << self + delegate :url_for, :optimize_routes_generation?, to: '@_routes' + attr_reader :_routes + def url_options; {}; end + end - # Any class that includes this module will get all - # named routes... - include routes.named_routes.module + url_helpers = routes.named_routes.url_helpers_module - # plus a singleton class method called _routes ... - included do - singleton_class.send(:redefine_method, :_routes) { routes } - end + # Make named_routes available in the module singleton + # as well, so one can do: + # Rails.application.routes.url_helpers.posts_path + extend url_helpers - # And an instance method _routes. Note that - # UrlFor (included in this module) add extra - # conveniences for working with @_routes. - define_method(:_routes) { @_routes || routes } + # Any class that includes this module will get all + # named routes... + include url_helpers + + if include_path_helpers + path_helpers = routes.named_routes.path_helpers_module + else + path_helpers = routes.named_routes.path_helpers_module(true) end + + include path_helpers + extend path_helpers + + # plus a singleton class method called _routes ... + included do + singleton_class.send(:redefine_method, :_routes) { routes } + end + + # And an instance method _routes. Note that + # UrlFor (included in this module) add extra + # conveniences for working with @_routes. + define_method(:_routes) { @_routes || routes } end end @@ -491,8 +563,8 @@ module ActionDispatch attr_reader :options, :recall, :set, :named_route - def initialize(options, recall, set) - @named_route = options.delete(:use_route) + def initialize(named_route, options, recall, set) + @named_route = named_route @options = options.dup @recall = recall.dup @set = set @@ -608,32 +680,34 @@ module ActionDispatch end def generate_extras(options, recall={}) - path, params = generate(options, recall) + route_key = options.delete :use_route + path, params = generate(route_key, options, recall) return path, params.keys end - def generate(options, recall = {}) - Generator.new(options, recall, self).generate + def generate(route_key, options, recall = {}) + Generator.new(route_key, options, recall, self).generate end + private :generate RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length, :trailing_slash, :anchor, :params, :only_path, :script_name, :original_script_name] - def mounted? - false - end - def optimize_routes_generation? - !mounted? && default_url_options.empty? + default_url_options.empty? end def find_script_name(options) - options.delete :script_name + options.delete(:script_name) { '' } + end + + def path_for(options, route_name = nil) # :nodoc: + url_for(options, route_name, PATH) end # The +options+ argument must be a hash whose keys are *symbols*. - def url_for(options) + def url_for(options, route_name = nil, url_strategy = UNKNOWN) options = default_url_options.merge options user = password = nil @@ -648,14 +722,14 @@ module ActionDispatch original_script_name = options.delete(:original_script_name) script_name = find_script_name options - if script_name && original_script_name + if original_script_name script_name = original_script_name + script_name end path_options = options.dup RESERVED_OPTIONS.each { |ro| path_options.delete ro } - path, params = generate(path_options, recall) + path, params = generate(route_name, path_options, recall) if options.key? :params params.merge! options[:params] @@ -667,7 +741,7 @@ module ActionDispatch options[:user] = user options[:password] = password - ActionDispatch::Http::URL.url_for(options) + url_strategy.call options end def call(env) diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index e624fe3c4a..eb554ec383 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -152,7 +152,9 @@ module ActionDispatch when nil _routes.url_for(url_options.symbolize_keys) when Hash - _routes.url_for(options.symbolize_keys.reverse_merge!(url_options)) + route_name = options.delete :use_route + _routes.url_for(options.symbolize_keys.reverse_merge!(url_options), + route_name) when String options when Symbol @@ -169,8 +171,7 @@ module ActionDispatch protected def optimize_routes_generation? - return @_optimized_routes if defined?(@_optimized_routes) - @_optimized_routes = _routes.optimize_routes_generation? && default_url_options.empty? + _routes.optimize_routes_generation? && default_url_options.empty? end def _with_routes(routes) diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb index 226baf9ad0..f325c35b57 100644 --- a/actionpack/lib/action_dispatch/testing/assertions.rb +++ b/actionpack/lib/action_dispatch/testing/assertions.rb @@ -1,18 +1,22 @@ +require 'rails-dom-testing' + module ActionDispatch module Assertions - autoload :DomAssertions, 'action_dispatch/testing/assertions/dom' autoload :ResponseAssertions, 'action_dispatch/testing/assertions/response' autoload :RoutingAssertions, 'action_dispatch/testing/assertions/routing' - autoload :SelectorAssertions, 'action_dispatch/testing/assertions/selector' - autoload :TagAssertions, 'action_dispatch/testing/assertions/tag' extend ActiveSupport::Concern - include DomAssertions include ResponseAssertions include RoutingAssertions - include SelectorAssertions - include TagAssertions + include Rails::Dom::Testing::Assertions + + def html_document + @html_document ||= if @response.content_type =~ /xml$/ + Nokogiri::XML::Document.parse(@response.body) + else + Nokogiri::HTML::Document.parse(@response.body) + end + end end end - diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb index 241a39393a..fb579b52fe 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb @@ -1,27 +1,3 @@ -require 'action_view/vendor/html-scanner' +require 'active_support/deprecation' -module ActionDispatch - module Assertions - module DomAssertions - # \Test two HTML strings for equivalency (e.g., identical up to reordering of attributes) - # - # # assert that the referenced method generates the appropriate HTML string - # assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com") - def assert_dom_equal(expected, actual, message = nil) - expected_dom = HTML::Document.new(expected).root - actual_dom = HTML::Document.new(actual).root - assert_equal expected_dom, actual_dom, message - end - - # The negated form of +assert_dom_equivalent+. - # - # # assert that the referenced method does not generate the specified HTML string - # assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com") - def assert_dom_not_equal(expected, actual, message = nil) - expected_dom = HTML::Document.new(expected).root - actual_dom = HTML::Document.new(actual).root - assert_not_equal expected_dom, actual_dom, message - end - end - end -end +ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::DomAssertions has been extracted to the rails-dom-testing gem.")
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index 0adc6c84ff..13a72220b3 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -73,13 +73,8 @@ module ActionDispatch if Regexp === fragment fragment else - handle = @controller || Class.new(ActionController::Metal) do - include ActionController::Redirecting - def initialize(request) - @_request = request - end - end.new(@request) - handle._compute_redirect_to_location(fragment) + handle = @controller || ActionController::Redirecting + handle._compute_redirect_to_location(@request, fragment) end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index f1f998d932..2cf38a9c2d 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -165,7 +165,7 @@ module ActionDispatch # ROUTES TODO: These assertions should really work in an integration context def method_missing(selector, *args, &block) - if defined?(@controller) && @controller && @routes && @routes.named_routes.helpers.include?(selector) + if defined?(@controller) && @controller && @routes && @routes.named_routes.route_defined?(selector) @controller.send(selector, *args, &block) else super diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 12023e6f77..19eca60f70 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -1,430 +1,3 @@ -require 'action_view/vendor/html-scanner' -require 'active_support/core_ext/object/inclusion' +require 'active_support/deprecation' -#-- -# Copyright (c) 2006 Assaf Arkin (http://labnotes.org) -# Under MIT and/or CC By license. -#++ - -module ActionDispatch - module Assertions - NO_STRIP = %w{pre script style textarea} - - # Adds the +assert_select+ method for use in Rails functional - # test cases, which can be used to make assertions on the response HTML of a controller - # action. You can also call +assert_select+ within another +assert_select+ to - # make assertions on elements selected by the enclosing assertion. - # - # Use +css_select+ to select elements without making an assertions, either - # from the response HTML or elements selected by the enclosing assertion. - # - # In addition to HTML responses, you can make the following assertions: - # - # * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions. - # * +assert_select_email+ - Assertions on the HTML body of an e-mail. - # - # Also see HTML::Selector to learn how to use selectors. - module SelectorAssertions - # Select and return all matching elements. - # - # If called with a single argument, uses that argument as a selector - # to match all elements of the current page. Returns an empty array - # if no match is found. - # - # If called with two arguments, uses the first argument as the base - # element and the second argument as the selector. Attempts to match the - # base element and any of its children. Returns an empty array if no - # match is found. - # - # The selector may be a CSS selector expression (String), an expression - # with substitution values (Array) or an HTML::Selector object. - # - # # Selects all div tags - # divs = css_select("div") - # - # # Selects all paragraph tags and does something interesting - # pars = css_select("p") - # pars.each do |par| - # # Do something fun with paragraphs here... - # end - # - # # Selects all list items in unordered lists - # items = css_select("ul>li") - # - # # Selects all form tags and then all inputs inside the form - # forms = css_select("form") - # forms.each do |form| - # inputs = css_select(form, "input") - # ... - # end - def css_select(*args) - # See assert_select to understand what's going on here. - arg = args.shift - - if arg.is_a?(HTML::Node) - root = arg - arg = args.shift - elsif arg == nil - raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" - elsif defined?(@selected) && @selected - matches = [] - - @selected.each do |selected| - subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup)) - subset.each do |match| - matches << match unless matches.any? { |m| m.equal?(match) } - end - end - - return matches - else - root = response_from_page - end - - case arg - when String - selector = HTML::Selector.new(arg, args) - when Array - selector = HTML::Selector.new(*arg) - when HTML::Selector - selector = arg - else raise ArgumentError, "Expecting a selector as the first argument" - end - - selector.select(root) - end - - # An assertion that selects elements and makes one or more equality tests. - # - # If the first argument is an element, selects all matching elements - # starting from (and including) that element and all its children in - # depth-first order. - # - # If no element if specified, calling +assert_select+ selects from the - # response HTML unless +assert_select+ is called from within an +assert_select+ block. - # - # When called with a block +assert_select+ passes an array of selected elements - # to the block. Calling +assert_select+ from the block, with no element specified, - # runs the assertion on the complete set of elements selected by the enclosing assertion. - # Alternatively the array may be iterated through so that +assert_select+ can be called - # separately for each element. - # - # - # ==== Example - # If the response contains two ordered lists, each with four list elements then: - # assert_select "ol" do |elements| - # elements.each do |element| - # assert_select element, "li", 4 - # end - # end - # - # will pass, as will: - # assert_select "ol" do - # assert_select "li", 8 - # end - # - # The selector may be a CSS selector expression (String), an expression - # with substitution values, or an HTML::Selector object. - # - # === Equality Tests - # - # The equality test may be one of the following: - # * <tt>true</tt> - Assertion is true if at least one element selected. - # * <tt>false</tt> - Assertion is true if no element selected. - # * <tt>String/Regexp</tt> - Assertion is true if the text value of at least - # one element matches the string or regular expression. - # * <tt>Integer</tt> - Assertion is true if exactly that number of - # elements are selected. - # * <tt>Range</tt> - Assertion is true if the number of selected - # elements fit the range. - # If no equality test specified, the assertion is true if at least one - # element selected. - # - # To perform more than one equality tests, use a hash with the following keys: - # * <tt>:text</tt> - Narrow the selection to elements that have this text - # value (string or regexp). - # * <tt>:html</tt> - Narrow the selection to elements that have this HTML - # content (string or regexp). - # * <tt>:count</tt> - Assertion is true if the number of selected elements - # is equal to this value. - # * <tt>:minimum</tt> - Assertion is true if the number of selected - # elements is at least this value. - # * <tt>:maximum</tt> - Assertion is true if the number of selected - # elements is at most this value. - # - # If the method is called with a block, once all equality tests are - # evaluated the block is called with an array of all matched elements. - # - # # At least one form element - # assert_select "form" - # - # # Form element includes four input fields - # assert_select "form input", 4 - # - # # Page title is "Welcome" - # assert_select "title", "Welcome" - # - # # Page title is "Welcome" and there is only one title element - # assert_select "title", {count: 1, text: "Welcome"}, - # "Wrong title or more than one title element" - # - # # Page contains no forms - # assert_select "form", false, "This page must contain no forms" - # - # # Test the content and style - # assert_select "body div.header ul.menu" - # - # # Use substitution values - # assert_select "ol>li#?", /item-\d+/ - # - # # All input fields in the form have a name - # assert_select "form input" do - # assert_select "[name=?]", /.+/ # Not empty - # end - def assert_select(*args, &block) - # Start with optional element followed by mandatory selector. - arg = args.shift - @selected ||= nil - - if arg.is_a?(HTML::Node) - # First argument is a node (tag or text, but also HTML root), - # so we know what we're selecting from. - root = arg - arg = args.shift - elsif arg == nil - # This usually happens when passing a node/element that - # happens to be nil. - raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" - elsif @selected - root = HTML::Node.new(nil) - root.children.concat @selected - else - # Otherwise just operate on the response document. - root = response_from_page - end - - # First or second argument is the selector: string and we pass - # all remaining arguments. Array and we pass the argument. Also - # accepts selector itself. - case arg - when String - selector = HTML::Selector.new(arg, args) - when Array - selector = HTML::Selector.new(*arg) - when HTML::Selector - selector = arg - else raise ArgumentError, "Expecting a selector as the first argument" - end - - # Next argument is used for equality tests. - equals = {} - case arg = args.shift - when Hash - equals = arg - when String, Regexp - equals[:text] = arg - when Integer - equals[:count] = arg - when Range - equals[:minimum] = arg.begin - equals[:maximum] = arg.end - when FalseClass - equals[:count] = 0 - when NilClass, TrueClass - equals[:minimum] = 1 - else raise ArgumentError, "I don't understand what you're trying to match" - end - - # By default we're looking for at least one match. - if equals[:count] - equals[:minimum] = equals[:maximum] = equals[:count] - else - equals[:minimum] = 1 unless equals[:minimum] - end - - # Last argument is the message we use if the assertion fails. - message = args.shift - #- message = "No match made with selector #{selector.inspect}" unless message - if args.shift - raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type" - end - - matches = selector.select(root) - # If text/html, narrow down to those elements that match it. - content_mismatch = nil - if match_with = equals[:text] - matches.delete_if do |match| - text = "" - stack = match.children.reverse - while node = stack.pop - if node.tag? - stack.concat node.children.reverse - else - content = node.content - text << content - end - end - text.strip! unless NO_STRIP.include?(match.name) - text.sub!(/\A\n/, '') if match.name == "textarea" - unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s) - content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, text) - true - end - end - elsif match_with = equals[:html] - matches.delete_if do |match| - html = match.children.map(&:to_s).join - html.strip! unless NO_STRIP.include?(match.name) - unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s) - content_mismatch ||= sprintf("<%s> expected but was\n<%s>", match_with, html) - true - end - end - end - # Expecting foo found bar element only if found zero, not if - # found one but expecting two. - message ||= content_mismatch if matches.empty? - # Test minimum/maximum occurrence. - min, max, count = equals[:minimum], equals[:maximum], equals[:count] - - # FIXME: minitest provides messaging when we use assert_operator, - # so is this custom message really needed? - message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size}) - if count - assert_equal count, matches.size, message - else - assert_operator matches.size, :>=, min, message if min - assert_operator matches.size, :<=, max, message if max - end - - # If a block is given call that block. Set @selected to allow - # nested assert_select, which can be nested several levels deep. - if block_given? && !matches.empty? - begin - in_scope, @selected = @selected, matches - yield matches - ensure - @selected = in_scope - end - end - - # Returns all matches elements. - matches - end - - def count_description(min, max, count) #:nodoc: - pluralize = lambda {|word, quantity| word << (quantity == 1 ? '' : 's')} - - if min && max && (max != min) - "between #{min} and #{max} elements" - elsif min && max && max == min && count - "exactly #{count} #{pluralize['element', min]}" - elsif min && !(min == 1 && max == 1) - "at least #{min} #{pluralize['element', min]}" - elsif max - "at most #{max} #{pluralize['element', max]}" - end - end - - # Extracts the content of an element, treats it as encoded HTML and runs - # nested assertion on it. - # - # You typically call this method within another assertion to operate on - # all currently selected elements. You can also pass an element or array - # of elements. - # - # The content of each element is un-encoded, and wrapped in the root - # element +encoded+. It then calls the block with all un-encoded elements. - # - # # Selects all bold tags from within the title of an Atom feed's entries (perhaps to nab a section name prefix) - # assert_select "feed[xmlns='http://www.w3.org/2005/Atom']" do - # # Select each entry item and then the title item - # assert_select "entry>title" do - # # Run assertions on the encoded title elements - # assert_select_encoded do - # assert_select "b" - # end - # end - # end - # - # - # # Selects all paragraph tags from within the description of an RSS feed - # assert_select "rss[version=2.0]" do - # # Select description element of each feed item. - # assert_select "channel>item>description" do - # # Run assertions on the encoded elements. - # assert_select_encoded do - # assert_select "p" - # end - # end - # end - def assert_select_encoded(element = nil, &block) - case element - when Array - elements = element - when HTML::Node - elements = [element] - when nil - unless elements = @selected - raise ArgumentError, "First argument is optional, but must be called from a nested assert_select" - end - else - raise ArgumentError, "Argument is optional, and may be node or array of nodes" - end - - fix_content = lambda do |node| - # Gets around a bug in the Rails 1.1 HTML parser. - node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { Rack::Utils.escapeHTML($1) } - end - - selected = elements.map do |elem| - text = elem.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join - root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root - css_select(root, "encoded:root", &block)[0] - end - - begin - old_selected, @selected = @selected, selected - assert_select ":root", &block - ensure - @selected = old_selected - end - end - - # Extracts the body of an email and runs nested assertions on it. - # - # You must enable deliveries for this assertion to work, use: - # ActionMailer::Base.perform_deliveries = true - # - # assert_select_email do - # assert_select "h1", "Email alert" - # end - # - # assert_select_email do - # items = assert_select "ol>li" - # items.each do - # # Work with items here... - # end - # end - def assert_select_email(&block) - deliveries = ActionMailer::Base.deliveries - assert !deliveries.empty?, "No e-mail in delivery list" - - deliveries.each do |delivery| - (delivery.parts.empty? ? [delivery] : delivery.parts).each do |part| - if part["Content-Type"].to_s =~ /^text\/html\W/ - root = HTML::Document.new(part.body.to_s).root - assert_select root, ":root", &block - end - end - end - end - - protected - # +assert_select+ and +css_select+ call this to obtain the content in the HTML page. - def response_from_page - html_document.root - end - end - end -end +ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::SelectorAssertions has been has been extracted to the rails-dom-testing gem.")
\ No newline at end of file diff --git a/actionpack/lib/action_dispatch/testing/assertions/tag.rb b/actionpack/lib/action_dispatch/testing/assertions/tag.rb deleted file mode 100644 index e5fe30ba82..0000000000 --- a/actionpack/lib/action_dispatch/testing/assertions/tag.rb +++ /dev/null @@ -1,135 +0,0 @@ -require 'action_view/vendor/html-scanner' - -module ActionDispatch - module Assertions - # Pair of assertions to testing elements in the HTML output of the response. - module TagAssertions - # Asserts that there is a tag/node/element in the body of the response - # that meets all of the given conditions. The +conditions+ parameter must - # be a hash of any of the following keys (all are optional): - # - # * <tt>:tag</tt>: the node type must match the corresponding value - # * <tt>:attributes</tt>: a hash. The node's attributes must match the - # corresponding values in the hash. - # * <tt>:parent</tt>: a hash. The node's parent must match the - # corresponding hash. - # * <tt>:child</tt>: a hash. At least one of the node's immediate children - # must meet the criteria described by the hash. - # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must - # meet the criteria described by the hash. - # * <tt>:descendant</tt>: a hash. At least one of the node's descendants - # must meet the criteria described by the hash. - # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must - # meet the criteria described by the hash. - # * <tt>:after</tt>: a hash. The node must be after any sibling meeting - # the criteria described by the hash, and at least one sibling must match. - # * <tt>:before</tt>: a hash. The node must be before any sibling meeting - # the criteria described by the hash, and at least one sibling must match. - # * <tt>:children</tt>: a hash, for counting children of a node. Accepts - # the keys: - # * <tt>:count</tt>: either a number or a range which must equal (or - # include) the number of children that match. - # * <tt>:less_than</tt>: the number of matching children must be less - # than this number. - # * <tt>:greater_than</tt>: the number of matching children must be - # greater than this number. - # * <tt>:only</tt>: another hash consisting of the keys to use - # to match on the children, and only matching children will be - # counted. - # * <tt>:content</tt>: the textual content of the node must match the - # given value. This will not match HTML tags in the body of a - # tag--only text. - # - # Conditions are matched using the following algorithm: - # - # * if the condition is a string, it must be a substring of the value. - # * if the condition is a regexp, it must match the value. - # * if the condition is a number, the value must match number.to_s. - # * if the condition is +true+, the value must not be +nil+. - # * if the condition is +false+ or +nil+, the value must be +nil+. - # - # # Assert that there is a "span" tag - # assert_tag tag: "span" - # - # # Assert that there is a "span" tag with id="x" - # assert_tag tag: "span", attributes: { id: "x" } - # - # # Assert that there is a "span" tag using the short-hand - # assert_tag :span - # - # # Assert that there is a "span" tag with id="x" using the short-hand - # assert_tag :span, attributes: { id: "x" } - # - # # Assert that there is a "span" inside of a "div" - # assert_tag tag: "span", parent: { tag: "div" } - # - # # Assert that there is a "span" somewhere inside a table - # assert_tag tag: "span", ancestor: { tag: "table" } - # - # # Assert that there is a "span" with at least one "em" child - # assert_tag tag: "span", child: { tag: "em" } - # - # # Assert that there is a "span" containing a (possibly nested) - # # "strong" tag. - # assert_tag tag: "span", descendant: { tag: "strong" } - # - # # Assert that there is a "span" containing between 2 and 4 "em" tags - # # as immediate children - # assert_tag tag: "span", - # children: { count: 2..4, only: { tag: "em" } } - # - # # Get funky: assert that there is a "div", with an "ul" ancestor - # # and an "li" parent (with "class" = "enum"), and containing a - # # "span" descendant that contains text matching /hello world/ - # assert_tag tag: "div", - # ancestor: { tag: "ul" }, - # parent: { tag: "li", - # attributes: { class: "enum" } }, - # descendant: { tag: "span", - # child: /hello world/ } - # - # <b>Please note</b>: +assert_tag+ and +assert_no_tag+ only work - # with well-formed XHTML. They recognize a few tags as implicitly self-closing - # (like br and hr and such) but will not work correctly with tags - # that allow optional closing tags (p, li, td). <em>You must explicitly - # close all of your tags to use these assertions.</em> - def assert_tag(*opts) - opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first - tag = find_tag(opts) - assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}" - end - - # Identical to +assert_tag+, but asserts that a matching tag does _not_ - # exist. (See +assert_tag+ for a full discussion of the syntax.) - # - # # Assert that there is not a "div" containing a "p" - # assert_no_tag tag: "div", descendant: { tag: "p" } - # - # # Assert that an unordered list is empty - # assert_no_tag tag: "ul", descendant: { tag: "li" } - # - # # Assert that there is not a "p" tag with between 1 to 3 "img" tags - # # as immediate children - # assert_no_tag tag: "p", - # children: { count: 1..3, only: { tag: "img" } } - def assert_no_tag(*opts) - opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first - tag = find_tag(opts) - assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}" - end - - def find_tag(conditions) - html_document.find(conditions) - end - - def find_all_tag(conditions) - html_document.find_all(conditions) - end - - def html_document - xml = @response.content_type =~ /xml$/ - @html_document ||= HTML::Document.new(@response.body, false, xml) - end - end - end -end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 17765d851b..2d1c3ac5c7 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -3,6 +3,7 @@ require 'uri' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/object/try' require 'rack/test' +require 'minitest' module ActionDispatch module Integration #:nodoc: @@ -188,8 +189,8 @@ module ActionDispatch # This makes app.url_for and app.foo_path available in the console if app.respond_to?(:routes) singleton_class.class_eval do - include app.routes.url_helpers if app.routes.respond_to?(:url_helpers) - include app.routes.mounted_helpers if app.routes.respond_to?(:mounted_helpers) + include app.routes.url_helpers + include app.routes.mounted_helpers end end @@ -200,7 +201,7 @@ module ActionDispatch @url_options ||= default_url_options.dup.tap do |url_options| url_options.reverse_merge!(controller.url_options) if controller - if @app.respond_to?(:routes) && @app.routes.respond_to?(:default_url_options) + if @app.respond_to?(:routes) url_options.reverse_merge!(@app.routes.default_url_options) end @@ -329,6 +330,7 @@ module ActionDispatch xml_http_request xhr get_via_redirect post_via_redirect).each do |method| define_method(method) do |*args| reset! unless integration_session + reset_template_assertion # reset the html_document variable, but only for new get/post calls @html_document = nil unless method == 'cookies' || method == 'assigns' integration_session.__send__(method, *args).tap do @@ -493,5 +495,9 @@ module ActionDispatch reset! unless integration_session integration_session.url_options end + + def document_root_element + html_document.root + end end end diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index 57c678843b..de3dc5f924 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -39,7 +39,7 @@ module ActionDispatch end def action=(action_name) - path_parameters["action"] = action_name.to_s + path_parameters[:action] = action_name.to_s end def if_modified_since=(last_modified) diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index 6584d20840..674fb253f4 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -15,6 +15,12 @@ silence_warnings do Encoding.default_external = "UTF-8" end +require 'drb' +require 'drb/unix' +require 'tempfile' + +PROCESS_COUNT = (ENV['N'] || 4).to_i + require 'active_support/testing/autorun' require 'abstract_controller' require 'action_controller' @@ -105,6 +111,9 @@ end module ActiveSupport class TestCase include ActionDispatch::DrawOnce + if ActiveSupport::Testing::Isolation.forking_env? && PROCESS_COUNT > 0 + parallelize_me! + end end end @@ -305,22 +314,95 @@ end module ActionDispatch module RoutingVerbs - def get(uri_or_host, path = nil) + def send_request(uri_or_host, method, path) host = uri_or_host.host unless path path ||= uri_or_host.path params = {'PATH_INFO' => path, - 'REQUEST_METHOD' => 'GET', + 'REQUEST_METHOD' => method, 'HTTP_HOST' => host} - routes.call(params)[2].join + routes.call(params) + end + + def request_path_params(path, options = {}) + method = options[:method] || 'GET' + resp = send_request URI('http://localhost' + path), method.to_s.upcase, nil + status = resp.first + if status == 404 + raise ActionController::RoutingError, "No route matches #{path.inspect}" + end + controller.request.path_parameters + end + + def get(uri_or_host, path = nil) + send_request(uri_or_host, 'GET', path)[2].join + end + + def post(uri_or_host, path = nil) + send_request(uri_or_host, 'POST', path)[2].join + end + + def put(uri_or_host, path = nil) + send_request(uri_or_host, 'PUT', path)[2].join + end + + def delete(uri_or_host, path = nil) + send_request(uri_or_host, 'DELETE', path)[2].join + end + + def patch(uri_or_host, path = nil) + send_request(uri_or_host, 'PATCH', path)[2].join end end end module RoutingTestHelpers - def url_for(set, options, recall = {}) - set.url_for options.merge(:only_path => true, :_recall => recall) + def url_for(set, options) + route_name = options.delete :use_route + set.url_for options.merge(:only_path => true), route_name + end + + def make_set(strict = true) + tc = self + TestSet.new ->(c) { tc.controller = c }, strict + end + + class TestSet < ActionDispatch::Routing::RouteSet + attr_reader :strict + + def initialize(block, strict = false) + @block = block + @strict = strict + super() + end + + class Dispatcher < ActionDispatch::Routing::RouteSet::Dispatcher + def initialize(defaults, set, block) + super(defaults) + @block = block + @set = set + end + + def controller(params, default_controller=true) + super(params, @set.strict) + end + + def controller_reference(controller_param) + block = @block + set = @set + super if @set.strict + Class.new(ActionController::Base) { + include set.url_helpers + define_method(:process) { |name| block.call(self) } + def to_a; [200, {}, []]; end + } + end + end + + def dispatcher defaults + TestSet::Dispatcher.new defaults, self, @block + end end end @@ -359,3 +441,76 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +require 'mocha/setup' # FIXME: stop using mocha + +class ForkingExecutor + class Server + include DRb::DRbUndumped + + def initialize + @queue = Queue.new + end + + def record reporter, result + reporter.record result + end + + def << o + o[2] = DRbObject.new(o[2]) if o + @queue << o + end + def pop; @queue.pop; end + end + + def initialize size + @size = size + @queue = Server.new + file = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('tests', 'fd') + @url = "drbunix://#{file}" + @pool = nil + DRb.start_service @url, @queue + end + + def << work; @queue << work; end + + def shutdown + pool = @size.times.map { + fork { + DRb.stop_service + queue = DRbObject.new_with_uri @url + while job = queue.pop + klass = job[0] + method = job[1] + reporter = job[2] + result = Minitest.run_one_method klass, method + if result.error? + translate_exceptions result + end + queue.record reporter, result + end + } + } + @size.times { @queue << nil } + pool.each { |pid| Process.waitpid pid } + end + + private + def translate_exceptions(result) + result.failures.map! { |e| + begin + Marshal.dump e + e + rescue TypeError + ex = Exception.new e.message + ex.set_backtrace e.backtrace + Minitest::UnexpectedError.new ex + end + } + end +end + +if ActiveSupport::Testing::Isolation.forking_env? && PROCESS_COUNT > 0 + # Use N processes (N defaults to 4) + Minitest.parallel_executor = ForkingExecutor.new(PROCESS_COUNT) +end diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index b6b5a218cc..2cf48df6c0 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'action_view/vendor/html-scanner' require 'controller/fake_controllers' class ActionPackAssertionsController < ActionController::Base @@ -147,11 +146,6 @@ end class ActionPackAssertionsControllerTest < ActionController::TestCase - def test_assert_tag_and_url_for - get :render_url - assert_tag :content => "/action_pack_assertions/flash_me" - end - def test_render_file_absolute_path get :render_file_absolute_path assert_match(/\A= Action Pack/, @response.body) @@ -488,6 +482,11 @@ class AssertTemplateTest < ActionController::TestCase assert_raise(ActiveSupport::TestCase::Assertion) do assert_template :file => 'test/hello_world' end + + get :render_file_absolute_path + assert_raise(ActiveSupport::TestCase::Assertion) do + assert_template file: nil + end end def test_with_nil_passes_when_no_template_rendered @@ -612,6 +611,24 @@ class AssertTemplateTest < ActionController::TestCase get :nothing assert_template nil + + get :partial + assert_template partial: 'test/_partial' + + get :nothing + assert_template partial: nil + + get :render_with_layout + assert_template layout: 'layouts/standard' + + get :nothing + assert_template layout: nil + + get :render_file_relative_path + assert_template file: 'README.rdoc' + + get :nothing + assert_template file: nil end end diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb deleted file mode 100644 index f07d201563..0000000000 --- a/actionpack/test/controller/assert_select_test.rb +++ /dev/null @@ -1,356 +0,0 @@ -# encoding: utf-8 -#-- -# Copyright (c) 2006 Assaf Arkin (http://labnotes.org) -# Under MIT and/or CC By license. -#++ - -require 'abstract_unit' -require 'controller/fake_controllers' - -require 'action_mailer' -require 'action_view' - -ActionMailer::Base.send(:include, ActionView::Layouts) -ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH - -class AssertSelectTest < ActionController::TestCase - Assertion = ActiveSupport::TestCase::Assertion - - class AssertSelectMailer < ActionMailer::Base - def test(html) - mail :body => html, :content_type => "text/html", - :subject => "Test e-mail", :from => "test@test.host", :to => "test <test@test.host>" - end - end - - class AssertMultipartSelectMailer < ActionMailer::Base - def test(options) - mail :subject => "Test e-mail", :from => "test@test.host", :to => "test <test@test.host>" do |format| - format.text { render :text => options[:text] } - format.html { render :text => options[:html] } - end - end - end - - class AssertSelectController < ActionController::Base - def response_with=(content) - @content = content - end - - def response_with(&block) - @update = block - end - - def html() - render :text=>@content, :layout=>false, :content_type=>Mime::HTML - @content = nil - end - - def xml() - render :text=>@content, :layout=>false, :content_type=>Mime::XML - @content = nil - end - end - - tests AssertSelectController - - def setup - super - @old_delivery_method = ActionMailer::Base.delivery_method - @old_perform_deliveries = ActionMailer::Base.perform_deliveries - ActionMailer::Base.delivery_method = :test - ActionMailer::Base.perform_deliveries = true - end - - def teardown - super - ActionMailer::Base.delivery_method = @old_delivery_method - ActionMailer::Base.perform_deliveries = @old_perform_deliveries - ActionMailer::Base.deliveries.clear - end - - def assert_failure(message, &block) - e = assert_raise(Assertion, &block) - assert_match(message, e.message) if Regexp === message - assert_equal(message, e.message) if String === message - end - - # - # Test assert select. - # - - def test_assert_select - render_html %Q{<div id="1"></div><div id="2"></div>} - assert_select "div", 2 - assert_failure(/\AExpected at least 1 element matching \"p\", found 0\.$/) { assert_select "p" } - end - - def test_equality_integer - render_html %Q{<div id="1"></div><div id="2"></div>} - assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) { assert_select "div", 3 } - assert_failure(/\AExpected exactly 0 elements matching \"div\", found 2\.$/) { assert_select "div", 0 } - end - - def test_equality_true_false - render_html %Q{<div id="1"></div><div id="2"></div>} - assert_nothing_raised { assert_select "div" } - assert_raise(Assertion) { assert_select "p" } - assert_nothing_raised { assert_select "div", true } - assert_raise(Assertion) { assert_select "p", true } - assert_raise(Assertion) { assert_select "div", false } - assert_nothing_raised { assert_select "p", false } - end - - def test_equality_false_message - render_html %Q{<div id="1"></div><div id="2"></div>} - assert_failure(/\AExpected exactly 0 elements matching \"div\", found 2\.$/) { assert_select "div", false } - end - - def test_equality_string_and_regexp - render_html %Q{<div id="1">foo</div><div id="2">foo</div>} - assert_nothing_raised { assert_select "div", "foo" } - assert_raise(Assertion) { assert_select "div", "bar" } - assert_failure(/\A<bar> expected but was\n<foo>\.$/) { assert_select "div", "bar" } - assert_nothing_raised { assert_select "div", :text=>"foo" } - assert_raise(Assertion) { assert_select "div", :text=>"bar" } - assert_nothing_raised { assert_select "div", /(foo|bar)/ } - assert_raise(Assertion) { assert_select "div", /foobar/ } - assert_nothing_raised { assert_select "div", :text=>/(foo|bar)/ } - assert_raise(Assertion) { assert_select "div", :text=>/foobar/ } - assert_raise(Assertion) { assert_select "p", :text=>/foobar/ } - end - - def test_equality_of_html - render_html %Q{<p>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</p>} - text = "\"This is not a big problem,\" he said." - html = "<em>\"This is <strong>not</strong> a big problem,\"</em> he said." - assert_nothing_raised { assert_select "p", text } - assert_raise(Assertion) { assert_select "p", html } - assert_nothing_raised { assert_select "p", :html=>html } - assert_raise(Assertion) { assert_select "p", :html=>text } - assert_failure(/\A<#{text}> expected but was\n<#{html}>\.$/) { assert_select "p", :html=>text } - # No stripping for pre. - render_html %Q{<pre>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</pre>} - text = "\n\"This is not a big problem,\" he said.\n" - html = "\n<em>\"This is <strong>not</strong> a big problem,\"</em> he said.\n" - assert_nothing_raised { assert_select "pre", text } - assert_raise(Assertion) { assert_select "pre", html } - assert_nothing_raised { assert_select "pre", :html=>html } - assert_raise(Assertion) { assert_select "pre", :html=>text } - end - - def test_strip_textarea - render_html %Q{<textarea>\n\nfoo\n</textarea>} - assert_select "textarea", "\nfoo\n" - render_html %Q{<textarea>\nfoo</textarea>} - assert_select "textarea", "foo" - end - - def test_counts - render_html %Q{<div id="1">foo</div><div id="2">foo</div>} - assert_nothing_raised { assert_select "div", 2 } - assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) do - assert_select "div", 3 - end - assert_nothing_raised { assert_select "div", 1..2 } - assert_failure(/\AExpected between 3 and 4 elements matching \"div\", found 2\.$/) do - assert_select "div", 3..4 - end - assert_nothing_raised { assert_select "div", :count=>2 } - assert_failure(/\AExpected exactly 3 elements matching \"div\", found 2\.$/) do - assert_select "div", :count=>3 - end - assert_nothing_raised { assert_select "div", :minimum=>1 } - assert_nothing_raised { assert_select "div", :minimum=>2 } - assert_failure(/\AExpected at least 3 elements matching \"div\", found 2\.$/) do - assert_select "div", :minimum=>3 - end - assert_nothing_raised { assert_select "div", :maximum=>2 } - assert_nothing_raised { assert_select "div", :maximum=>3 } - assert_failure(/\AExpected at most 1 element matching \"div\", found 2\.$/) do - assert_select "div", :maximum=>1 - end - assert_nothing_raised { assert_select "div", :minimum=>1, :maximum=>2 } - assert_failure(/\AExpected between 3 and 4 elements matching \"div\", found 2\.$/) do - assert_select "div", :minimum=>3, :maximum=>4 - end - end - - def test_substitution_values - render_html %Q{<div id="1">foo</div><div id="2">foo</div>} - assert_select "div#?", /\d+/ do |elements| - assert_equal 2, elements.size - end - assert_select "div" do - assert_select "div#?", /\d+/ do |elements| - assert_equal 2, elements.size - assert_select "#1" - assert_select "#2" - end - end - end - - def test_nested_assert_select - render_html %Q{<div id="1">foo</div><div id="2">foo</div>} - assert_select "div" do |elements| - assert_equal 2, elements.size - assert_select elements[0], "#1" - assert_select elements[1], "#2" - end - assert_select "div" do - assert_select "div" do |elements| - assert_equal 2, elements.size - # Testing in a group is one thing - assert_select "#1,#2" - # Testing individually is another. - assert_select "#1" - assert_select "#2" - assert_select "#3", false - end - end - - assert_failure(/\AExpected at least 1 element matching \"#4\", found 0\.$/) do - assert_select "div" do - assert_select "#4" - end - end - end - - def test_assert_select_text_match - render_html %Q{<div id="1"><span>foo</span></div><div id="2"><span>bar</span></div>} - assert_select "div" do - assert_nothing_raised { assert_select "div", "foo" } - assert_nothing_raised { assert_select "div", "bar" } - assert_nothing_raised { assert_select "div", /\w*/ } - assert_nothing_raised { assert_select "div", :text => /\w*/, :count=>2 } - assert_raise(Assertion) { assert_select "div", :text=>"foo", :count=>2 } - assert_nothing_raised { assert_select "div", :html=>"<span>bar</span>" } - assert_nothing_raised { assert_select "div", :html=>"<span>bar</span>" } - assert_nothing_raised { assert_select "div", :html=>/\w*/ } - assert_nothing_raised { assert_select "div", :html=>/\w*/, :count=>2 } - assert_raise(Assertion) { assert_select "div", :html=>"<span>foo</span>", :count=>2 } - end - end - - def test_elect_with_xml_namespace_attributes - render_html %Q{<link xlink:href="http://nowhere.com"></link>} - assert_nothing_raised { assert_select "link[xlink:href=http://nowhere.com]" } - end - - # - # Test css_select. - # - - def test_css_select - render_html %Q{<div id="1"></div><div id="2"></div>} - assert_equal 2, css_select("div").size - assert_equal 0, css_select("p").size - end - - def test_nested_css_select - render_html %Q{<div id="1">foo</div><div id="2">foo</div>} - assert_select "div#?", /\d+/ do |elements| - assert_equal 1, css_select(elements[0], "div").size - assert_equal 1, css_select(elements[1], "div").size - end - assert_select "div" do - assert_equal 2, css_select("div").size - css_select("div").each do |element| - # Testing as a group is one thing - assert !css_select("#1,#2").empty? - # Testing individually is another - assert !css_select("#1").empty? - assert !css_select("#2").empty? - end - end - end - - def test_feed_item_encoded - render_xml <<-EOF -<rss version="2.0"> - <channel> - <item> - <description> - <![CDATA[ - <p>Test 1</p> - ]]> - </description> - </item> - <item> - <description> - <![CDATA[ - <p>Test 2</p> - ]]> - </description> - </item> - </channel> -</rss> -EOF - assert_select "channel item description" do - # Test element regardless of wrapper. - assert_select_encoded do - assert_select "p", :count=>2, :text=>/Test/ - end - # Test through encoded wrapper. - assert_select_encoded do - assert_select "encoded p", :count=>2, :text=>/Test/ - end - # Use :root instead (recommended) - assert_select_encoded do - assert_select ":root p", :count=>2, :text=>/Test/ - end - # Test individually. - assert_select "description" do |elements| - assert_select_encoded elements[0] do - assert_select "p", "Test 1" - end - assert_select_encoded elements[1] do - assert_select "p", "Test 2" - end - end - end - - # Test that we only un-encode element itself. - assert_select "channel item" do - assert_select_encoded do - assert_select "p", 0 - end - end - end - - # - # Test assert_select_email - # - - def test_assert_select_email - assert_raise(Assertion) { assert_select_email {} } - AssertSelectMailer.test("<div><p>foo</p><p>bar</p></div>").deliver - assert_select_email do - assert_select "div:root" do - assert_select "p:first-child", "foo" - assert_select "p:last-child", "bar" - end - end - end - - def test_assert_select_email_multipart - AssertMultipartSelectMailer.test(:html => "<div><p>foo</p><p>bar</p></div>", :text => 'foo bar').deliver - assert_select_email do - assert_select "div:root" do - assert_select "p:first-child", "foo" - assert_select "p:last-child", "bar" - end - end - end - - protected - def render_html(html) - @controller.response_with = html - get :html - end - - def render_xml(xml) - @controller.response_with = xml - get :xml - end -end diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb index ef90fff178..8c6c8a0aa7 100644 --- a/actionpack/test/controller/http_token_authentication_test.rb +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -139,16 +139,36 @@ class HttpTokenAuthenticationTest < ActionController::TestCase assert_equal(expected, actual) end + test "token_and_options returns correct token with nounce option" do + token = "rcHu+HzSFw89Ypyhn/896A=" + nonce_hash = {nonce: "123abc"} + actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token, nonce_hash)) + expected_token = token + expected_nonce = {"nonce" => nonce_hash[:nonce]} + assert_equal(expected_token, actual.first) + assert_equal(expected_nonce, actual.last) + end + test "token_and_options returns nil with no value after the equal sign" do actual = ActionController::HttpAuthentication::Token.token_and_options(malformed_request).first expected = nil assert_equal(expected, actual) end + test "raw_params returns a tuple of two key value pair strings" do + auth = sample_request("rcHu+HzSFw89Ypyhn/896A=").authorization.to_s + actual = ActionController::HttpAuthentication::Token.raw_params(auth) + expected = ["token=\"rcHu+HzSFw89Ypyhn/896A=\"", "nonce=\"def\""] + assert_equal(expected, actual) + end + private - def sample_request(token) - @sample_request ||= OpenStruct.new authorization: %{Token token="#{token}", nonce="def"} + def sample_request(token, options = {nonce: "def"}) + authorization = options.inject([%{Token token="#{token}"}]) do |arr, (k, v)| + arr << "#{k}=\"#{v}\"" + end.join(", ") + @sample_request ||= OpenStruct.new authorization: authorization end def malformed_request diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 214eab2f0d..99b53e6fd2 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -1,6 +1,6 @@ require 'abstract_unit' require 'controller/fake_controllers' -require 'action_view/vendor/html-scanner' +require 'rails/engine' class SessionTest < ActiveSupport::TestCase StubApp = lambda { |env| @@ -292,7 +292,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest assert_equal({}, cookies.to_hash) assert_equal "OK", body assert_equal "OK", response.body - assert_kind_of HTML::Document, html_document + assert_kind_of Nokogiri::HTML::Document, html_document assert_equal 1, request_count end end @@ -308,7 +308,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest assert_equal({}, cookies.to_hash) assert_equal "Created", body assert_equal "Created", response.body - assert_kind_of HTML::Document, html_document + assert_kind_of Nokogiri::HTML::Document, html_document assert_equal 1, request_count end end @@ -368,7 +368,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest assert_response :redirect assert_response :found assert_equal "<html><body>You are being <a href=\"http://www.example.com/get\">redirected</a>.</body></html>", response.body - assert_kind_of HTML::Document, html_document + assert_kind_of Nokogiri::HTML::Document, html_document assert_equal 1, request_count follow_redirect! @@ -595,7 +595,7 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest @routes ||= ActionDispatch::Routing::RouteSet.new end - class MountedApp + class MountedApp < Rails::Engine def self.routes @routes ||= ActionDispatch::Routing::RouteSet.new end diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb index 0500b7c789..7fd1276e98 100644 --- a/actionpack/test/controller/live_stream_test.rb +++ b/actionpack/test/controller/live_stream_test.rb @@ -162,7 +162,7 @@ module ActionController end def with_stale - render :text => 'stale' if stale?(:etag => "123") + render text: 'stale' if stale?(etag: "123", template: false) end def exception_in_view diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb index 18037b3d2f..49be7caf38 100644 --- a/actionpack/test/controller/log_subscriber_test.rb +++ b/actionpack/test/controller/log_subscriber_test.rb @@ -85,7 +85,7 @@ class ACLogSubscriberTest < ActionController::TestCase @old_logger = ActionController::Base.logger - @cache_path = File.expand_path('../temp/test_cache', File.dirname(__FILE__)) + @cache_path = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('tmp', 'cache') @controller.cache_store = :file_store, @cache_path ActionController::LogSubscriber.attach_to :action_controller end diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 41503e11a8..1bc7ad3015 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -128,6 +128,12 @@ class RespondToController < ActionController::Base end end + def json_with_callback + respond_to do |type| + type.json { render :json => 'JS', :callback => 'alert' } + end + end + def iphone_with_html_response_type request.format = :iphone if request.env["HTTP_ACCEPT"] == "text/iphone" @@ -511,6 +517,13 @@ class RespondToControllerTest < ActionController::TestCase assert_equal '<html><div id="html">HTML for all_types_with_layout</div></html>', @response.body end + def test_json_with_callback_sets_javascript_content_type + @request.accept = 'application/json' + get :json_with_callback + assert_equal '/**/alert(JS)', @response.body + assert_equal 'text/javascript', @response.content_type + end + def test_xhr xhr :get, :js_or_html assert_equal 'JS', @response.body diff --git a/actionpack/test/controller/mime/respond_with_test.rb b/actionpack/test/controller/mime/respond_with_test.rb deleted file mode 100644 index 115f3b2f41..0000000000 --- a/actionpack/test/controller/mime/respond_with_test.rb +++ /dev/null @@ -1,737 +0,0 @@ -require 'abstract_unit' -require 'controller/fake_models' - -class RespondWithController < ActionController::Base - class CustomerWithJson < Customer - def to_json; super; end - end - - respond_to :html, :json, :touch - respond_to :xml, :except => :using_resource_with_block - respond_to :js, :only => [ :using_resource_with_block, :using_resource, 'using_hash_resource' ] - - def using_resource - respond_with(resource) - end - - def using_hash_resource - respond_with({:result => resource}) - end - - def using_resource_with_block - respond_with(resource) do |format| - format.csv { render :text => "CSV" } - end - end - - def using_resource_with_overwrite_block - respond_with(resource) do |format| - format.html { render :text => "HTML" } - end - end - - def using_resource_with_collection - respond_with([resource, Customer.new("jamis", 9)]) - end - - def using_resource_with_parent - respond_with(Quiz::Store.new("developer?", 11), Customer.new("david", 13)) - end - - def using_resource_with_status_and_location - respond_with(resource, :location => "http://test.host/", :status => :created) - end - - def using_resource_with_json - respond_with(CustomerWithJson.new("david", request.delete? ? nil : 13)) - end - - def using_invalid_resource_with_template - respond_with(resource) - end - - def using_options_with_template - @customer = resource - respond_with(@customer, :status => 123, :location => "http://test.host/") - end - - def using_resource_with_responder - responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" } - respond_with(resource, :responder => responder) - end - - def using_resource_with_action - respond_with(resource, :action => :foo) do |format| - format.html { raise ActionView::MissingTemplate.new([], "bar", ["foo"], {}, false) } - end - end - - def using_responder_with_respond - responder = Class.new(ActionController::Responder) do - def respond; @controller.render :text => "respond #{format}"; end - end - respond_with(resource, :responder => responder) - end - - def respond_with_additional_params - @params = RespondWithController.params - respond_with({:result => resource}, @params) - end - -protected - def self.params - { - :foo => 'bar' - } - end - - def resource - Customer.new("david", request.delete? ? nil : 13) - end -end - -class InheritedRespondWithController < RespondWithController - clear_respond_to - respond_to :xml, :json - - def index - respond_with(resource) do |format| - format.json { render :text => "JSON" } - end - end -end - -class RenderJsonRespondWithController < RespondWithController - clear_respond_to - respond_to :json - - def index - respond_with(resource) do |format| - format.json { render :json => RenderJsonTestException.new('boom') } - end - end - - def create - resource = ValidatedCustomer.new(params[:name], 1) - respond_with(resource) do |format| - format.json do - if resource.errors.empty? - render :json => { :valid => true } - else - render :json => { :valid => false } - end - end - end - end -end - -class CsvRespondWithController < ActionController::Base - respond_to :csv - - class RespondWithCsv - def to_csv - "c,s,v" - end - end - - def index - respond_with(RespondWithCsv.new) - end -end - -class EmptyRespondWithController < ActionController::Base - def index - respond_with(Customer.new("david", 13)) - end -end - -class RespondWithControllerTest < ActionController::TestCase - def setup - super - @request.host = "www.example.com" - Mime::Type.register_alias('text/html', :iphone) - Mime::Type.register_alias('text/html', :touch) - Mime::Type.register('text/x-mobile', :mobile) - end - - def teardown - super - Mime::Type.unregister(:iphone) - Mime::Type.unregister(:touch) - Mime::Type.unregister(:mobile) - end - - def test_respond_with_shouldnt_modify_original_hash - get :respond_with_additional_params - assert_equal RespondWithController.params, assigns(:params) - end - - def test_using_resource - @request.accept = "application/xml" - get :using_resource - assert_equal "application/xml", @response.content_type - assert_equal "<name>david</name>", @response.body - - @request.accept = "application/json" - assert_raise ActionView::MissingTemplate do - get :using_resource - end - end - - def test_using_resource_with_js_simply_tries_to_render_the_template - @request.accept = "text/javascript" - get :using_resource - assert_equal "text/javascript", @response.content_type - assert_equal "alert(\"Hi\");", @response.body - end - - def test_using_hash_resource_with_js_raises_an_error_if_template_cant_be_found - @request.accept = "text/javascript" - assert_raise ActionView::MissingTemplate do - get :using_hash_resource - end - end - - def test_using_hash_resource - @request.accept = "application/xml" - get :using_hash_resource - assert_equal "application/xml", @response.content_type - assert_equal "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hash>\n <name>david</name>\n</hash>\n", @response.body - - @request.accept = "application/json" - get :using_hash_resource - assert_equal "application/json", @response.content_type - assert @response.body.include?("result") - assert @response.body.include?('"name":"david"') - assert @response.body.include?('"id":13') - end - - def test_using_hash_resource_with_post - @request.accept = "application/json" - assert_raise ArgumentError, "Nil location provided. Can't build URI." do - post :using_hash_resource - end - end - - def test_using_resource_with_block - @request.accept = "*/*" - get :using_resource_with_block - assert_equal "text/html", @response.content_type - assert_equal 'Hello world!', @response.body - - @request.accept = "text/csv" - get :using_resource_with_block - assert_equal "text/csv", @response.content_type - assert_equal "CSV", @response.body - - @request.accept = "application/xml" - get :using_resource - assert_equal "application/xml", @response.content_type - assert_equal "<name>david</name>", @response.body - end - - def test_using_resource_with_overwrite_block - get :using_resource_with_overwrite_block - assert_equal "text/html", @response.content_type - assert_equal "HTML", @response.body - end - - def test_not_acceptable - @request.accept = "application/xml" - assert_raises(ActionController::UnknownFormat) do - get :using_resource_with_block - end - - @request.accept = "text/javascript" - assert_raises(ActionController::UnknownFormat) do - get :using_resource_with_overwrite_block - end - end - - def test_using_resource_for_post_with_html_redirects_on_success - with_test_route_set do - post :using_resource - assert_equal "text/html", @response.content_type - assert_equal 302, @response.status - assert_equal "http://www.example.com/customers/13", @response.location - assert @response.redirect? - end - end - - def test_using_resource_for_post_with_html_rerender_on_failure - with_test_route_set do - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - post :using_resource - assert_equal "text/html", @response.content_type - assert_equal 200, @response.status - assert_equal "New world!\n", @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_post_with_xml_yields_created_on_success - with_test_route_set do - @request.accept = "application/xml" - post :using_resource - assert_equal "application/xml", @response.content_type - assert_equal 201, @response.status - assert_equal "<name>david</name>", @response.body - assert_equal "http://www.example.com/customers/13", @response.location - end - end - - def test_using_resource_for_post_with_xml_yields_unprocessable_entity_on_failure - with_test_route_set do - @request.accept = "application/xml" - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - post :using_resource - assert_equal "application/xml", @response.content_type - assert_equal 422, @response.status - assert_equal errors.to_xml, @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_post_with_json_yields_unprocessable_entity_on_failure - with_test_route_set do - @request.accept = "application/json" - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - post :using_resource - assert_equal "application/json", @response.content_type - assert_equal 422, @response.status - errors = {:errors => errors} - assert_equal errors.to_json, @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_patch_with_html_redirects_on_success - with_test_route_set do - patch :using_resource - assert_equal "text/html", @response.content_type - assert_equal 302, @response.status - assert_equal "http://www.example.com/customers/13", @response.location - assert @response.redirect? - end - end - - def test_using_resource_for_patch_with_html_rerender_on_failure - with_test_route_set do - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - patch :using_resource - assert_equal "text/html", @response.content_type - assert_equal 200, @response.status - assert_equal "Edit world!\n", @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_patch_with_html_rerender_on_failure_even_on_method_override - with_test_route_set do - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - @request.env["rack.methodoverride.original_method"] = "POST" - patch :using_resource - assert_equal "text/html", @response.content_type - assert_equal 200, @response.status - assert_equal "Edit world!\n", @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_put_with_html_redirects_on_success - with_test_route_set do - put :using_resource - assert_equal "text/html", @response.content_type - assert_equal 302, @response.status - assert_equal "http://www.example.com/customers/13", @response.location - assert @response.redirect? - end - end - - def test_using_resource_for_put_with_html_rerender_on_failure - with_test_route_set do - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - put :using_resource - - assert_equal "text/html", @response.content_type - assert_equal 200, @response.status - assert_equal "Edit world!\n", @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_put_with_html_rerender_on_failure_even_on_method_override - with_test_route_set do - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - @request.env["rack.methodoverride.original_method"] = "POST" - put :using_resource - assert_equal "text/html", @response.content_type - assert_equal 200, @response.status - assert_equal "Edit world!\n", @response.body - assert_nil @response.location - end - end - - def test_using_resource_for_put_with_xml_yields_no_content_on_success - @request.accept = "application/xml" - put :using_resource - assert_equal "application/xml", @response.content_type - assert_equal 204, @response.status - assert_equal "", @response.body - end - - def test_using_resource_for_put_with_json_yields_no_content_on_success - @request.accept = "application/json" - put :using_resource_with_json - assert_equal "application/json", @response.content_type - assert_equal 204, @response.status - assert_equal "", @response.body - end - - def test_using_resource_for_put_with_xml_yields_unprocessable_entity_on_failure - @request.accept = "application/xml" - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - put :using_resource - assert_equal "application/xml", @response.content_type - assert_equal 422, @response.status - assert_equal errors.to_xml, @response.body - assert_nil @response.location - end - - def test_using_resource_for_put_with_json_yields_unprocessable_entity_on_failure - @request.accept = "application/json" - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - put :using_resource - assert_equal "application/json", @response.content_type - assert_equal 422, @response.status - errors = {:errors => errors} - assert_equal errors.to_json, @response.body - assert_nil @response.location - end - - def test_using_resource_for_delete_with_html_redirects_on_success - with_test_route_set do - Customer.any_instance.stubs(:destroyed?).returns(true) - delete :using_resource - assert_equal "text/html", @response.content_type - assert_equal 302, @response.status - assert_equal "http://www.example.com/customers", @response.location - end - end - - def test_using_resource_for_delete_with_xml_yields_no_content_on_success - Customer.any_instance.stubs(:destroyed?).returns(true) - @request.accept = "application/xml" - delete :using_resource - assert_equal "application/xml", @response.content_type - assert_equal 204, @response.status - assert_equal "", @response.body - end - - def test_using_resource_for_delete_with_json_yields_no_content_on_success - Customer.any_instance.stubs(:destroyed?).returns(true) - @request.accept = "application/json" - delete :using_resource_with_json - assert_equal "application/json", @response.content_type - assert_equal 204, @response.status - assert_equal "", @response.body - end - - def test_using_resource_for_delete_with_html_redirects_on_failure - with_test_route_set do - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - Customer.any_instance.stubs(:destroyed?).returns(false) - delete :using_resource - assert_equal "text/html", @response.content_type - assert_equal 302, @response.status - assert_equal "http://www.example.com/customers", @response.location - end - end - - def test_using_resource_with_parent_for_get - @request.accept = "application/xml" - get :using_resource_with_parent - assert_equal "application/xml", @response.content_type - assert_equal 200, @response.status - assert_equal "<name>david</name>", @response.body - end - - def test_using_resource_with_parent_for_post - with_test_route_set do - @request.accept = "application/xml" - - post :using_resource_with_parent - assert_equal "application/xml", @response.content_type - assert_equal 201, @response.status - assert_equal "<name>david</name>", @response.body - assert_equal "http://www.example.com/quiz_stores/11/customers/13", @response.location - - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - post :using_resource - assert_equal "application/xml", @response.content_type - assert_equal 422, @response.status - assert_equal errors.to_xml, @response.body - assert_nil @response.location - end - end - - def test_using_resource_with_collection - @request.accept = "application/xml" - get :using_resource_with_collection - assert_equal "application/xml", @response.content_type - assert_equal 200, @response.status - assert_match(/<name>david<\/name>/, @response.body) - assert_match(/<name>jamis<\/name>/, @response.body) - end - - def test_using_resource_with_action - @controller.instance_eval do - def render(params={}) - self.response_body = "#{params[:action]} - #{formats}" - end - end - - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - - post :using_resource_with_action - assert_equal "foo - #{[:html].to_s}", @controller.response.body - end - - def test_respond_as_responder_entry_point - @request.accept = "text/html" - get :using_responder_with_respond - assert_equal "respond html", @response.body - - @request.accept = "application/xml" - get :using_responder_with_respond - assert_equal "respond xml", @response.body - end - - def test_clear_respond_to - @controller = InheritedRespondWithController.new - @request.accept = "text/html" - assert_raises(ActionController::UnknownFormat) do - get :index - end - end - - def test_first_in_respond_to_has_higher_priority - @controller = InheritedRespondWithController.new - @request.accept = "*/*" - get :index - assert_equal "application/xml", @response.content_type - assert_equal "<name>david</name>", @response.body - end - - def test_block_inside_respond_with_is_rendered - @controller = InheritedRespondWithController.new - @request.accept = "application/json" - get :index - assert_equal "JSON", @response.body - end - - def test_render_json_object_responds_to_str_still_produce_json - @controller = RenderJsonRespondWithController.new - @request.accept = "application/json" - get :index, :format => :json - assert_match(/"message":"boom"/, @response.body) - assert_match(/"error":"RenderJsonTestException"/, @response.body) - end - - def test_api_response_with_valid_resource_respect_override_block - @controller = RenderJsonRespondWithController.new - post :create, :name => "sikachu", :format => :json - assert_equal '{"valid":true}', @response.body - end - - def test_api_response_with_invalid_resource_respect_override_block - @controller = RenderJsonRespondWithController.new - post :create, :name => "david", :format => :json - assert_equal '{"valid":false}', @response.body - end - - def test_no_double_render_is_raised - @request.accept = "text/html" - assert_raise ActionView::MissingTemplate do - get :using_resource - end - end - - def test_using_resource_with_status_and_location - @request.accept = "text/html" - post :using_resource_with_status_and_location - assert @response.redirect? - assert_equal "http://test.host/", @response.location - - @request.accept = "application/xml" - get :using_resource_with_status_and_location - assert_equal 201, @response.status - end - - def test_using_resource_with_status_and_location_with_invalid_resource - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - - @request.accept = "text/xml" - - post :using_resource_with_status_and_location - assert_equal errors.to_xml, @response.body - assert_equal 422, @response.status - assert_equal nil, @response.location - - put :using_resource_with_status_and_location - assert_equal errors.to_xml, @response.body - assert_equal 422, @response.status - assert_equal nil, @response.location - end - - def test_using_invalid_resource_with_template - errors = { :name => :invalid } - Customer.any_instance.stubs(:errors).returns(errors) - - @request.accept = "text/xml" - - post :using_invalid_resource_with_template - assert_equal errors.to_xml, @response.body - assert_equal 422, @response.status - assert_equal nil, @response.location - - put :using_invalid_resource_with_template - assert_equal errors.to_xml, @response.body - assert_equal 422, @response.status - assert_equal nil, @response.location - end - - def test_using_options_with_template - @request.accept = "text/xml" - - post :using_options_with_template - assert_equal "<customer-name>david</customer-name>", @response.body - assert_equal 123, @response.status - assert_equal "http://test.host/", @response.location - - put :using_options_with_template - assert_equal "<customer-name>david</customer-name>", @response.body - assert_equal 123, @response.status - assert_equal "http://test.host/", @response.location - end - - def test_using_resource_with_responder - get :using_resource_with_responder - assert_equal "Resource name is david", @response.body - end - - def test_using_resource_with_set_responder - RespondWithController.responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" } - get :using_resource - assert_equal "Resource name is david", @response.body - ensure - RespondWithController.responder = ActionController::Responder - end - - def test_uses_renderer_if_an_api_behavior - ActionController::Renderers.add :csv do |obj, options| - send_data obj.to_csv, type: Mime::CSV - end - @controller = CsvRespondWithController.new - get :index, format: 'csv' - assert_equal Mime::CSV, @response.content_type - assert_equal "c,s,v", @response.body - ensure - ActionController::Renderers.remove :csv - end - - def test_raises_missing_renderer_if_an_api_behavior_with_no_renderer - @controller = CsvRespondWithController.new - assert_raise ActionController::MissingRenderer do - get :index, format: 'csv' - end - end - - def test_removing_renderers - ActionController::Renderers.add :csv do |obj, options| - send_data obj.to_csv, type: Mime::CSV - end - @controller = CsvRespondWithController.new - @request.accept = "text/csv" - get :index, format: 'csv' - assert_equal Mime::CSV, @response.content_type - - ActionController::Renderers.remove :csv - assert_raise ActionController::MissingRenderer do - get :index, format: 'csv' - end - ensure - ActionController::Renderers.remove :csv - end - - def test_error_is_raised_if_no_respond_to_is_declared_and_respond_with_is_called - @controller = EmptyRespondWithController.new - @request.accept = "*/*" - assert_raise RuntimeError do - get :index - end - end - - private - def with_test_route_set - with_routing do |set| - set.draw do - resources :customers - resources :quiz_stores do - resources :customers - end - get ":controller/:action" - end - yield - end - end -end - -class FlashResponder < ActionController::Responder - def initialize(controller, resources, options={}) - super - end - - def to_html - controller.flash[:notice] = 'Success' - super - end -end - -class FlashResponderController < ActionController::Base - self.responder = FlashResponder - respond_to :html - - def index - respond_with Object.new do |format| - format.html { render :text => 'HTML' } - end - end -end - -class FlashResponderControllerTest < ActionController::TestCase - tests FlashResponderController - - def test_respond_with_block_executed - get :index - assert_equal 'HTML', @response.body - end - - def test_flash_responder_executed - get :index - assert_equal 'Success', flash[:notice] - end -end diff --git a/actionpack/test/controller/mime/responders_test.rb b/actionpack/test/controller/mime/responders_test.rb new file mode 100644 index 0000000000..032b4c0ab1 --- /dev/null +++ b/actionpack/test/controller/mime/responders_test.rb @@ -0,0 +1,32 @@ +require 'abstract_unit' +require 'controller/fake_models' + +class ResponderTest < ActionController::TestCase + def test_class_level_respond_to + e = assert_raises(NoMethodError) do + Class.new(ActionController::Base) do + respond_to :json + end + end + + assert_includes e.message, '`responders` gem' + assert_includes e.message, '~> 2.0' + end + + def test_respond_with + klass = Class.new(ActionController::Base) do + def index + respond_with Customer.new("david", 13) + end + end + + @controller = klass.new + + e = assert_raises(NoMethodError) do + get :index + end + + assert_includes e.message, '`responders` gem' + assert_includes e.message, '~> 2.0' + end +end diff --git a/actionpack/test/controller/new_base/render_body_test.rb b/actionpack/test/controller/new_base/render_body_test.rb index fad848349a..f4a3db8b41 100644 --- a/actionpack/test/controller/new_base/render_body_test.rb +++ b/actionpack/test/controller/new_base/render_body_test.rb @@ -111,17 +111,17 @@ module RenderBody assert_status 404 end - test "rendering body with nil returns an empty body padded for Safari" do + test "rendering body with nil returns an empty body" do get "/render_body/with_layout/with_nil" - assert_body " " + assert_body "" assert_status 200 end - test "Rendering body with nil and custom status code returns an empty body padded for Safari and the status" do + test "Rendering body with nil and custom status code returns an empty body and the status" do get "/render_body/with_layout/with_nil_and_status" - assert_body " " + assert_body "" assert_status 403 end diff --git a/actionpack/test/controller/new_base/render_html_test.rb b/actionpack/test/controller/new_base/render_html_test.rb index bfe0271df7..fe11501eeb 100644 --- a/actionpack/test/controller/new_base/render_html_test.rb +++ b/actionpack/test/controller/new_base/render_html_test.rb @@ -114,17 +114,17 @@ module RenderHtml assert_status 404 end - test "rendering text with nil returns an empty body padded for Safari" do + test "rendering text with nil returns an empty body" do get "/render_html/with_layout/with_nil" - assert_body " " + assert_body "" assert_status 200 end - test "Rendering text with nil and custom status code returns an empty body padded for Safari and the status" do + test "Rendering text with nil and custom status code returns an empty body and the status" do get "/render_html/with_layout/with_nil_and_status" - assert_body " " + assert_body "" assert_status 403 end diff --git a/actionpack/test/controller/new_base/render_plain_test.rb b/actionpack/test/controller/new_base/render_plain_test.rb index dba2e9f13e..0e36d36b50 100644 --- a/actionpack/test/controller/new_base/render_plain_test.rb +++ b/actionpack/test/controller/new_base/render_plain_test.rb @@ -106,17 +106,17 @@ module RenderPlain assert_status 404 end - test "rendering text with nil returns an empty body padded for Safari" do + test "rendering text with nil returns an empty body" do get "/render_plain/with_layout/with_nil" - assert_body " " + assert_body "" assert_status 200 end - test "Rendering text with nil and custom status code returns an empty body padded for Safari and the status" do + test "Rendering text with nil and custom status code returns an empty body and the status" do get "/render_plain/with_layout/with_nil_and_status" - assert_body " " + assert_body "" assert_status 403 end diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb index b7a9cf92f2..e87811776a 100644 --- a/actionpack/test/controller/new_base/render_template_test.rb +++ b/actionpack/test/controller/new_base/render_template_test.rb @@ -9,7 +9,7 @@ module RenderTemplate "locals.html.erb" => "The secret is <%= secret %>", "xml_template.xml.builder" => "xml.html do\n xml.p 'Hello'\nend", "with_raw.html.erb" => "Hello <%=raw '<strong>this is raw</strong>' %>", - "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in a html template", + "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in an html template", "with_implicit_raw.text.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in a text template", "test/with_json.html.erb" => "<%= render :template => 'test/with_json', :formats => [:json] %>", "test/with_json.json.erb" => "<%= render :template => 'test/final', :formats => [:json] %>", @@ -114,7 +114,7 @@ module RenderTemplate get :with_implicit_raw - assert_body "Hello <strong>this is also raw</strong> in a html template" + assert_body "Hello <strong>this is also raw</strong> in an html template" assert_status 200 get :with_implicit_raw, format: 'text' diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb index abb81d7e71..10bad57cd6 100644 --- a/actionpack/test/controller/new_base/render_text_test.rb +++ b/actionpack/test/controller/new_base/render_text_test.rb @@ -106,17 +106,17 @@ module RenderText assert_status 404 end - test "rendering text with nil returns an empty body padded for Safari" do + test "rendering text with nil returns an empty body" do get "/render_text/with_layout/with_nil" - assert_body " " + assert_body "" assert_status 200 end - test "Rendering text with nil and custom status code returns an empty body padded for Safari and the status" do + test "Rendering text with nil and custom status code returns an empty body and the status" do get "/render_text/with_layout/with_nil_and_status" - assert_body " " + assert_body "" assert_status 403 end diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb new file mode 100644 index 0000000000..059f310d49 --- /dev/null +++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb @@ -0,0 +1,29 @@ +require 'abstract_unit' +require 'action_controller/metal/strong_parameters' + +class AlwaysPermittedParametersTest < ActiveSupport::TestCase + def setup + ActionController::Parameters.action_on_unpermitted_parameters = :raise + ActionController::Parameters.always_permitted_parameters = %w( controller action format ) + end + + def teardown + ActionController::Parameters.action_on_unpermitted_parameters = false + ActionController::Parameters.always_permitted_parameters = %w( controller action ) + end + + test "shows deprecations warning on NEVER_UNPERMITTED_PARAMS" do + assert_deprecated do + ActionController::Parameters::NEVER_UNPERMITTED_PARAMS + end + end + + test "permits parameters that are whitelisted" do + params = ActionController::Parameters.new({ + book: { pages: 65 }, + format: "json" + }) + permitted = params.permit book: [:pages] + assert permitted.permitted? + end +end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 4331333b98..103ca9c776 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -90,6 +90,10 @@ class RedirectController < ActionController::Base redirect_to nil end + def redirect_to_params + redirect_to ActionController::Parameters.new(status: 200, protocol: 'javascript', f: '%0Aeval(name)') + end + def redirect_to_with_block redirect_to proc { "http://www.rubyonrails.org/" } end @@ -281,6 +285,12 @@ class RedirectTest < ActionController::TestCase end end + def test_redirect_to_params + assert_raise(ActionController::ActionControllerError) do + get :redirect_to_params + end + end + def test_redirect_to_with_block get :redirect_to_with_block assert_response :redirect diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb index de8d1cbd9b..ada978aa11 100644 --- a/actionpack/test/controller/render_json_test.rb +++ b/actionpack/test/controller/render_json_test.rb @@ -101,7 +101,7 @@ class RenderJsonTest < ActionController::TestCase def test_render_json_with_callback xhr :get, :render_json_hello_world_with_callback - assert_equal 'alert({"hello":"world"})', @response.body + assert_equal '/**/alert({"hello":"world"})', @response.body assert_equal 'text/javascript', @response.content_type end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 9926130c02..b036b6c08e 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -10,11 +10,17 @@ class TestControllerWithExtraEtags < ActionController::Base etag { nil } def fresh - render text: "stale" if stale?(etag: '123') + render text: "stale" if stale?(etag: '123', template: false) end def array - render text: "stale" if stale?(etag: %w(1 2 3)) + render text: "stale" if stale?(etag: %w(1 2 3), template: false) + end + + def with_template + if stale? template: 'test/hello_world' + render text: 'stale' + end end end @@ -409,6 +415,32 @@ class EtagRenderTest < ActionController::TestCase assert_response :success end + def test_etag_reflects_template_digest + get :with_template + assert_response :ok + assert_not_nil etag = @response.etag + + request.if_none_match = etag + get :with_template + assert_response :not_modified + + # Modify the template digest + path = File.expand_path('../../fixtures/test/hello_world.erb', __FILE__) + old = File.read(path) + + begin + File.write path, 'foo' + ActionView::Digestor.cache.clear + + request.if_none_match = etag + get :with_template + assert_response :ok + assert_not_equal etag, @response.etag + ensure + File.write path, old + end + end + def etag(record) Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record)).inspect end diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index 2a5aad9c0e..05ad8b6cdc 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -389,7 +389,8 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController SecureRandom.stubs(:base64).returns(@token + '<=?') get :meta assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token' - assert_select 'meta[name=?][content=?]', 'csrf-token', 'cf50faa3fe97702ca1ae<=?' + assert_select 'meta[name=?]', 'csrf-token' + assert_match(/cf50faa3fe97702ca1ae<=\?/, @response.body) end end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index 721dad4dd9..c18914cc8e 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -77,10 +77,11 @@ class LegacyRouteSetTests < ActiveSupport::TestCase include ActionDispatch::RoutingVerbs attr_reader :rs + attr_accessor :controller alias :routes :rs def setup - @rs = ::ActionDispatch::Routing::RouteSet.new + @rs = make_set @response = nil end @@ -317,11 +318,16 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal '/admin/user/show/10', url_for(rs, { :controller => 'admin/user', :action => 'show', :id => 10 }) - assert_equal '/admin/user/show', url_for(rs, { :action => 'show' }, { :controller => 'admin/user', :action => 'list', :id => '10' }) - assert_equal '/admin/user/list/10', url_for(rs, {}, { :controller => 'admin/user', :action => 'list', :id => '10' }) + get URI('http://test.host/admin/user/list/10') - assert_equal '/admin/stuff', url_for(rs, { :controller => 'stuff' }, { :controller => 'admin/user', :action => 'list', :id => '10' }) - assert_equal '/stuff', url_for(rs, { :controller => '/stuff' }, { :controller => 'admin/user', :action => 'list', :id => '10' }) + assert_equal({ :controller => 'admin/user', :action => 'list', :id => '10' }, + controller.request.path_parameters) + + assert_equal '/admin/user/show', controller.url_for({ :action => 'show', :only_path => true }) + assert_equal '/admin/user/list/10', controller.url_for({:only_path => true}) + + assert_equal '/admin/stuff', controller.url_for({ :controller => 'stuff', :only_path => true }) + assert_equal '/stuff', controller.url_for({ :controller => '/stuff', :only_path => true }) end def test_ignores_leading_slash @@ -518,9 +524,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_changing_controller rs.draw { get ':controller/:action/:id' } + get URI('http://test.host/admin/user/index/10') + assert_equal '/admin/stuff/show/10', - url_for(rs, {:controller => 'stuff', :action => 'show', :id => 10}, - {:controller => 'admin/user', :action => 'index'}) + controller.url_for({:controller => 'stuff', :action => 'show', :id => 10, :only_path => true}) end def test_paths_escaped @@ -579,8 +586,12 @@ class LegacyRouteSetTests < ActiveSupport::TestCase get '*path' => 'content#show_file' end + get URI('http://test.host/pages/boo') + assert_equal({:controller=>"content", :action=>"show_file", :path=>"pages/boo"}, + controller.request.path_parameters) + assert_equal '/pages/boo', - url_for(rs, {}, { :controller => 'content', :action => 'show_file', :path => %w(pages boo) }) + controller.url_for(:only_path => true) end def test_backwards @@ -589,7 +600,8 @@ class LegacyRouteSetTests < ActiveSupport::TestCase get ':controller(/:action(/:id))' end - assert_equal '/page/20', url_for(rs, { :id => 20 }, { :controller => 'pages', :action => 'show' }) + get URI('http://test.host/pages/show') + assert_equal '/page/20', controller.url_for({ :id => 20, :only_path => true }) assert_equal '/page/20', url_for(rs, { :controller => 'pages', :id => 20, :action => 'show' }) assert_equal '/pages/boo', url_for(rs, { :controller => 'pages', :action => 'boo' }) end @@ -630,7 +642,8 @@ class LegacyRouteSetTests < ActiveSupport::TestCase def test_action_expiry rs.draw { get ':controller(/:action(/:id))' } - assert_equal '/content', url_for(rs, { :controller => 'content' }, { :controller => 'content', :action => 'show' }) + get URI('http://test.host/content/show') + assert_equal '/content', controller.url_for(:controller => 'content', :only_path => true) end def test_requirement_should_prevent_optional_id @@ -673,14 +686,18 @@ class LegacyRouteSetTests < ActiveSupport::TestCase assert_equal '/pages/2005/6/12', url_for(rs, { :controller => 'content', :action => 'list_pages', :year => 2005, :month => 6, :day => 12 }) + get URI('http://test.host/pages/2005/6/12') + assert_equal({ :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' }, + controller.request.path_parameters) + assert_equal '/pages/2005/6/4', - url_for(rs, { :day => 4 }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' }) + controller.url_for({ :day => 4, :only_path => true }) assert_equal '/pages/2005/6', - url_for(rs, { :day => nil }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' }) + controller.url_for({ :day => nil, :only_path => true }) assert_equal '/pages/2005', - url_for(rs, { :day => nil, :month => nil }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' }) + controller.url_for({ :day => nil, :month => nil, :only_path => true }) end def test_root_url_generation_with_controller_and_action @@ -838,9 +855,15 @@ end class RouteSetTest < ActiveSupport::TestCase include RoutingTestHelpers + include ActionDispatch::RoutingVerbs - def set - @set ||= ROUTING::RouteSet.new + attr_reader :set + alias :routes :set + attr_accessor :controller + + def setup + super + @set = make_set end def request @@ -941,7 +964,8 @@ class RouteSetTest < ActiveSupport::TestCase get '/admin/users' => 'admin/users#index', :as => "users" end - MockController.build(set.url_helpers).new + get URI('http://test.host/people') + controller end def test_named_route_url_method @@ -1038,12 +1062,12 @@ class RouteSetTest < ActiveSupport::TestCase get '/:controller(/:action(/:id))' end - assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages')) - assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages/index')) - assert_equal({:controller => 'pages', :action => 'list'}, set.recognize_path('/pages/list')) + assert_equal({:controller => 'pages', :action => 'index'}, request_path_params('/pages')) + assert_equal({:controller => 'pages', :action => 'index'}, request_path_params('/pages/index')) + assert_equal({:controller => 'pages', :action => 'list'}, request_path_params('/pages/list')) - assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/pages/show/10')) - assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10')) + assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/pages/show/10')) + assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/page/10')) end def test_route_constraints_on_request_object_with_anchors_are_valid @@ -1095,9 +1119,7 @@ class RouteSetTest < ActiveSupport::TestCase get "/people" => "missing#index" end - assert_raise(ActionController::RoutingError) { - set.recognize_path("/people", :method => :get) - } + assert_raises(ActionController::RoutingError) { request_path_params '/people' } end def test_recognize_with_encoded_id_and_regex @@ -1105,8 +1127,8 @@ class RouteSetTest < ActiveSupport::TestCase get 'page/:id' => 'pages#show', :id => /[a-zA-Z0-9\+]+/ end - assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10')) - assert_equal({:controller => 'pages', :action => 'show', :id => 'hello+world'}, set.recognize_path('/page/hello+world')) + assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/page/10')) + assert_equal({:controller => 'pages', :action => 'show', :id => 'hello+world'}, request_path_params('/page/hello+world')) end def test_recognize_with_http_methods @@ -1119,40 +1141,40 @@ class RouteSetTest < ActiveSupport::TestCase delete "/people/:id" => "people#destroy" end - params = set.recognize_path("/people", :method => :get) + params = request_path_params("/people", :method => :get) assert_equal("index", params[:action]) - params = set.recognize_path("/people", :method => :post) + params = request_path_params("/people", :method => :post) assert_equal("create", params[:action]) - params = set.recognize_path("/people/5", :method => :put) + params = request_path_params("/people/5", :method => :put) assert_equal("update", params[:action]) - params = set.recognize_path("/people/5", :method => :patch) + params = request_path_params("/people/5", :method => :patch) assert_equal("update", params[:action]) assert_raise(ActionController::UnknownHttpMethod) { - set.recognize_path("/people", :method => :bacon) + request_path_params("/people", :method => :bacon) } - params = set.recognize_path("/people/5", :method => :get) + params = request_path_params("/people/5", :method => :get) assert_equal("show", params[:action]) assert_equal("5", params[:id]) - params = set.recognize_path("/people/5", :method => :put) + params = request_path_params("/people/5", :method => :put) assert_equal("update", params[:action]) assert_equal("5", params[:id]) - params = set.recognize_path("/people/5", :method => :patch) + params = request_path_params("/people/5", :method => :patch) assert_equal("update", params[:action]) assert_equal("5", params[:id]) - params = set.recognize_path("/people/5", :method => :delete) + params = request_path_params("/people/5", :method => :delete) assert_equal("destroy", params[:action]) assert_equal("5", params[:id]) assert_raise(ActionController::RoutingError) { - set.recognize_path("/people/5", :method => :post) + request_path_params("/people/5", :method => :post) } end @@ -1162,11 +1184,11 @@ class RouteSetTest < ActiveSupport::TestCase root :to => "people#index" end - params = set.recognize_path("/people", :method => :get) + params = request_path_params("/people", :method => :get) assert_equal("people", params[:controller]) assert_equal("index", params[:action]) - params = set.recognize_path("/", :method => :get) + params = request_path_params("/", :method => :get) assert_equal("people", params[:controller]) assert_equal("index", params[:action]) end @@ -1177,7 +1199,7 @@ class RouteSetTest < ActiveSupport::TestCase :year => /\d{4}/, :day => /\d{1,2}/, :month => /\d{1,2}/ end - params = set.recognize_path("/articles/2005/11/05/a-very-interesting-article", :method => :get) + params = request_path_params("/articles/2005/11/05/a-very-interesting-article", :method => :get) assert_equal("permalink", params[:action]) assert_equal("2005", params[:year]) assert_equal("11", params[:month]) @@ -1191,7 +1213,7 @@ class RouteSetTest < ActiveSupport::TestCase get '/profile' => 'profile#index' end - set.recognize_path("/profile") rescue nil + request_path_params("/profile") rescue nil assert !Object.const_defined?("Profiler__"), "Profiler should not be loaded" end @@ -1204,17 +1226,17 @@ class RouteSetTest < ActiveSupport::TestCase get "people/:id(.:format)" => "people#show" end - params = set.recognize_path("/people/5", :method => :get) + params = request_path_params("/people/5", :method => :get) assert_equal("show", params[:action]) assert_equal("5", params[:id]) - params = set.recognize_path("/people/5", :method => :put) + params = request_path_params("/people/5", :method => :put) assert_equal("update", params[:action]) - params = set.recognize_path("/people/5", :method => :patch) + params = request_path_params("/people/5", :method => :patch) assert_equal("update", params[:action]) - params = set.recognize_path("/people/5.png", :method => :get) + params = request_path_params("/people/5.png", :method => :get) assert_equal("show", params[:action]) assert_equal("5", params[:id]) assert_equal("png", params[:format]) @@ -1233,7 +1255,7 @@ class RouteSetTest < ActiveSupport::TestCase def test_root_map set.draw { root :to => 'people#index' } - params = set.recognize_path("", :method => :get) + params = request_path_params("", :method => :get) assert_equal("people", params[:controller]) assert_equal("index", params[:action]) end @@ -1247,7 +1269,7 @@ class RouteSetTest < ActiveSupport::TestCase end - params = set.recognize_path("/api/inventory", :method => :get) + params = request_path_params("/api/inventory", :method => :get) assert_equal("api/products", params[:controller]) assert_equal("inventory", params[:action]) end @@ -1259,7 +1281,7 @@ class RouteSetTest < ActiveSupport::TestCase end end - params = set.recognize_path("/api", :method => :get) + params = request_path_params("/api", :method => :get) assert_equal("api/products", params[:controller]) assert_equal("index", params[:action]) end @@ -1271,7 +1293,7 @@ class RouteSetTest < ActiveSupport::TestCase end end - params = set.recognize_path("/prefix/inventory", :method => :get) + params = request_path_params("/prefix/inventory", :method => :get) assert_equal("api/products", params[:controller]) assert_equal("inventory", params[:action]) end @@ -1283,38 +1305,36 @@ class RouteSetTest < ActiveSupport::TestCase end end - params = set.recognize_path("/inventory", :method => :get) + params = request_path_params("/inventory", :method => :get) assert_equal("api/products", params[:controller]) assert_equal("inventory", params[:action]) end - def test_generate_changes_controller_module - set.draw { get ':controller/:action/:id' } - current = { :controller => "bling/bloop", :action => "bap", :id => 9 } - - assert_equal "/foo/bar/baz/7", - url_for(set, { :controller => "foo/bar", :action => "baz", :id => 7 }, current) - end - def test_id_is_sticky_when_it_ought_to_be + @set = make_set false + set.draw do get ':controller/:id/:action' end - url = url_for(set, { :action => "destroy" }, { :controller => "people", :action => "show", :id => "7" }) - assert_equal "/people/7/destroy", url + get URI('http://test.host/people/7/show') + + assert_equal "/people/7/destroy", controller.url_for(:action => 'destroy', :only_path => true) end def test_use_static_path_when_possible + @set = make_set false + set.draw do get 'about' => "welcome#about" get ':controller/:action/:id' end - url = url_for(set, { :controller => "welcome", :action => "about" }, - { :controller => "welcome", :action => "get", :id => "7" }) + get URI('http://test.host/welcom/get/7') - assert_equal "/about", url + assert_equal "/about", controller.url_for(:controller => 'welcome', + :action => 'about', + :only_path => true) end def test_generate @@ -1349,38 +1369,51 @@ class RouteSetTest < ActiveSupport::TestCase end def test_named_routes_are_never_relative_to_modules + @set = make_set false + set.draw do get "/connection/manage(/:action)" => 'connection/manage#index' get "/connection/connection" => "connection/connection#index" get '/connection' => 'connection#index', :as => 'family_connection' end - url = url_for(set, { :controller => "connection" }, { :controller => 'connection/manage' }) + assert_equal({ :controller => 'connection/manage', + :action => 'index', }, request_path_params('/connection/manage')) + + url = controller.url_for({ :controller => "connection", :only_path => true }) assert_equal "/connection/connection", url - url = url_for(set, { :use_route => :family_connection, :controller => "connection" }, { :controller => 'connection/manage' }) + url = controller.url_for({ :use_route => :family_connection, + :controller => "connection", :only_path => true }) assert_equal "/connection", url end def test_action_left_off_when_id_is_recalled + @set = make_set false + set.draw do get ':controller(/:action(/:id))' end - assert_equal '/books', url_for(set, - {:controller => 'books', :action => 'index'}, - {:controller => 'books', :action => 'show', :id => '10'} - ) + + get URI('http://test.host/books/show/10') + + assert_equal '/books', controller.url_for(:controller => 'books', + :only_path => true, + :action => 'index') end def test_query_params_will_be_shown_when_recalled + @set = make_set false + set.draw do get 'show_weblog/:parameter' => 'weblog#show' get ':controller(/:action(/:id))' end - assert_equal '/weblog/edit?parameter=1', url_for(set, - {:action => 'edit', :parameter => 1}, - {:controller => 'weblog', :action => 'show', :parameter => 1} - ) + + get URI('http://test.host/weblog/show/1') + + assert_equal '/weblog/edit?parameter=1', controller.url_for( + {:action => 'edit', :parameter => 1, :only_path => true}) end def test_format_is_not_inherit @@ -1388,22 +1421,30 @@ class RouteSetTest < ActiveSupport::TestCase get '/posts(.:format)' => 'posts#index' end - assert_equal '/posts', url_for(set, - {:controller => 'posts'}, - {:controller => 'posts', :action => 'index', :format => 'xml'} - ) + get URI('http://test.host/posts.xml') + assert_equal({:controller => 'posts', :action => 'index', :format => 'xml'}, + controller.request.path_parameters) + + assert_equal '/posts', controller.url_for( + {:controller => 'posts', :only_path => true}) - assert_equal '/posts.xml', url_for(set, - {:controller => 'posts', :format => 'xml'}, - {:controller => 'posts', :action => 'index', :format => 'xml'} - ) + assert_equal '/posts.xml', controller.url_for( + {:controller => 'posts', :format => 'xml', :only_path => true}) end def test_expiry_determination_should_consider_values_with_to_param + @set = make_set false + set.draw { get 'projects/:project_id/:controller/:action' } - assert_equal '/projects/1/weblog/show', url_for(set, - { :action => 'show', :project_id => 1 }, - { :controller => 'weblog', :action => 'show', :project_id => '1' }) + + get URI('http://test.host/projects/1/weblog/show') + + assert_equal( + { :controller => 'weblog', :action => 'show', :project_id => '1' }, + controller.request.path_parameters) + + assert_equal '/projects/1/weblog/show', + controller.url_for({ :action => 'show', :project_id => 1, :only_path => true }) end def test_named_route_in_nested_resource @@ -1606,7 +1647,6 @@ class RouteSetTest < ActiveSupport::TestCase end def test_slashes_are_implied - @set = nil set.draw { get("/:controller(/:action(/:id))") } assert_equal '/content', url_for(set, { :controller => 'content', :action => 'index' }) @@ -1704,7 +1744,43 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal '/ibocorp', url_for(set, { :controller => 'ibocorp', :action => "show", :page => 1 }) end + include ActionDispatch::RoutingVerbs + + class TestSet < ROUTING::RouteSet + def initialize(block) + @block = block + super() + end + + class Dispatcher < ROUTING::RouteSet::Dispatcher + def initialize(defaults, set, block) + super(defaults) + @block = block + @set = set + end + + def controller_reference(controller_param) + block = @block + set = @set + Class.new(ActionController::Base) { + include set.url_helpers + define_method(:process) { |name| block.call(self) } + def to_a; [200, {}, []]; end + } + end + end + + def dispatcher defaults + TestSet::Dispatcher.new defaults, self, @block + end + end + + alias :routes :set + def test_generate_with_optional_params_recalls_last_request + controller = nil + @set = TestSet.new ->(c) { controller = c } + set.draw do get "blog/", :controller => "blog", :action => "index" @@ -1719,23 +1795,29 @@ class RouteSetTest < ActiveSupport::TestCase get "*anything", :controller => "blog", :action => "unknown_request" end - assert_equal({:controller => "blog", :action => "index"}, set.recognize_path("/blog")) - assert_equal({:controller => "blog", :action => "show", :id => "123"}, set.recognize_path("/blog/show/123")) - assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :day => nil, :month => nil }, set.recognize_path("/blog/2004")) - assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => nil }, set.recognize_path("/blog/2004/12")) - assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => "25"}, set.recognize_path("/blog/2004/12/25")) - assert_equal({:controller => "articles", :action => "edit", :id => "123"}, set.recognize_path("/blog/articles/edit/123")) - assert_equal({:controller => "articles", :action => "show_stats"}, set.recognize_path("/blog/articles/show_stats")) - assert_equal({:controller => "blog", :action => "unknown_request", :anything => "blog/wibble"}, set.recognize_path("/blog/wibble")) - assert_equal({:controller => "blog", :action => "unknown_request", :anything => "junk"}, set.recognize_path("/junk")) + recognize_path = ->(path) { + get(URI("http://example.org" + path)) + controller.request.path_parameters + } + + assert_equal({:controller => "blog", :action => "index"}, recognize_path.("/blog")) + assert_equal({:controller => "blog", :action => "show", :id => "123"}, recognize_path.("/blog/show/123")) + assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :day => nil, :month => nil }, recognize_path.("/blog/2004")) + assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => nil }, recognize_path.("/blog/2004/12")) + assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => "25"}, recognize_path.("/blog/2004/12/25")) + assert_equal({:controller => "articles", :action => "edit", :id => "123"}, recognize_path.("/blog/articles/edit/123")) + assert_equal({:controller => "articles", :action => "show_stats"}, recognize_path.("/blog/articles/show_stats")) + assert_equal({:controller => "blog", :action => "unknown_request", :anything => "blog/wibble"}, recognize_path.("/blog/wibble")) + assert_equal({:controller => "blog", :action => "unknown_request", :anything => "junk"}, recognize_path.("/junk")) + + get URI('http://example.org/blog/2006/07/28') - last_request = set.recognize_path("/blog/2006/07/28").freeze - assert_equal({:controller => "blog", :action => "show_date", :year => "2006", :month => "07", :day => "28"}, last_request) - assert_equal("/blog/2006/07/25", url_for(set, { :day => 25 }, last_request)) - assert_equal("/blog/2005", url_for(set, { :year => 2005 }, last_request)) - assert_equal("/blog/show/123", url_for(set, { :action => "show" , :id => 123 }, last_request)) - assert_equal("/blog/2006", url_for(set, { :year => 2006 }, last_request)) - assert_equal("/blog/2006", url_for(set, { :year => 2006, :month => nil }, last_request)) + assert_equal({:controller => "blog", :action => "show_date", :year => "2006", :month => "07", :day => "28"}, controller.request.path_parameters) + assert_equal("/blog/2006/07/25", controller.url_for({ :day => 25, :only_path => true })) + assert_equal("/blog/2005", controller.url_for({ :year => 2005, :only_path => true })) + assert_equal("/blog/show/123", controller.url_for({ :action => "show" , :id => 123, :only_path => true })) + assert_equal("/blog/2006", controller.url_for({ :year => 2006, :only_path => true })) + assert_equal("/blog/2006", controller.url_for({ :year => 2006, :month => nil, :only_path => true })) end private @@ -1808,6 +1890,9 @@ class RackMountIntegrationTests < ActiveSupport::TestCase root :to => "news#index" } + attr_reader :routes + attr_reader :controller + def setup @routes = ActionDispatch::Routing::RouteSet.new @routes.draw(&Mapping) diff --git a/actionpack/test/controller/selector_test.rb b/actionpack/test/controller/selector_test.rb deleted file mode 100644 index 1e80c8601c..0000000000 --- a/actionpack/test/controller/selector_test.rb +++ /dev/null @@ -1,629 +0,0 @@ -#-- -# Copyright (c) 2006 Assaf Arkin (http://labnotes.org) -# Under MIT and/or CC By license. -#++ - -require 'abstract_unit' -require 'controller/fake_controllers' -require 'action_view/vendor/html-scanner' - -class SelectorTest < ActiveSupport::TestCase - # - # Basic selector: element, id, class, attributes. - # - - def test_element - parse(%Q{<div id="1"></div><p></p><div id="2"></div>}) - # Match element by name. - select("div") - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "2", @matches[1].attributes["id"] - # Not case sensitive. - select("DIV") - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "2", @matches[1].attributes["id"] - # Universal match (all elements). - select("*") - assert_equal 3, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal nil, @matches[1].attributes["id"] - assert_equal "2", @matches[2].attributes["id"] - end - - - def test_identifier - parse(%Q{<div id="1"></div><p></p><div id="2"></div>}) - # Match element by ID. - select("div#1") - assert_equal 1, @matches.size - assert_equal "1", @matches[0].attributes["id"] - # Match element by ID, substitute value. - select("div#?", 2) - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - # Element name does not match ID. - select("p#?", 2) - assert_equal 0, @matches.size - # Use regular expression. - select("#?", /\d/) - assert_equal 2, @matches.size - end - - - def test_class_name - parse(%Q{<div id="1" class=" foo "></div><p id="2" class=" foo bar "></p><div id="3" class="bar"></div>}) - # Match element with specified class. - select("div.foo") - assert_equal 1, @matches.size - assert_equal "1", @matches[0].attributes["id"] - # Match any element with specified class. - select("*.foo") - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "2", @matches[1].attributes["id"] - # Match elements with other class. - select("*.bar") - assert_equal 2, @matches.size - assert_equal "2", @matches[0].attributes["id"] - assert_equal "3", @matches[1].attributes["id"] - # Match only element with both class names. - select("*.bar.foo") - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - end - - - def test_attribute - parse(%Q{<div id="1"></div><p id="2" title="" bar="foo"></p><div id="3" title="foo"></div>}) - # Match element with attribute. - select("div[title]") - assert_equal 1, @matches.size - assert_equal "3", @matches[0].attributes["id"] - # Match any element with attribute. - select("*[title]") - assert_equal 2, @matches.size - assert_equal "2", @matches[0].attributes["id"] - assert_equal "3", @matches[1].attributes["id"] - # Match element with attribute value. - select("*[title=foo]") - assert_equal 1, @matches.size - assert_equal "3", @matches[0].attributes["id"] - # Match element with attribute and attribute value. - select("[bar=foo][title]") - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - # Not case sensitive. - select("[BAR=foo][TiTle]") - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - end - - - def test_attribute_quoted - parse(%Q{<div id="1" title="foo"></div><div id="2" title="bar"></div><div id="3" title=" bar "></div>}) - # Match without quotes. - select("[title = bar]") - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - # Match with single quotes. - select("[title = 'bar' ]") - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - # Match with double quotes. - select("[title = \"bar\" ]") - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - # Match with spaces. - select("[title = \" bar \" ]") - assert_equal 1, @matches.size - assert_equal "3", @matches[0].attributes["id"] - end - - - def test_attribute_equality - parse(%Q{<div id="1" title="foo bar"></div><div id="2" title="barbaz"></div>}) - # Match (fail) complete value. - select("[title=bar]") - assert_equal 0, @matches.size - # Match space-separate word. - select("[title~=foo]") - assert_equal 1, @matches.size - assert_equal "1", @matches[0].attributes["id"] - select("[title~=bar]") - assert_equal 1, @matches.size - assert_equal "1", @matches[0].attributes["id"] - # Match beginning of value. - select("[title^=ba]") - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - # Match end of value. - select("[title$=ar]") - assert_equal 1, @matches.size - assert_equal "1", @matches[0].attributes["id"] - # Match text in value. - select("[title*=bar]") - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "2", @matches[1].attributes["id"] - # Match first space separated word. - select("[title|=foo]") - assert_equal 1, @matches.size - assert_equal "1", @matches[0].attributes["id"] - select("[title|=bar]") - assert_equal 0, @matches.size - end - - - # - # Selector composition: groups, sibling, children - # - - - def test_selector_group - parse(%Q{<h1 id="1"></h1><h2 id="2"></h2><h3 id="3"></h3>}) - # Simple group selector. - select("h1,h3") - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "3", @matches[1].attributes["id"] - select("h1 , h3") - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "3", @matches[1].attributes["id"] - # Complex group selector. - parse(%Q{<h1 id="1"><a href="foo"></a></h1><h2 id="2"><a href="bar"></a></h2><h3 id="2"><a href="baz"></a></h3>}) - select("h1 a, h3 a") - assert_equal 2, @matches.size - assert_equal "foo", @matches[0].attributes["href"] - assert_equal "baz", @matches[1].attributes["href"] - # And now for the three selector challenge. - parse(%Q{<h1 id="1"><a href="foo"></a></h1><h2 id="2"><a href="bar"></a></h2><h3 id="2"><a href="baz"></a></h3>}) - select("h1 a, h2 a, h3 a") - assert_equal 3, @matches.size - assert_equal "foo", @matches[0].attributes["href"] - assert_equal "bar", @matches[1].attributes["href"] - assert_equal "baz", @matches[2].attributes["href"] - end - - - def test_sibling_selector - parse(%Q{<h1 id="1"></h1><h2 id="2"></h2><h3 id="3"></h3>}) - # Test next sibling. - select("h1+*") - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - select("h1+h2") - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - select("h1+h3") - assert_equal 0, @matches.size - select("*+h3") - assert_equal 1, @matches.size - assert_equal "3", @matches[0].attributes["id"] - # Test any sibling. - select("h1~*") - assert_equal 2, @matches.size - assert_equal "2", @matches[0].attributes["id"] - assert_equal "3", @matches[1].attributes["id"] - select("h2~*") - assert_equal 1, @matches.size - assert_equal "3", @matches[0].attributes["id"] - end - - - def test_children_selector - parse(%Q{<div><p id="1"><span id="2"></span></p></div><div><p id="3"><span id="4" class="foo"></span></p></div>}) - # Test child selector. - select("div>p") - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "3", @matches[1].attributes["id"] - select("div>span") - assert_equal 0, @matches.size - select("div>p#3") - assert_equal 1, @matches.size - assert_equal "3", @matches[0].attributes["id"] - select("div>p>span") - assert_equal 2, @matches.size - assert_equal "2", @matches[0].attributes["id"] - assert_equal "4", @matches[1].attributes["id"] - # Test descendant selector. - select("div p") - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "3", @matches[1].attributes["id"] - select("div span") - assert_equal 2, @matches.size - assert_equal "2", @matches[0].attributes["id"] - assert_equal "4", @matches[1].attributes["id"] - select("div *#3") - assert_equal 1, @matches.size - assert_equal "3", @matches[0].attributes["id"] - select("div *#4") - assert_equal 1, @matches.size - assert_equal "4", @matches[0].attributes["id"] - # This is here because it failed before when whitespaces - # were not properly stripped. - select("div .foo") - assert_equal 1, @matches.size - assert_equal "4", @matches[0].attributes["id"] - end - - - # - # Pseudo selectors: root, nth-child, empty, content, etc - # - - - def test_root_selector - parse(%Q{<div id="1"><div id="2"></div></div>}) - # Can only find element if it's root. - select(":root") - assert_equal 1, @matches.size - assert_equal "1", @matches[0].attributes["id"] - select("#1:root") - assert_equal 1, @matches.size - assert_equal "1", @matches[0].attributes["id"] - select("#2:root") - assert_equal 0, @matches.size - # Opposite for nth-child. - select("#1:nth-child(1)") - assert_equal 0, @matches.size - end - - - def test_nth_child_odd_even - parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>}) - # Test odd nth children. - select("tr:nth-child(odd)") - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "3", @matches[1].attributes["id"] - # Test even nth children. - select("tr:nth-child(even)") - assert_equal 2, @matches.size - assert_equal "2", @matches[0].attributes["id"] - assert_equal "4", @matches[1].attributes["id"] - end - - - def test_nth_child_a_is_zero - parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>}) - # Test the third child. - select("tr:nth-child(0n+3)") - assert_equal 1, @matches.size - assert_equal "3", @matches[0].attributes["id"] - # Same but an can be omitted when zero. - select("tr:nth-child(3)") - assert_equal 1, @matches.size - assert_equal "3", @matches[0].attributes["id"] - # Second element (but not every second element). - select("tr:nth-child(0n+2)") - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - # Before first and past last returns nothing.: - assert_raise(ArgumentError) { select("tr:nth-child(-1)") } - select("tr:nth-child(0)") - assert_equal 0, @matches.size - select("tr:nth-child(5)") - assert_equal 0, @matches.size - end - - - def test_nth_child_a_is_one - parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>}) - # a is group of one, pick every element in group. - select("tr:nth-child(1n+0)") - assert_equal 4, @matches.size - # Same but a can be omitted when one. - select("tr:nth-child(n+0)") - assert_equal 4, @matches.size - # Same but b can be omitted when zero. - select("tr:nth-child(n)") - assert_equal 4, @matches.size - end - - - def test_nth_child_b_is_zero - parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>}) - # If b is zero, pick the n-th element (here each one). - select("tr:nth-child(n+0)") - assert_equal 4, @matches.size - # If b is zero, pick the n-th element (here every second). - select("tr:nth-child(2n+0)") - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "3", @matches[1].attributes["id"] - # If a and b are both zero, no element selected. - select("tr:nth-child(0n+0)") - assert_equal 0, @matches.size - select("tr:nth-child(0)") - assert_equal 0, @matches.size - end - - - def test_nth_child_a_is_negative - parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>}) - # Since a is -1, picks the first three elements. - select("tr:nth-child(-n+3)") - assert_equal 3, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "2", @matches[1].attributes["id"] - assert_equal "3", @matches[2].attributes["id"] - # Since a is -2, picks the first in every second of first four elements. - select("tr:nth-child(-2n+3)") - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "3", @matches[1].attributes["id"] - # Since a is -2, picks the first in every second of first three elements. - select("tr:nth-child(-2n+2)") - assert_equal 1, @matches.size - assert_equal "1", @matches[0].attributes["id"] - end - - - def test_nth_child_b_is_negative - parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>}) - # Select last of four. - select("tr:nth-child(4n-1)") - assert_equal 1, @matches.size - assert_equal "4", @matches[0].attributes["id"] - # Select first of four. - select("tr:nth-child(4n-4)") - assert_equal 1, @matches.size - assert_equal "1", @matches[0].attributes["id"] - # Select last of every second. - select("tr:nth-child(2n-1)") - assert_equal 2, @matches.size - assert_equal "2", @matches[0].attributes["id"] - assert_equal "4", @matches[1].attributes["id"] - # Select nothing since an+b always < 0 - select("tr:nth-child(-1n-1)") - assert_equal 0, @matches.size - end - - - def test_nth_child_substitution_values - parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>}) - # Test with ?n?. - select("tr:nth-child(?n?)", 2, 1) - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "3", @matches[1].attributes["id"] - select("tr:nth-child(?n?)", 2, 2) - assert_equal 2, @matches.size - assert_equal "2", @matches[0].attributes["id"] - assert_equal "4", @matches[1].attributes["id"] - select("tr:nth-child(?n?)", 4, 2) - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - # Test with ? (b only). - select("tr:nth-child(?)", 3) - assert_equal 1, @matches.size - assert_equal "3", @matches[0].attributes["id"] - select("tr:nth-child(?)", 5) - assert_equal 0, @matches.size - end - - - def test_nth_last_child - parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>}) - # Last two elements. - select("tr:nth-last-child(-n+2)") - assert_equal 2, @matches.size - assert_equal "3", @matches[0].attributes["id"] - assert_equal "4", @matches[1].attributes["id"] - # All old elements counting from last one. - select("tr:nth-last-child(odd)") - assert_equal 2, @matches.size - assert_equal "2", @matches[0].attributes["id"] - assert_equal "4", @matches[1].attributes["id"] - end - - - def test_nth_of_type - parse(%Q{<table><thead></thead><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>}) - # First two elements. - select("tr:nth-of-type(-n+2)") - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "2", @matches[1].attributes["id"] - # All old elements counting from last one. - select("tr:nth-last-of-type(odd)") - assert_equal 2, @matches.size - assert_equal "2", @matches[0].attributes["id"] - assert_equal "4", @matches[1].attributes["id"] - end - - - def test_first_and_last - parse(%Q{<table><thead></thead><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>}) - # First child. - select("tr:first-child") - assert_equal 0, @matches.size - select(":first-child") - assert_equal 1, @matches.size - assert_equal "thead", @matches[0].name - # First of type. - select("tr:first-of-type") - assert_equal 1, @matches.size - assert_equal "1", @matches[0].attributes["id"] - select("thead:first-of-type") - assert_equal 1, @matches.size - assert_equal "thead", @matches[0].name - select("div:first-of-type") - assert_equal 0, @matches.size - # Last child. - select("tr:last-child") - assert_equal 1, @matches.size - assert_equal "4", @matches[0].attributes["id"] - # Last of type. - select("tr:last-of-type") - assert_equal 1, @matches.size - assert_equal "4", @matches[0].attributes["id"] - select("thead:last-of-type") - assert_equal 1, @matches.size - assert_equal "thead", @matches[0].name - select("div:last-of-type") - assert_equal 0, @matches.size - end - - - def test_only_child_and_only_type_first_and_last - # Only child. - parse(%Q{<table><tr></tr></table>}) - select("table:only-child") - assert_equal 0, @matches.size - select("tr:only-child") - assert_equal 1, @matches.size - assert_equal "tr", @matches[0].name - parse(%Q{<table><tr></tr><tr></tr></table>}) - select("tr:only-child") - assert_equal 0, @matches.size - # Only of type. - parse(%Q{<table><thead></thead><tr></tr><tr></tr></table>}) - select("thead:only-of-type") - assert_equal 1, @matches.size - assert_equal "thead", @matches[0].name - select("td:only-of-type") - assert_equal 0, @matches.size - end - - - def test_empty - parse(%Q{<table><tr></tr></table>}) - select("table:empty") - assert_equal 0, @matches.size - select("tr:empty") - assert_equal 1, @matches.size - parse(%Q{<div> </div>}) - select("div:empty") - assert_equal 1, @matches.size - end - - - def test_content - parse(%Q{<div> </div>}) - select("div:content()") - assert_equal 1, @matches.size - parse(%Q{<div>something </div>}) - select("div:content()") - assert_equal 0, @matches.size - select("div:content(something)") - assert_equal 1, @matches.size - select("div:content( 'something' )") - assert_equal 1, @matches.size - select("div:content( \"something\" )") - assert_equal 1, @matches.size - select("div:content(?)", "something") - assert_equal 1, @matches.size - select("div:content(?)", /something/) - assert_equal 1, @matches.size - end - - - # - # Test negation. - # - - - def test_element_negation - parse(%Q{<p></p><div></div>}) - select("*") - assert_equal 2, @matches.size - select("*:not(p)") - assert_equal 1, @matches.size - assert_equal "div", @matches[0].name - select("*:not(div)") - assert_equal 1, @matches.size - assert_equal "p", @matches[0].name - select("*:not(span)") - assert_equal 2, @matches.size - end - - - def test_id_negation - parse(%Q{<p id="1"></p><p id="2"></p>}) - select("p") - assert_equal 2, @matches.size - select(":not(#1)") - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - select(":not(#2)") - assert_equal 1, @matches.size - assert_equal "1", @matches[0].attributes["id"] - end - - - def test_class_name_negation - parse(%Q{<p class="foo"></p><p class="bar"></p>}) - select("p") - assert_equal 2, @matches.size - select(":not(.foo)") - assert_equal 1, @matches.size - assert_equal "bar", @matches[0].attributes["class"] - select(":not(.bar)") - assert_equal 1, @matches.size - assert_equal "foo", @matches[0].attributes["class"] - end - - - def test_attribute_negation - parse(%Q{<p title="foo"></p><p title="bar"></p>}) - select("p") - assert_equal 2, @matches.size - select(":not([title=foo])") - assert_equal 1, @matches.size - assert_equal "bar", @matches[0].attributes["title"] - select(":not([title=bar])") - assert_equal 1, @matches.size - assert_equal "foo", @matches[0].attributes["title"] - end - - - def test_pseudo_class_negation - parse(%Q{<div><p id="1"></p><p id="2"></p></div>}) - select("p") - assert_equal 2, @matches.size - select("p:not(:first-child)") - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - select("p:not(:nth-child(2))") - assert_equal 1, @matches.size - assert_equal "1", @matches[0].attributes["id"] - end - - - def test_negation_details - parse(%Q{<p id="1"></p><p id="2"></p><p id="3"></p>}) - assert_raise(ArgumentError) { select(":not(") } - assert_raise(ArgumentError) { select(":not(:not())") } - select("p:not(#1):not(#3)") - assert_equal 1, @matches.size - assert_equal "2", @matches[0].attributes["id"] - end - - - def test_select_from_element - parse(%Q{<div><p id="1"></p><p id="2"></p></div>}) - select("div") - @matches = @matches[0].select("p") - assert_equal 2, @matches.size - assert_equal "1", @matches[0].attributes["id"] - assert_equal "2", @matches[1].attributes["id"] - end - - -protected - - def parse(html) - @html = HTML::Document.new(html).root - end - - def select(*selector) - @matches = HTML.selector(*selector).select(@html) - end - -end diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb index ff23b22040..f7eba1ef43 100644 --- a/actionpack/test/controller/show_exceptions_test.rb +++ b/actionpack/test/controller/show_exceptions_test.rb @@ -32,7 +32,7 @@ module ShowExceptions test 'show diagnostics from a local ip if show_detailed_exceptions? is set to request.local?' do @app = ShowExceptionsController.action(:boom) - ['127.0.0.1', '127.0.0.127', '::1', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1%0'].each do |ip_address| + ['127.0.0.1', '127.0.0.127', '127.12.1.1', '::1', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1%0'].each do |ip_address| self.remote_addr = ip_address get '/' assert_match(/boom/, body) diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 1141feeff7..1280e0d9a3 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -221,7 +221,7 @@ XML assert_equal 200, @response.status end - def test_head_params_as_sting + def test_head_params_as_string assert_raise(NoMethodError) { head :test_params, "document body", :id => 10 } end @@ -353,168 +353,6 @@ XML assert_equal "bar", assigns[:bar] end - def test_assert_tag_tag - process :test_html_output - - # there is a 'form' tag - assert_tag :tag => 'form' - # there is not an 'hr' tag - assert_no_tag :tag => 'hr' - end - - def test_assert_tag_attributes - process :test_html_output - - # there is a tag with an 'id' of 'bar' - assert_tag :attributes => { :id => "bar" } - # there is no tag with a 'name' of 'baz' - assert_no_tag :attributes => { :name => "baz" } - end - - def test_assert_tag_parent - process :test_html_output - - # there is a tag with a parent 'form' tag - assert_tag :parent => { :tag => "form" } - # there is no tag with a parent of 'input' - assert_no_tag :parent => { :tag => "input" } - end - - def test_assert_tag_child - process :test_html_output - - # there is a tag with a child 'input' tag - assert_tag :child => { :tag => "input" } - # there is no tag with a child 'strong' tag - assert_no_tag :child => { :tag => "strong" } - end - - def test_assert_tag_ancestor - process :test_html_output - - # there is a 'li' tag with an ancestor having an id of 'foo' - assert_tag :ancestor => { :attributes => { :id => "foo" } }, :tag => "li" - # there is no tag of any kind with an ancestor having an href matching 'foo' - assert_no_tag :ancestor => { :attributes => { :href => /foo/ } } - end - - def test_assert_tag_descendant - process :test_html_output - - # there is a tag with a descendant 'li' tag - assert_tag :descendant => { :tag => "li" } - # there is no tag with a descendant 'html' tag - assert_no_tag :descendant => { :tag => "html" } - end - - def test_assert_tag_sibling - process :test_html_output - - # there is a tag with a sibling of class 'item' - assert_tag :sibling => { :attributes => { :class => "item" } } - # there is no tag with a sibling 'ul' tag - assert_no_tag :sibling => { :tag => "ul" } - end - - def test_assert_tag_after - process :test_html_output - - # there is a tag following a sibling 'div' tag - assert_tag :after => { :tag => "div" } - # there is no tag following a sibling tag with id 'bar' - assert_no_tag :after => { :attributes => { :id => "bar" } } - end - - def test_assert_tag_before - process :test_html_output - - # there is a tag preceding a tag with id 'bar' - assert_tag :before => { :attributes => { :id => "bar" } } - # there is no tag preceding a 'form' tag - assert_no_tag :before => { :tag => "form" } - end - - def test_assert_tag_children_count - process :test_html_output - - # there is a tag with 2 children - assert_tag :children => { :count => 2 } - # in particular, there is a <ul> tag with two children (a nameless pair of <li>s) - assert_tag :tag => 'ul', :children => { :count => 2 } - # there is no tag with 4 children - assert_no_tag :children => { :count => 4 } - end - - def test_assert_tag_children_less_than - process :test_html_output - - # there is a tag with less than 5 children - assert_tag :children => { :less_than => 5 } - # there is no 'ul' tag with less than 2 children - assert_no_tag :children => { :less_than => 2 }, :tag => "ul" - end - - def test_assert_tag_children_greater_than - process :test_html_output - - # there is a 'body' tag with more than 1 children - assert_tag :children => { :greater_than => 1 }, :tag => "body" - # there is no tag with more than 10 children - assert_no_tag :children => { :greater_than => 10 } - end - - def test_assert_tag_children_only - process :test_html_output - - # there is a tag containing only one child with an id of 'foo' - assert_tag :children => { :count => 1, - :only => { :attributes => { :id => "foo" } } } - # there is no tag containing only one 'li' child - assert_no_tag :children => { :count => 1, :only => { :tag => "li" } } - end - - def test_assert_tag_content - process :test_html_output - - # the output contains the string "Name" - assert_tag :content => /Name/ - # the output does not contain the string "test" - assert_no_tag :content => /test/ - end - - def test_assert_tag_multiple - process :test_html_output - - # there is a 'div', id='bar', with an immediate child whose 'action' - # attribute matches the regexp /somewhere/. - assert_tag :tag => "div", :attributes => { :id => "bar" }, - :child => { :attributes => { :action => /somewhere/ } } - - # there is no 'div', id='foo', with a 'ul' child with more than - # 2 "li" children. - assert_no_tag :tag => "div", :attributes => { :id => "foo" }, - :child => { - :tag => "ul", - :children => { :greater_than => 2, - :only => { :tag => "li" } } } - end - - def test_assert_tag_children_without_content - process :test_html_output - - # there is a form tag with an 'input' child which is a self closing tag - assert_tag :tag => "form", - :children => { :count => 1, - :only => { :tag => "input" } } - - # the body tag has an 'a' child which in turn has an 'img' child - assert_tag :tag => "body", - :children => { :count => 1, - :only => { :tag => "a", - :children => { :count => 1, - :only => { :tag => "img" } } } } - end - def test_should_not_impose_childless_html_tags_in_xml process :test_xml_output @@ -529,23 +367,6 @@ XML assert err.empty? end - def test_assert_tag_attribute_matching - @response.body = '<input type="text" name="my_name">' - assert_tag :tag => 'input', - :attributes => { :name => /my/, :type => 'text' } - assert_no_tag :tag => 'input', - :attributes => { :name => 'my', :type => 'text' } - assert_no_tag :tag => 'input', - :attributes => { :name => /^my$/, :type => 'text' } - end - - def test_assert_tag_content_matching - @response.body = "<p>hello world</p>" - assert_tag :tag => "p", :content => "hello world" - assert_tag :tag => "p", :content => /hello/ - assert_no_tag :tag => "p", :content => "hello" - end - def test_assert_generates assert_generates 'controller/action/5', :controller => 'controller', :action => 'action', :id => '5' assert_generates 'controller/action/7', {:id => "7"}, {:controller => "controller", :action => "action"} @@ -713,6 +534,7 @@ XML def test_header_properly_reset_after_remote_http_request xhr :get, :test_params assert_nil @request.env['HTTP_X_REQUESTED_WITH'] + assert_nil @request.env['HTTP_ACCEPT'] end def test_header_properly_reset_after_get_request @@ -721,10 +543,10 @@ XML assert_nil @request.instance_variable_get("@request_method") end - def test_params_reset_after_post_request + def test_params_reset_between_post_requests post :no_op, :foo => "bar" assert_equal "bar", @request.params[:foo] - @request.recycle! + post :no_op assert @request.params[:foo].blank? end @@ -737,12 +559,12 @@ XML assert_equal "baz", @request.filtered_parameters[:foo] end - def test_symbolized_path_params_reset_after_request + def test_path_params_reset_between_request get :test_params, :id => "foo" - assert_equal "foo", @request.symbolized_path_parameters[:id] - @request.recycle! + assert_equal "foo", @request.path_parameters[:id] + get :test_params - assert_nil @request.symbolized_path_parameters[:id] + assert_nil @request.path_parameters[:id] end def test_request_protocol_is_reset_after_request diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb index 6c2311e7a5..24a09222b1 100644 --- a/actionpack/test/controller/url_for_integration_test.rb +++ b/actionpack/test/controller/url_for_integration_test.rb @@ -6,6 +6,7 @@ require 'active_support/core_ext/object/with_options' module ActionPack class URLForIntegrationTest < ActiveSupport::TestCase include RoutingTestHelpers + include ActionDispatch::RoutingVerbs Model = Struct.new(:to_param) @@ -61,8 +62,11 @@ module ActionPack root :to => "news#index" } + attr_reader :routes + attr_accessor :controller + def setup - @routes = ActionDispatch::Routing::RouteSet.new + @routes = make_set false @routes.draw(&Mapping) end @@ -70,9 +74,9 @@ module ActionPack ['/admin/users',[ { :use_route => 'admin_users' }]], ['/admin/users',[ { :controller => 'admin/users' }]], ['/admin/users',[ { :controller => 'admin/users', :action => 'index' }]], - ['/admin/users',[ { :action => 'index' }, { :controller => 'admin/users' }]], - ['/admin/users',[ { :controller => 'users', :action => 'index' }, { :controller => 'admin/accounts' }]], - ['/people',[ { :controller => '/people', :action => 'index' }, { :controller => 'admin/accounts' }]], + ['/admin/users',[ { :action => 'index' }, { :controller => 'admin/users', :action => 'index' }, '/admin/users']], + ['/admin/users',[ { :controller => 'users', :action => 'index' }, { :controller => 'admin/accounts', :action => 'show', :id => '1' }, '/admin/accounts/show/1']], + ['/people',[ { :controller => '/people', :action => 'index' }, {:controller=>"admin/accounts", :action=>"foo", :id=>"bar"}, '/admin/accounts/foo/bar']], ['/admin/posts',[ { :controller => 'admin/posts' }]], ['/admin/posts/new',[ { :controller => 'admin/posts', :action => 'new' }]], @@ -86,11 +90,11 @@ module ActionPack ['/archive?year=january',[ { :controller => 'archive', :action => 'index', :year => 'january' }]], ['/people',[ { :controller => 'people', :action => 'index' }]], - ['/people',[ { :action => 'index' }, { :controller => 'people' }]], - ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]], - ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]], - ['/people',[ {}, { :controller => 'people', :action => 'index' }]], - ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'show', :id => '1' }]], + ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'index' }, '/people']], + ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']], + ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']], + ['/people',[ {}, { :controller => 'people', :action => 'index' }, '/people']], + ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']], ['/people/new',[ { :use_route => 'new_person' }]], ['/people/new',[ { :controller => 'people', :action => 'new' }]], ['/people/1',[ { :use_route => 'person', :id => '1' }]], @@ -98,11 +102,11 @@ module ActionPack ['/people/1.xml',[ { :controller => 'people', :action => 'show', :id => '1', :format => 'xml' }]], ['/people/1',[ { :controller => 'people', :action => 'show', :id => 1 }]], ['/people/1',[ { :controller => 'people', :action => 'show', :id => Model.new('1') }]], - ['/people/1',[ { :action => 'show', :id => '1' }, { :controller => 'people', :action => 'index' }]], - ['/people/1',[ { :action => 'show', :id => 1 }, { :controller => 'people', :action => 'show', :id => '1' }]], - ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]], - ['/people/1',[ {}, { :controller => 'people', :action => 'show', :id => '1' }]], - ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'index', :id => '1' }]], + ['/people/1',[ { :action => 'show', :id => '1' }, { :controller => 'people', :action => 'index' }, '/people']], + ['/people/1',[ { :action => 'show', :id => 1 }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']], + ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']], + ['/people/1',[ {}, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']], + ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'index', :id => '1' }, '/people/index/1']], ['/people/1/edit',[ { :controller => 'people', :action => 'edit', :id => '1' }]], ['/people/1/edit.xml',[ { :controller => 'people', :action => 'edit', :id => '1', :format => 'xml' }]], ['/people/1/edit',[ { :use_route => 'edit_person', :id => '1' }]], @@ -118,16 +122,15 @@ module ActionPack ['/project',[ { :controller => 'project', :action => 'index' }]], ['/projects/1',[ { :controller => 'project', :action => 'index', :project_id => '1' }]], - ['/projects/1',[ { :controller => 'project', :action => 'index'}, {:project_id => '1' }]], + ['/projects/1',[ { :controller => 'project', :action => 'index'}, {:project_id => '1', :controller => 'project', :action => 'index' }, '/projects/1']], ['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index', :project_id => '1' }]], - ['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index' }, { :project_id => '1' }]], + ['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index' }, { :controller => 'project', :action => 'index', :project_id => '1' }, '/projects/1']], ['/clients',[ { :controller => 'projects', :action => 'index' }]], ['/clients?project_id=1',[ { :controller => 'projects', :action => 'index', :project_id => '1' }]], - ['/clients',[ { :controller => 'projects', :action => 'index' }, { :project_id => '1' }]], - ['/clients',[ { :action => 'index' }, { :controller => 'projects', :action => 'index', :project_id => '1' }]], + ['/clients',[ { :controller => 'projects', :action => 'index' }, { :project_id => '1', :controller => 'project', :action => 'index' }, '/projects/1']], - ['/comment/20',[ { :id => 20 }, { :controller => 'comments', :action => 'show' }]], + ['/comment/20',[ { :id => 20 }, { :controller => 'comments', :action => 'show' }, '/comments/show']], ['/comment/20',[ { :controller => 'comments', :id => 20, :action => 'show' }]], ['/comments/boo',[ { :controller => 'comments', :action => 'boo' }]], @@ -144,24 +147,21 @@ module ActionPack ['/notes',[ { :page_id => nil, :controller => 'notes' }]], ['/notes',[ { :controller => 'notes' }]], ['/notes/print',[ { :controller => 'notes', :action => 'print' }]], - ['/notes/print',[ {}, { :controller => 'notes', :action => 'print' }]], - - ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1' }]], - ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1', :foo => 'bar' }]], - ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1' }]], - ['/notes/index/1',[ { :action => 'index' }, { :controller => 'notes', :id => '1' }]], - ['/notes/index/1',[ {}, { :controller => 'notes', :id => '1' }]], - ['/notes/show/1',[ {}, { :controller => 'notes', :action => 'show', :id => '1' }]], - ['/notes/index/1',[ { :controller => 'notes', :id => '1' }, { :foo => 'bar' }]], - ['/posts',[ { :controller => 'posts' }, { :controller => 'notes', :action => 'show', :id => '1' }]], - ['/notes/list',[ { :action => 'list' }, { :controller => 'notes', :action => 'show', :id => '1' }]], + ['/notes/print',[ {}, { :controller => 'notes', :action => 'print' }, '/notes/print']], + + ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :action => 'index', :id => '1' }, '/notes/index/1']], + ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']], + ['/notes/index/1',[ { :action => 'index' }, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']], + ['/notes/index/1',[ {}, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']], + ['/notes/show/1',[ {}, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']], + ['/posts',[ { :controller => 'posts' }, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']], + ['/notes/list',[ { :action => 'list' }, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']], ['/posts/ping',[ { :controller => 'posts', :action => 'ping' }]], ['/posts/show/1',[ { :controller => 'posts', :action => 'show', :id => '1' }]], ['/posts',[ { :controller => 'posts' }]], ['/posts',[ { :controller => 'posts', :action => 'index' }]], - ['/posts',[ { :controller => 'posts' }, { :controller => 'posts', :action => 'index' }]], - ['/posts/create',[ { :action => 'create' }, { :controller => 'posts' }]], + ['/posts/create',[ { :action => 'create' }, {:day=>nil, :month=>nil, :controller=>"posts", :action=>"show_date"}, '/blog']], ['/posts?foo=bar',[ { :controller => 'posts', :foo => 'bar' }]], ['/posts?foo%5B%5D=bar&foo%5B%5D=baz', [{ :controller => 'posts', :foo => ['bar', 'baz'] }]], ['/posts?page=2', [{ :controller => 'posts', :page => 2 }]], @@ -169,9 +169,20 @@ module ActionPack ['/news.rss', [{ :controller => 'news', :action => 'index', :format => 'rss' }]], ].each_with_index do |(url, params), i| - define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do - assert_equal url, url_for(@routes, *params), params.inspect - end + if params.length > 1 + hash, path_params, route = *params + hash[:only_path] = true + + define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do + get URI('http://test.host' + route.to_s) + assert_equal path_params, controller.request.path_parameters + assert_equal url, controller.url_for(hash), params.inspect + end + else + define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do + assert_equal url, url_for(@routes, params.first), params.inspect + end + end end end end diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 7210c68e73..9f086af664 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -287,12 +287,12 @@ module AbstractController # We need to create a new class in order to install the new named route. kls = Class.new { include set.url_helpers } controller = kls.new - assert controller.respond_to?(:home_url) + assert_respond_to controller, :home_url assert_equal '/brave/new/world', - controller.send(:url_for, :controller => 'brave', :action => 'new', :id => 'world', :only_path => true) + controller.url_for(:controller => 'brave', :action => 'new', :id => 'world', :only_path => true) - assert_equal("/home/sweet/home/alabama", controller.send(:home_url, :user => 'alabama', :host => 'unused', :only_path => true)) - assert_equal("/home/sweet/home/alabama", controller.send(:home_path, 'alabama')) + assert_equal("/home/sweet/home/alabama", controller.home_path(:user => 'alabama', :host => 'unused', :only_path => true)) + assert_equal("/home/sweet/home/alabama", controller.home_path('alabama')) end end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 0f145666d1..9b03c805a0 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -21,6 +21,16 @@ class CookiesTest < ActionController::TestCase end end + class JSONWrapper + def initialize(obj) + @obj = obj + end + + def as_json(options = nil) + "wrapped: #{@obj.as_json(options)}" + end + end + class TestController < ActionController::Base def authenticate cookies["user_name"] = "david" @@ -85,6 +95,11 @@ class CookiesTest < ActionController::TestCase head :ok end + def set_wrapped_signed_cookie + cookies.signed[:user_id] = JSONWrapper.new(45) + head :ok + end + def get_signed_cookie cookies.signed[:user_id] head :ok @@ -95,6 +110,11 @@ class CookiesTest < ActionController::TestCase head :ok end + def set_wrapped_encrypted_cookie + cookies.encrypted[:foo] = JSONWrapper.new('bar') + head :ok + end + def get_encrypted_cookie cookies.encrypted[:foo] head :ok @@ -369,6 +389,35 @@ class CookiesTest < ActionController::TestCase assert_equal 'Jamie', @controller.send(:cookies).permanent[:user_name] end + def test_signed_cookie_using_default_digest + get :set_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"] + secret = key_generator.generate_key(signed_cookie_salt) + + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: 'SHA1') + assert_equal verifier.generate(45), cookies[:user_id] + end + + def test_signed_cookie_using_custom_digest + @request.env["action_dispatch.cookies_digest"] = 'SHA256' + get :set_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 45, cookies[:user_id] + assert_equal 45, cookies.signed[:user_id] + + key_generator = @request.env["action_dispatch.key_generator"] + signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"] + secret = key_generator.generate_key(signed_cookie_salt) + + verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: 'SHA256') + assert_equal verifier.generate(45), cookies[:user_id] + end + def test_signed_cookie_using_default_serializer get :set_signed_cookie cookies = @controller.send :cookies @@ -392,6 +441,14 @@ class CookiesTest < ActionController::TestCase assert_equal 45, cookies.signed[:user_id] end + def test_wrapped_signed_cookie_using_json_serializer + @request.env["action_dispatch.cookies_serializer"] = :json + get :set_wrapped_signed_cookie + cookies = @controller.send :cookies + assert_not_equal 'wrapped: 45', cookies[:user_id] + assert_equal 'wrapped: 45', cookies.signed[:user_id] + end + def test_signed_cookie_using_custom_serializer @request.env["action_dispatch.cookies_serializer"] = CustomSerializer get :set_signed_cookie @@ -474,6 +531,17 @@ class CookiesTest < ActionController::TestCase assert_equal 'bar', cookies.encrypted[:foo] end + def test_wrapped_encrypted_cookie_using_json_serializer + @request.env["action_dispatch.cookies_serializer"] = :json + get :set_wrapped_encrypted_cookie + cookies = @controller.send :cookies + assert_not_equal 'wrapped: bar', cookies[:foo] + assert_raises ::JSON::ParserError do + cookies.signed[:foo] + end + assert_equal 'wrapped: bar', cookies.encrypted[:foo] + end + def test_encrypted_cookie_using_custom_serializer @request.env["action_dispatch.cookies_serializer"] = CustomSerializer get :set_encrypted_cookie @@ -481,6 +549,27 @@ class CookiesTest < ActionController::TestCase assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo] end + def test_encrypted_cookie_using_custom_digest + @request.env["action_dispatch.cookies_digest"] = 'SHA256' + get :set_encrypted_cookie + cookies = @controller.send :cookies + assert_not_equal 'bar', cookies[:foo] + assert_equal 'bar', cookies.encrypted[:foo] + + sign_secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"]) + + sha1_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: 'SHA1') + sha256_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: 'SHA256') + + assert_raises(ActiveSupport::MessageVerifier::InvalidSignature) do + sha1_verifier.verify(cookies[:foo]) + end + + assert_nothing_raised do + sha256_verifier.verify(cookies[:foo]) + end + end + def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json @request.env["action_dispatch.cookies_serializer"] = :hybrid diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 8660deb634..24526fb00e 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -10,6 +10,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest @closed = false end + # We're obliged to implement this (even though it doesn't actually + # get called here) to properly comply with the Rack SPEC def each end diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb index d8d3209dac..889f9a4736 100644 --- a/actionpack/test/dispatch/mapper_test.rb +++ b/actionpack/test/dispatch/mapper_test.rb @@ -3,7 +3,7 @@ require 'abstract_unit' module ActionDispatch module Routing class MapperTest < ActiveSupport::TestCase - class FakeSet + class FakeSet < ActionDispatch::Routing::RouteSet attr_reader :routes alias :set :routes @@ -38,7 +38,7 @@ module ActionDispatch def test_mapping_requirements options = { :controller => 'foo', :action => 'bar', :via => :get } - m = Mapper::Mapping.build({}, '/store/:name(*rest)', options) + m = Mapper::Mapping.build({}, FakeSet.new, '/store/:name(*rest)', nil, options) _, _, requirements, _ = m.to_route assert_equal(/.+?/, requirements[:rest]) end diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb index bd3f6274de..d5a4d8ee11 100644 --- a/actionpack/test/dispatch/mount_test.rb +++ b/actionpack/test/dispatch/mount_test.rb @@ -1,12 +1,18 @@ require 'abstract_unit' +require 'rails/engine' class TestRoutingMount < ActionDispatch::IntegrationTest Router = ActionDispatch::Routing::RouteSet.new - class FakeEngine + class AppWithRoutes < Rails::Engine def self.routes @routes ||= ActionDispatch::Routing::RouteSet.new end + end + + # Test for mounting apps that respond to routes, but aren't Rails-like apps. + class SinatraLikeApp + def self.routes; Object.new; end def self.call(env) [200, {"Content-Type" => "text/html"}, ["OK"]] @@ -21,22 +27,23 @@ class TestRoutingMount < ActionDispatch::IntegrationTest mount SprocketsApp, :at => "/sprockets" mount SprocketsApp => "/shorthand" - mount FakeEngine, :at => "/fakeengine", :as => :fake - mount FakeEngine, :at => "/getfake", :via => :get + mount SinatraLikeApp, :at => "/fakeengine", :as => :fake + mount SinatraLikeApp, :at => "/getfake", :via => :get scope "/its_a" do mount SprocketsApp, :at => "/sprocket" end resources :users do - mount FakeEngine, :at => "/fakeengine", :as => :fake_mounted_at_resource + mount AppWithRoutes, :at => "/fakeengine", :as => :fake_mounted_at_resource end mount SprocketsApp, :at => "/", :via => :get end + APP = RoutedRackApp.new Router def app - Router + APP end def test_app_name_is_properly_generated_when_engine_is_mounted_in_resources diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index cd31e8e326..f90d5499d7 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'rack/test' +require 'rails/engine' module TestGenerationPrefix class Post @@ -23,73 +24,48 @@ module TestGenerationPrefix class WithMountedEngine < ActionDispatch::IntegrationTest include Rack::Test::Methods - class BlogEngine - def self.routes - @routes ||= begin - routes = ActionDispatch::Routing::RouteSet.new - routes.draw do - get "/posts/:id", :to => "inside_engine_generating#show", :as => :post - get "/posts", :to => "inside_engine_generating#index", :as => :posts - get "/url_to_application", :to => "inside_engine_generating#url_to_application" - get "/polymorphic_path_for_engine", :to => "inside_engine_generating#polymorphic_path_for_engine" - get "/conflicting_url", :to => "inside_engine_generating#conflicting" - get "/foo", :to => "never#invoked", :as => :named_helper_that_should_be_invoked_only_in_respond_to_test - - get "/relative_path_root", :to => redirect("") - get "/relative_path_redirect", :to => redirect("foo") - get "/relative_option_root", :to => redirect(:path => "") - get "/relative_option_redirect", :to => redirect(:path => "foo") - get "/relative_custom_root", :to => redirect { |params, request| "" } - get "/relative_custom_redirect", :to => redirect { |params, request| "foo" } - - get "/absolute_path_root", :to => redirect("/") - get "/absolute_path_redirect", :to => redirect("/foo") - get "/absolute_option_root", :to => redirect(:path => "/") - get "/absolute_option_redirect", :to => redirect(:path => "/foo") - get "/absolute_custom_root", :to => redirect { |params, request| "/" } - get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" } - end - - routes - end - end - - def self.call(env) - env['action_dispatch.routes'] = routes - routes.call(env) + class BlogEngine < Rails::Engine + routes.draw do + get "/posts/:id", :to => "inside_engine_generating#show", :as => :post + get "/posts", :to => "inside_engine_generating#index", :as => :posts + get "/url_to_application", :to => "inside_engine_generating#url_to_application" + get "/polymorphic_path_for_engine", :to => "inside_engine_generating#polymorphic_path_for_engine" + get "/conflicting_url", :to => "inside_engine_generating#conflicting" + get "/foo", :to => "never#invoked", :as => :named_helper_that_should_be_invoked_only_in_respond_to_test + + get "/relative_path_root", :to => redirect("") + get "/relative_path_redirect", :to => redirect("foo") + get "/relative_option_root", :to => redirect(:path => "") + get "/relative_option_redirect", :to => redirect(:path => "foo") + get "/relative_custom_root", :to => redirect { |params, request| "" } + get "/relative_custom_redirect", :to => redirect { |params, request| "foo" } + + get "/absolute_path_root", :to => redirect("/") + get "/absolute_path_redirect", :to => redirect("/foo") + get "/absolute_option_root", :to => redirect(:path => "/") + get "/absolute_option_redirect", :to => redirect(:path => "/foo") + get "/absolute_custom_root", :to => redirect { |params, request| "/" } + get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" } end end - class RailsApplication - def self.routes - @routes ||= begin - routes = ActionDispatch::Routing::RouteSet.new - routes.draw do - scope "/:omg", :omg => "awesome" do - mount BlogEngine => "/blog", :as => "blog_engine" - end - get "/posts/:id", :to => "outside_engine_generating#post", :as => :post - get "/generate", :to => "outside_engine_generating#index" - get "/polymorphic_path_for_app", :to => "outside_engine_generating#polymorphic_path_for_app" - get "/polymorphic_path_for_engine", :to => "outside_engine_generating#polymorphic_path_for_engine" - get "/polymorphic_with_url_for", :to => "outside_engine_generating#polymorphic_with_url_for" - get "/conflicting_url", :to => "outside_engine_generating#conflicting" - get "/ivar_usage", :to => "outside_engine_generating#ivar_usage" - root :to => "outside_engine_generating#index" - end - - routes + class RailsApplication < Rails::Engine + routes.draw do + scope "/:omg", :omg => "awesome" do + mount BlogEngine => "/blog", :as => "blog_engine" end - end - - def self.call(env) - env['action_dispatch.routes'] = routes - routes.call(env) + get "/posts/:id", :to => "outside_engine_generating#post", :as => :post + get "/generate", :to => "outside_engine_generating#index" + get "/polymorphic_path_for_app", :to => "outside_engine_generating#polymorphic_path_for_app" + get "/polymorphic_path_for_engine", :to => "outside_engine_generating#polymorphic_path_for_engine" + get "/polymorphic_with_url_for", :to => "outside_engine_generating#polymorphic_with_url_for" + get "/conflicting_url", :to => "outside_engine_generating#conflicting" + get "/ivar_usage", :to => "outside_engine_generating#ivar_usage" + root :to => "outside_engine_generating#index" end end # force draw - RailsApplication.routes RailsApplication.routes.define_mounted_helper(:main_app) class ::InsideEngineGeneratingController < ActionController::Base @@ -161,19 +137,15 @@ module TestGenerationPrefix end def app - RailsApplication + RailsApplication.instance end - def engine_object - @engine_object ||= EngineObject.new - end - - def app_object - @app_object ||= AppObject.new - end + attr_reader :engine_object, :app_object def setup RailsApplication.routes.default_url_options = {} + @engine_object = EngineObject.new + @app_object = AppObject.new end include BlogEngine.routes.mounted_helpers @@ -395,27 +367,12 @@ module TestGenerationPrefix end end - class RailsApplication - def self.routes - @routes ||= begin - routes = ActionDispatch::Routing::RouteSet.new - routes.draw do - mount BlogEngine => "/" - end - - routes - end - end - - def self.call(env) - env['action_dispatch.routes'] = routes - routes.call(env) + class RailsApplication < Rails::Engine + routes.draw do + mount BlogEngine => "/" end end - # force draw - RailsApplication.routes - class ::PostsController < ActionController::Base include BlogEngine.routes.url_helpers include RailsApplication.routes.mounted_helpers @@ -426,7 +383,7 @@ module TestGenerationPrefix end def app - RailsApplication + RailsApplication.instance end test "generating path inside engine" do diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb index 2db3fee6bb..926472163e 100644 --- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb @@ -8,7 +8,11 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest end def parse - self.class.last_request_parameters = request.request_parameters + self.class.last_request_parameters = begin + request.request_parameters + rescue EOFError + {} + end self.class.last_parameters = request.parameters head :ok end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index e4950a5d6b..fe9ee6f73d 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -528,6 +528,13 @@ class RequestCGI < BaseRequestTest end end +class LocalhostTest < BaseRequestTest + test "IPs that match localhost" do + request = stub_request("REMOTE_IP" => "127.1.1.1", "REMOTE_ADDR" => "127.1.1.1") + assert request.local? + end +end + class RequestCookie < BaseRequestTest test "cookie syntax resilience" do request = stub_request("HTTP_COOKIE" => "_session_id=c84ace84796670c052c6ceb2451fb0f2; is_admin=yes") @@ -632,12 +639,14 @@ class RequestProtocol < BaseRequestTest end class RequestMethod < BaseRequestTest - test "request methods" do - [:post, :get, :patch, :put, :delete].each do |method| - request = stub_request('REQUEST_METHOD' => method.to_s.upcase) + test "method returns environment's request method when it has not been + overridden by middleware".squish do - assert_equal method.to_s.upcase, request.method - assert_equal method, request.method_symbol + ActionDispatch::Request::HTTP_METHODS.each do |method| + request = stub_request('REQUEST_METHOD' => method) + + assert_equal method, request.method + assert_equal method.underscore.to_sym, request.method_symbol end end @@ -647,28 +656,18 @@ class RequestMethod < BaseRequestTest end end - test "allow method hacking on post" do - %w(GET OPTIONS PATCH PUT POST DELETE).each do |method| - request = stub_request 'REQUEST_METHOD' => method.to_s.upcase - - assert_equal(method == "HEAD" ? "GET" : method, request.method) - end + test "method returns original value of environment request method on POST" do + request = stub_request('rack.methodoverride.original_method' => 'POST') + assert_equal 'POST', request.method end - test "invalid method hacking on post raises exception" do + test "method raises exception on invalid HTTP method" do assert_raise(ActionController::UnknownHttpMethod) do - stub_request('REQUEST_METHOD' => '_RANDOM_METHOD').request_method + stub_request('rack.methodoverride.original_method' => '_RANDOM_METHOD').method end - end - - test "restrict method hacking" do - [:get, :patch, :put, :delete].each do |method| - request = stub_request( - 'action_dispatch.request.request_parameters' => { :_method => 'put' }, - 'REQUEST_METHOD' => method.to_s.upcase - ) - assert_equal method.to_s.upcase, request.method + assert_raise(ActionController::UnknownHttpMethod) do + stub_request('REQUEST_METHOD' => '_RANDOM_METHOD').method end end @@ -798,6 +797,12 @@ class RequestFormat < BaseRequestTest assert_not request.format.json? end + test "format does not throw exceptions when malformed parameters" do + request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2") + assert request.formats + assert request.format.html? + end + test "formats with xhr request" do request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest" request.expects(:parameters).at_least_once.returns({}) @@ -893,15 +898,15 @@ class RequestParameters < BaseRequestTest assert_equal({"bar" => 2}, request.query_parameters) end - test "parameters still accessible after rack parse error" do + test "parameters not accessible after rack parse error" do request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2") - assert_raises(ActionController::BadRequest) do - # rack will raise a TypeError when parsing this query string - request.parameters + 2.times do + assert_raises(ActionController::BadRequest) do + # rack will raise a TypeError when parsing this query string + request.parameters + end end - - assert_equal({}, request.parameters) end test "we have access to the original exception" do @@ -1066,7 +1071,7 @@ class RequestEtag < BaseRequestTest end end -class RequestVarient < BaseRequestTest +class RequestVariant < BaseRequestTest test "setting variant" do request = stub_request diff --git a/actionpack/test/dispatch/routing/concerns_test.rb b/actionpack/test/dispatch/routing/concerns_test.rb index 9f37701656..7ef513b0c8 100644 --- a/actionpack/test/dispatch/routing/concerns_test.rb +++ b/actionpack/test/dispatch/routing/concerns_test.rb @@ -36,7 +36,8 @@ class RoutingConcernsTest < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = RoutedRackApp.new Routes + def app; APP end def test_accessing_concern_from_resources get "/posts/1/comments" diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 778dbfc74d..b8e20c52a0 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -2022,6 +2022,28 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest assert_equal '/blogs/1/posts/2/comments/new', new_blog_post_comment_path(:blog_id => 1, :post_id => 2) end + def test_direct_children_of_shallow_resources + draw do + resources :blogs do + resources :posts, shallow: true do + resources :comments + end + end + end + + post '/posts/1/comments' + assert_equal 'comments#create', @response.body + assert_equal '/posts/1/comments', post_comments_path('1') + + get '/posts/2/comments/new' + assert_equal 'comments#new', @response.body + assert_equal '/posts/2/comments/new', new_post_comment_path('2') + + get '/posts/1/comments' + assert_equal 'comments#index', @response.body + assert_equal '/posts/1/comments', post_comments_path('1') + end + def test_shallow_nested_resources_within_scope draw do scope '/hello' do @@ -2831,7 +2853,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - def test_symbolized_path_parameters_is_not_stale + def test_path_parameters_is_not_stale draw do scope '/countries/:country', :constraints => lambda { |params, req| %w(all France).include?(params[:country]) } do get '/', :to => 'countries#index' @@ -3148,7 +3170,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest get '/downloads/1/1.tar' assert_equal 'downloads#show', @response.body - assert_equal expected_params, @request.symbolized_path_parameters + assert_equal expected_params, @request.path_parameters assert_equal '/downloads/1/1.tar', download_path('1') assert_equal '/downloads/1/1.tar', download_path('1', '1') end @@ -3421,19 +3443,19 @@ private def draw(&block) self.class.stub_controllers do |routes| - @app = routes - @app.default_url_options = { host: 'www.example.com' } - @app.draw(&block) + routes.default_url_options = { host: 'www.example.com' } + routes.draw(&block) + @app = RoutedRackApp.new routes end end def url_for(options = {}) - @app.url_helpers.url_for(options) + @app.routes.url_helpers.url_for(options) end def method_missing(method, *args, &block) if method.to_s =~ /_(path|url)$/ - @app.url_helpers.send(method, *args, &block) + @app.routes.url_helpers.send(method, *args, &block) else super end @@ -3501,8 +3523,10 @@ class TestAltApp < ActionDispatch::IntegrationTest get "/" => TestAltApp::AltApp.new end + APP = build_app AltRoutes + def app - AltRoutes + APP end def test_alt_request_without_header @@ -3529,15 +3553,16 @@ class TestAppendingRoutes < ActionDispatch::IntegrationTest def setup super s = self - @app = ActionDispatch::Routing::RouteSet.new - @app.append do + routes = ActionDispatch::Routing::RouteSet.new + routes.append do get '/hello' => s.simple_app('fail') get '/goodbye' => s.simple_app('goodbye') end - @app.draw do + routes.draw do get '/hello' => s.simple_app('hello') end + @app = self.class.build_app routes end def test_goodbye_should_be_available @@ -3566,8 +3591,9 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest end def draw(&block) - @app = ActionDispatch::Routing::RouteSet.new - @app.draw(&block) + routes = ActionDispatch::Routing::RouteSet.new + routes.draw(&block) + @app = self.class.build_app routes end def test_missing_controller @@ -3667,8 +3693,10 @@ class TestDefaultScope < ActionDispatch::IntegrationTest resources :posts end + APP = build_app DefaultScopeRoutes + def app - DefaultScopeRoutes + APP end include DefaultScopeRoutes.url_helpers @@ -3693,11 +3721,14 @@ class TestHttpMethods < ActionDispatch::IntegrationTest lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [response] ] } end - setup do + attr_reader :app + + def setup s = self - @app = ActionDispatch::Routing::RouteSet.new + routes = ActionDispatch::Routing::RouteSet.new + @app = RoutedRackApp.new routes - @app.draw do + routes.draw do (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method| match '/' => s.simple_app(method), :via => method.underscore.to_sym end @@ -3728,7 +3759,8 @@ class TestUriPathEscaping < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end test 'escapes slash in generated path segment' do assert_equal '/a%20b%2Fc+d', segment_path(:segment => 'a b/c+d') @@ -3759,7 +3791,8 @@ class TestUnicodePaths < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end test 'recognizes unicode path' do get "/#{Rack::Utils.escape("ほげ")}" @@ -3790,7 +3823,8 @@ class TestMultipleNestedController < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end test "controller option which starts with '/' from multiple nested controller" do get "/foo/bar/baz" @@ -3809,7 +3843,8 @@ class TestTildeAndMinusPaths < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end test 'recognizes tilde path' do get "/~user" @@ -3836,7 +3871,8 @@ class TestRedirectInterpolation < ActionDispatch::IntegrationTest end end - def app; Routes end + APP = build_app Routes + def app; APP end test "redirect escapes interpolated parameters with redirect proc" do get "/foo/1%3E" @@ -3878,7 +3914,8 @@ class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest end end - def app; Routes end + APP = build_app Routes + def app; APP end test "parameters are reset between constraint checks" do get "/bar" @@ -3898,7 +3935,8 @@ class TestGlobRoutingMapper < ActionDispatch::IntegrationTest end #include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end def test_glob_constraint get "/dummy" @@ -3930,7 +3968,8 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end test 'enabled when not mounted and default_url_options is empty' do assert Routes.url_helpers.optimize_routes_generation? @@ -4002,7 +4041,8 @@ class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest end end - def app; Routes end + APP = build_app Routes + def app; APP end include Routes.url_helpers @@ -4037,7 +4077,8 @@ class TestUrlConstraints < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end test "constraints are copied to defaults when using constraints method" do assert_equal 'http://admin.example.com/', admin_root_url @@ -4118,8 +4159,9 @@ class TestOptionalRootSegments < ActionDispatch::IntegrationTest end end + APP = build_app Routes def app - Routes + APP end include Routes.url_helpers @@ -4150,7 +4192,8 @@ class TestPortConstraints < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end def test_integer_port_constraints get 'http://www.example.com/integer' @@ -4198,7 +4241,8 @@ class TestFormatConstraints < ActionDispatch::IntegrationTest end include Routes.url_helpers - def app; Routes end + APP = build_app Routes + def app; APP end def test_string_format_constraints get 'http://www.example.com/string' @@ -4248,11 +4292,9 @@ end class TestCallableConstraintValidation < ActionDispatch::IntegrationTest def test_constraint_with_object_not_callable assert_raises(ArgumentError) do - ActionDispatch::Routing::RouteSet.new.tap do |app| - app.draw do - ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } - get '/test', to: ok, constraints: Object.new - end + ActionDispatch::Routing::RouteSet.new.draw do + ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] } + get '/test', to: ok, constraints: Object.new end end end @@ -4267,8 +4309,9 @@ class TestRouteDefaults < ActionDispatch::IntegrationTest end end + APP = build_app Routes def app - Routes + APP end include Routes.url_helpers @@ -4296,8 +4339,9 @@ class TestRackAppRouteGeneration < ActionDispatch::IntegrationTest end end + APP = build_app Routes def app - Routes + APP end include Routes.url_helpers @@ -4322,8 +4366,9 @@ class TestRedirectRouteGeneration < ActionDispatch::IntegrationTest end end + APP = build_app Routes def app - Routes + APP end include Routes.url_helpers @@ -4346,7 +4391,8 @@ class TestUrlGenerationErrors < ActionDispatch::IntegrationTest end end - def app; Routes end + APP = build_app Routes + def app; APP end include Routes.url_helpers diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb index b8479e8836..9f810cad01 100644 --- a/actionpack/test/dispatch/session/cache_store_test.rb +++ b/actionpack/test/dispatch/session/cache_store_test.rb @@ -148,16 +148,15 @@ class CacheStoreTest < ActionDispatch::IntegrationTest def test_prevents_session_fixation with_test_route_set do - get '/get_session_value' - assert_response :success - assert_equal 'foo: nil', response.body - session_id = cookies['_session_id'] + assert_equal nil, @cache.read('_session_id:0xhax') - reset! + cookies['_session_id'] = '0xhax' + get '/set_session_value' - get '/set_session_value', :_session_id => session_id assert_response :success - assert_not_equal session_id, cookies['_session_id'] + assert_not_equal '0xhax', cookies['_session_id'] + assert_equal nil, @cache.read('_session_id:0xhax') + assert_equal({'foo' => 'bar'}, @cache.read("_session_id:#{cookies['_session_id']}")) end end @@ -169,8 +168,8 @@ class CacheStoreTest < ActionDispatch::IntegrationTest end @app = self.class.build_app(set) do |middleware| - cache = ActiveSupport::Cache::MemoryStore.new - middleware.use ActionDispatch::Session::CacheStore, :key => '_session_id', :cache => cache + @cache = ActiveSupport::Cache::MemoryStore.new + middleware.use ActionDispatch::Session::CacheStore, :key => '_session_id', :cache => @cache middleware.delete "ActionDispatch::ShowExceptions" end diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index 38bd234f37..323fbc285e 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -37,7 +37,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest get "/", {}, {'action_dispatch.show_exceptions' => true} assert_response 500 assert_equal "500 error fixture\n", body - + get "/bad_params", {}, {'action_dispatch.show_exceptions' => true} assert_response 400 assert_equal "400 error fixture\n", body @@ -92,6 +92,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest exceptions_app = lambda do |env| assert_kind_of AbstractController::ActionNotFound, env["action_dispatch.exception"] assert_equal "/404", env["PATH_INFO"] + assert_equal "/not_found_original_exception", env["action_dispatch.original_path"] [404, { "Content-Type" => "text/plain" }, ["YOU FAILED BRO"]] end diff --git a/actionpack/test/dispatch/template_assertions_test.rb b/actionpack/test/dispatch/template_assertions_test.rb new file mode 100644 index 0000000000..3c393f937b --- /dev/null +++ b/actionpack/test/dispatch/template_assertions_test.rb @@ -0,0 +1,98 @@ +require 'abstract_unit' + +class AssertTemplateController < ActionController::Base + def render_with_partial + render partial: 'test/partial' + end + + def render_with_template + render 'test/hello_world' + end + + def render_with_layout + @variable_for_layout = nil + render 'test/hello_world', layout: "layouts/standard" + end + + def render_with_file + render file: 'README.rdoc' + end + + def render_nothing + head :ok + end +end + +class AssertTemplateControllerTest < ActionDispatch::IntegrationTest + def test_template_reset_between_requests + get '/assert_template/render_with_template' + assert_template 'test/hello_world' + + get '/assert_template/render_nothing' + assert_template nil + end + + def test_partial_reset_between_requests + get '/assert_template/render_with_partial' + assert_template partial: 'test/_partial' + + get '/assert_template/render_nothing' + assert_template partial: nil + end + + def test_layout_reset_between_requests + get '/assert_template/render_with_layout' + assert_template layout: 'layouts/standard' + + get '/assert_template/render_nothing' + assert_template layout: nil + end + + def test_file_reset_between_requests + get '/assert_template/render_with_file' + assert_template file: 'README.rdoc' + + get '/assert_template/render_nothing' + assert_template file: nil + end + + def test_template_reset_between_requests_when_opening_a_session + open_session do |session| + session.get '/assert_template/render_with_template' + session.assert_template 'test/hello_world' + + session.get '/assert_template/render_nothing' + session.assert_template nil + end + end + + def test_partial_reset_between_requests_when_opening_a_session + open_session do |session| + session.get '/assert_template/render_with_partial' + session.assert_template partial: 'test/_partial' + + session.get '/assert_template/render_nothing' + session.assert_template partial: nil + end + end + + def test_layout_reset_between_requests_when_opening_a_session + open_session do |session| + session.get '/assert_template/render_with_layout' + session.assert_template layout: 'layouts/standard' + + session.get '/assert_template/render_nothing' + session.assert_template layout: nil + end + end + + def test_file_reset_between_requests_when_opening_a_session + open_session do |session| + session.get '/assert_template/render_with_file' + session.assert_template file: 'README.rdoc' + + session.get '/assert_template/render_nothing' + session.assert_template file: nil + end + end +end diff --git a/actionpack/test/dispatch/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb index 9f6381f118..55ebbd5143 100644 --- a/actionpack/test/dispatch/uploaded_file_test.rb +++ b/actionpack/test/dispatch/uploaded_file_test.rb @@ -18,6 +18,12 @@ module ActionDispatch assert_equal "UTF-8", uf.original_filename.encoding.to_s end + def test_filename_should_always_be_in_utf_8 + uf = Http::UploadedFile.new(:filename => 'foo'.encode(Encoding::SHIFT_JIS), + :tempfile => Object.new) + assert_equal "UTF-8", uf.original_filename.encoding.to_s + end + def test_content_type uf = Http::UploadedFile.new(:type => 'foo', :tempfile => Object.new) assert_equal 'foo', uf.content_type diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb index a4dfd0a63d..8f79e7bf9a 100644 --- a/actionpack/test/dispatch/url_generation_test.rb +++ b/actionpack/test/dispatch/url_generation_test.rb @@ -20,12 +20,14 @@ module TestUrlGeneration mount MyRouteGeneratingController.action(:index), at: '/bar' end + APP = build_app Routes + def _routes Routes end def app - Routes + APP end test "generating URLS normally" do diff --git a/actionpack/test/fixtures/respond_with/edit.html.erb b/actionpack/test/fixtures/respond_with/edit.html.erb deleted file mode 100644 index ae82dfa4fc..0000000000 --- a/actionpack/test/fixtures/respond_with/edit.html.erb +++ /dev/null @@ -1 +0,0 @@ -Edit world! diff --git a/actionpack/test/fixtures/respond_with/new.html.erb b/actionpack/test/fixtures/respond_with/new.html.erb deleted file mode 100644 index 96c8f1b88b..0000000000 --- a/actionpack/test/fixtures/respond_with/new.html.erb +++ /dev/null @@ -1 +0,0 @@ -New world! diff --git a/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb b/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb deleted file mode 100644 index bf5869ed22..0000000000 --- a/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb +++ /dev/null @@ -1 +0,0 @@ -<content>I should not be displayed</content>
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb b/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb deleted file mode 100644 index b313017913..0000000000 --- a/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb +++ /dev/null @@ -1 +0,0 @@ -<customer-name><%= @customer.name %></customer-name>
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_with/using_resource.js.erb b/actionpack/test/fixtures/respond_with/using_resource.js.erb deleted file mode 100644 index 4417680bce..0000000000 --- a/actionpack/test/fixtures/respond_with/using_resource.js.erb +++ /dev/null @@ -1 +0,0 @@ -alert("Hi");
\ No newline at end of file diff --git a/actionpack/test/fixtures/respond_with/using_resource_with_block.html.erb b/actionpack/test/fixtures/respond_with/using_resource_with_block.html.erb deleted file mode 100644 index 6769dd60bd..0000000000 --- a/actionpack/test/fixtures/respond_with/using_resource_with_block.html.erb +++ /dev/null @@ -1 +0,0 @@ -Hello world!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_changing_priority.html.erb b/actionpack/test/fixtures/test/_changing_priority.html.erb deleted file mode 100644 index 3225efc49a..0000000000 --- a/actionpack/test/fixtures/test/_changing_priority.html.erb +++ /dev/null @@ -1 +0,0 @@ -HTML
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_changing_priority.json.erb b/actionpack/test/fixtures/test/_changing_priority.json.erb deleted file mode 100644 index 7fa41dce66..0000000000 --- a/actionpack/test/fixtures/test/_changing_priority.json.erb +++ /dev/null @@ -1 +0,0 @@ -JSON
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_counter.html.erb b/actionpack/test/fixtures/test/_counter.html.erb deleted file mode 100644 index fd245bfc70..0000000000 --- a/actionpack/test/fixtures/test/_counter.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= counter_counter %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_customer.erb b/actionpack/test/fixtures/test/_customer.erb deleted file mode 100644 index d8220afeda..0000000000 --- a/actionpack/test/fixtures/test/_customer.erb +++ /dev/null @@ -1 +0,0 @@ -Hello: <%= customer.name rescue "Anonymous" %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_customer_counter.erb b/actionpack/test/fixtures/test/_customer_counter.erb deleted file mode 100644 index 3435979dba..0000000000 --- a/actionpack/test/fixtures/test/_customer_counter.erb +++ /dev/null @@ -1 +0,0 @@ -<%= customer_counter.name %><%= customer_counter_counter %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_customer_counter_with_as.erb b/actionpack/test/fixtures/test/_customer_counter_with_as.erb deleted file mode 100644 index 1241eb604d..0000000000 --- a/actionpack/test/fixtures/test/_customer_counter_with_as.erb +++ /dev/null @@ -1 +0,0 @@ -<%= client.name %><%= client_counter %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_customer_greeting.erb b/actionpack/test/fixtures/test/_customer_greeting.erb deleted file mode 100644 index 6acbcb20c4..0000000000 --- a/actionpack/test/fixtures/test/_customer_greeting.erb +++ /dev/null @@ -1 +0,0 @@ -<%= greeting %>: <%= customer_greeting.name %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_customer_with_var.erb b/actionpack/test/fixtures/test/_customer_with_var.erb deleted file mode 100644 index 00047dd20e..0000000000 --- a/actionpack/test/fixtures/test/_customer_with_var.erb +++ /dev/null @@ -1 +0,0 @@ -<%= customer.name %> <%= customer.name %> <%= customer.name %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb b/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb deleted file mode 100644 index 1cc8d41475..0000000000 --- a/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb +++ /dev/null @@ -1 +0,0 @@ -Hello <%= name %> diff --git a/actionpack/test/fixtures/test/_first_json_partial.json.erb b/actionpack/test/fixtures/test/_first_json_partial.json.erb deleted file mode 100644 index 790ee896db..0000000000 --- a/actionpack/test/fixtures/test/_first_json_partial.json.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render :partial => "test/second_json_partial" %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_form.erb b/actionpack/test/fixtures/test/_form.erb deleted file mode 100644 index 01107f1cb2..0000000000 --- a/actionpack/test/fixtures/test/_form.erb +++ /dev/null @@ -1 +0,0 @@ -<%= form.label :title %> diff --git a/actionpack/test/fixtures/test/_hash_greeting.erb b/actionpack/test/fixtures/test/_hash_greeting.erb deleted file mode 100644 index fc54a36f2a..0000000000 --- a/actionpack/test/fixtures/test/_hash_greeting.erb +++ /dev/null @@ -1 +0,0 @@ -<%= greeting %>: <%= hash_greeting[:first_name] %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_hash_object.erb b/actionpack/test/fixtures/test/_hash_object.erb deleted file mode 100644 index 34a92c6a56..0000000000 --- a/actionpack/test/fixtures/test/_hash_object.erb +++ /dev/null @@ -1,2 +0,0 @@ -<%= hash_object[:first_name] %> -<%= hash_object[:first_name].reverse %> diff --git a/actionpack/test/fixtures/test/_hello.builder b/actionpack/test/fixtures/test/_hello.builder deleted file mode 100644 index ef52f632d1..0000000000 --- a/actionpack/test/fixtures/test/_hello.builder +++ /dev/null @@ -1 +0,0 @@ -xm.hello
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_json_change_priority.json.erb b/actionpack/test/fixtures/test/_json_change_priority.json.erb deleted file mode 100644 index e69de29bb2..0000000000 --- a/actionpack/test/fixtures/test/_json_change_priority.json.erb +++ /dev/null diff --git a/actionpack/test/fixtures/test/_labelling_form.erb b/actionpack/test/fixtures/test/_labelling_form.erb deleted file mode 100644 index 1b95763165..0000000000 --- a/actionpack/test/fixtures/test/_labelling_form.erb +++ /dev/null @@ -1 +0,0 @@ -<%= labelling_form.label :title %> diff --git a/actionpack/test/fixtures/test/_layout_for_partial.html.erb b/actionpack/test/fixtures/test/_layout_for_partial.html.erb deleted file mode 100644 index 666efadbb6..0000000000 --- a/actionpack/test/fixtures/test/_layout_for_partial.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -Before (<%= name %>) -<%= yield %> -After
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_partial_for_use_in_layout.html.erb b/actionpack/test/fixtures/test/_partial_for_use_in_layout.html.erb deleted file mode 100644 index 3a03a64e31..0000000000 --- a/actionpack/test/fixtures/test/_partial_for_use_in_layout.html.erb +++ /dev/null @@ -1 +0,0 @@ -Inside from partial (<%= name %>)
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_partial_html_erb.html.erb b/actionpack/test/fixtures/test/_partial_html_erb.html.erb deleted file mode 100644 index 4b54875782..0000000000 --- a/actionpack/test/fixtures/test/_partial_html_erb.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= "partial.html.erb" %> diff --git a/actionpack/test/fixtures/test/_partial_name_local_variable.erb b/actionpack/test/fixtures/test/_partial_name_local_variable.erb deleted file mode 100644 index cc3a91c89f..0000000000 --- a/actionpack/test/fixtures/test/_partial_name_local_variable.erb +++ /dev/null @@ -1 +0,0 @@ -<%= partial_name_local_variable %> diff --git a/actionpack/test/fixtures/test/_partial_only.erb b/actionpack/test/fixtures/test/_partial_only.erb deleted file mode 100644 index a44b3eed40..0000000000 --- a/actionpack/test/fixtures/test/_partial_only.erb +++ /dev/null @@ -1 +0,0 @@ -only partial
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_partial_only_html.html b/actionpack/test/fixtures/test/_partial_only_html.html deleted file mode 100644 index d2d630bd40..0000000000 --- a/actionpack/test/fixtures/test/_partial_only_html.html +++ /dev/null @@ -1 +0,0 @@ -only html partial
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_partial_with_partial.erb b/actionpack/test/fixtures/test/_partial_with_partial.erb deleted file mode 100644 index ee0d5037b6..0000000000 --- a/actionpack/test/fixtures/test/_partial_with_partial.erb +++ /dev/null @@ -1,2 +0,0 @@ -<%= render 'test/partial' %> -partial with partial diff --git a/actionpack/test/fixtures/test/_person.erb b/actionpack/test/fixtures/test/_person.erb deleted file mode 100644 index b2e5688956..0000000000 --- a/actionpack/test/fixtures/test/_person.erb +++ /dev/null @@ -1,2 +0,0 @@ -Second: <%= name %> -Third: <%= @name %> diff --git a/actionpack/test/fixtures/test/_raise_indentation.html.erb b/actionpack/test/fixtures/test/_raise_indentation.html.erb deleted file mode 100644 index f9a93728fe..0000000000 --- a/actionpack/test/fixtures/test/_raise_indentation.html.erb +++ /dev/null @@ -1,13 +0,0 @@ -<p>First paragraph</p> -<p>Second paragraph</p> -<p>Third paragraph</p> -<p>Fourth paragraph</p> -<p>Fifth paragraph</p> -<p>Sixth paragraph</p> -<p>Seventh paragraph</p> -<p>Eight paragraph</p> -<p>Ninth paragraph</p> -<p>Tenth paragraph</p> -<%= raise "error here!" %> -<p>Eleventh paragraph</p> -<p>Twelfth paragraph</p>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_second_json_partial.json.erb b/actionpack/test/fixtures/test/_second_json_partial.json.erb deleted file mode 100644 index 5ebb7f1afd..0000000000 --- a/actionpack/test/fixtures/test/_second_json_partial.json.erb +++ /dev/null @@ -1 +0,0 @@ -Third level
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/action_talk_to_layout.erb b/actionpack/test/fixtures/test/action_talk_to_layout.erb deleted file mode 100644 index 36e896daa8..0000000000 --- a/actionpack/test/fixtures/test/action_talk_to_layout.erb +++ /dev/null @@ -1,2 +0,0 @@ -<% @title = "Talking to the layout" -%> -Action was here!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/calling_partial_with_layout.html.erb b/actionpack/test/fixtures/test/calling_partial_with_layout.html.erb deleted file mode 100644 index ac44bc0d81..0000000000 --- a/actionpack/test/fixtures/test/calling_partial_with_layout.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render(:layout => "layout_for_partial", :partial => "partial_for_use_in_layout", :locals => { :name => "David" }) %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/capturing.erb b/actionpack/test/fixtures/test/capturing.erb deleted file mode 100644 index 1addaa40d9..0000000000 --- a/actionpack/test/fixtures/test/capturing.erb +++ /dev/null @@ -1,4 +0,0 @@ -<% days = capture do %> - Dreamy days -<% end %> -<%= days %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/change_priority.html.erb b/actionpack/test/fixtures/test/change_priority.html.erb deleted file mode 100644 index 5618977d05..0000000000 --- a/actionpack/test/fixtures/test/change_priority.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -<%= render :partial => "test/json_change_priority", formats: :json %> -HTML Template, but <%= render :partial => "test/changing_priority" %> partial
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/content_for.erb b/actionpack/test/fixtures/test/content_for.erb deleted file mode 100644 index 1fb829f54c..0000000000 --- a/actionpack/test/fixtures/test/content_for.erb +++ /dev/null @@ -1 +0,0 @@ -<% content_for :title do -%>Putting stuff in the title!<% end -%>Great stuff!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/content_for_concatenated.erb b/actionpack/test/fixtures/test/content_for_concatenated.erb deleted file mode 100644 index e65f629574..0000000000 --- a/actionpack/test/fixtures/test/content_for_concatenated.erb +++ /dev/null @@ -1,3 +0,0 @@ -<% content_for :title, "Putting stuff " - content_for :title, "in the title!" -%> -Great stuff!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/content_for_with_parameter.erb b/actionpack/test/fixtures/test/content_for_with_parameter.erb deleted file mode 100644 index aeb6f73ce0..0000000000 --- a/actionpack/test/fixtures/test/content_for_with_parameter.erb +++ /dev/null @@ -1,2 +0,0 @@ -<% content_for :title, "Putting stuff in the title!" -%> -Great stuff!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/formatted_html_erb.html.erb b/actionpack/test/fixtures/test/formatted_html_erb.html.erb deleted file mode 100644 index 1c64efabd8..0000000000 --- a/actionpack/test/fixtures/test/formatted_html_erb.html.erb +++ /dev/null @@ -1 +0,0 @@ -formatted html erb
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/greeting.html.erb b/actionpack/test/fixtures/test/greeting.html.erb deleted file mode 100644 index 62fb0293f0..0000000000 --- a/actionpack/test/fixtures/test/greeting.html.erb +++ /dev/null @@ -1 +0,0 @@ -<p>This is grand!</p> diff --git a/actionpack/test/fixtures/test/greeting.xml.erb b/actionpack/test/fixtures/test/greeting.xml.erb deleted file mode 100644 index 62fb0293f0..0000000000 --- a/actionpack/test/fixtures/test/greeting.xml.erb +++ /dev/null @@ -1 +0,0 @@ -<p>This is grand!</p> diff --git a/actionpack/test/fixtures/test/hello,world.erb b/actionpack/test/fixtures/test/hello,world.erb deleted file mode 100644 index bc8fa5e0ca..0000000000 --- a/actionpack/test/fixtures/test/hello,world.erb +++ /dev/null @@ -1 +0,0 @@ -Hello w*rld!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/hello.builder b/actionpack/test/fixtures/test/hello.builder deleted file mode 100644 index a471553941..0000000000 --- a/actionpack/test/fixtures/test/hello.builder +++ /dev/null @@ -1,4 +0,0 @@ -xml.html do - xml.p "Hello #{@name}" - xml << render(:file => "test/greeting") -end
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/hello_world_container.builder b/actionpack/test/fixtures/test/hello_world_container.builder deleted file mode 100644 index e48d75c405..0000000000 --- a/actionpack/test/fixtures/test/hello_world_container.builder +++ /dev/null @@ -1,3 +0,0 @@ -xml.test do - render :partial => 'hello', :locals => { :xm => xml } -end
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/hello_world_from_rxml.builder b/actionpack/test/fixtures/test/hello_world_from_rxml.builder deleted file mode 100644 index 619a97ba96..0000000000 --- a/actionpack/test/fixtures/test/hello_world_from_rxml.builder +++ /dev/null @@ -1,3 +0,0 @@ -xml.html do - xml.p "Hello" -end diff --git a/actionpack/test/fixtures/test/hello_world_with_layout_false.erb b/actionpack/test/fixtures/test/hello_world_with_layout_false.erb deleted file mode 100644 index 6769dd60bd..0000000000 --- a/actionpack/test/fixtures/test/hello_world_with_layout_false.erb +++ /dev/null @@ -1 +0,0 @@ -Hello world!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/html_template.html.erb b/actionpack/test/fixtures/test/html_template.html.erb deleted file mode 100644 index 1bbc2b7f09..0000000000 --- a/actionpack/test/fixtures/test/html_template.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render :partial => "test/first_json_partial", formats: :json %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/hyphen-ated.erb b/actionpack/test/fixtures/test/hyphen-ated.erb deleted file mode 100644 index cd0875583a..0000000000 --- a/actionpack/test/fixtures/test/hyphen-ated.erb +++ /dev/null @@ -1 +0,0 @@ -Hello world! diff --git a/actionpack/test/fixtures/test/list.erb b/actionpack/test/fixtures/test/list.erb deleted file mode 100644 index 0a4bda58ee..0000000000 --- a/actionpack/test/fixtures/test/list.erb +++ /dev/null @@ -1 +0,0 @@ -<%= @test_unchanged = 'goodbye' %><%= render :partial => 'customer', :collection => @customers %><%= @test_unchanged %> diff --git a/actionpack/test/fixtures/test/non_erb_block_content_for.builder b/actionpack/test/fixtures/test/non_erb_block_content_for.builder deleted file mode 100644 index d539a425a4..0000000000 --- a/actionpack/test/fixtures/test/non_erb_block_content_for.builder +++ /dev/null @@ -1,4 +0,0 @@ -content_for :title do - 'Putting stuff in the title!' -end -xml << "Great stuff!" diff --git a/actionpack/test/fixtures/test/potential_conflicts.erb b/actionpack/test/fixtures/test/potential_conflicts.erb deleted file mode 100644 index a5e964e359..0000000000 --- a/actionpack/test/fixtures/test/potential_conflicts.erb +++ /dev/null @@ -1,4 +0,0 @@ -First: <%= @name %> -<%= render :partial => "person", :locals => { :name => "Stephan" } -%> -Fourth: <%= @name %> -Fifth: <%= name %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/proper_block_detection.erb b/actionpack/test/fixtures/test/proper_block_detection.erb deleted file mode 100644 index b55efbb25d..0000000000 --- a/actionpack/test/fixtures/test/proper_block_detection.erb +++ /dev/null @@ -1 +0,0 @@ -<%= @todo %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/render_file_from_template.html.erb b/actionpack/test/fixtures/test/render_file_from_template.html.erb deleted file mode 100644 index fde9f4bb64..0000000000 --- a/actionpack/test/fixtures/test/render_file_from_template.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render :file => @path %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/render_file_with_locals_and_default.erb b/actionpack/test/fixtures/test/render_file_with_locals_and_default.erb deleted file mode 100644 index 9b4900acc5..0000000000 --- a/actionpack/test/fixtures/test/render_file_with_locals_and_default.erb +++ /dev/null @@ -1 +0,0 @@ -<%= secret ||= 'one' %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.da.html.erb b/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.da.html.erb deleted file mode 100644 index 0740b2d07c..0000000000 --- a/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.da.html.erb +++ /dev/null @@ -1 +0,0 @@ -Hey HTML! diff --git a/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb b/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb deleted file mode 100644 index 4a11845cfe..0000000000 --- a/actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb +++ /dev/null @@ -1 +0,0 @@ -Hello HTML!
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/render_implicit_js_template_without_layout.js.erb b/actionpack/test/fixtures/test/render_implicit_js_template_without_layout.js.erb deleted file mode 100644 index 892ae5eca2..0000000000 --- a/actionpack/test/fixtures/test/render_implicit_js_template_without_layout.js.erb +++ /dev/null @@ -1 +0,0 @@ -alert('hello');
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb b/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb deleted file mode 100644 index 1461b95186..0000000000 --- a/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render partial: 'test/_directory/partial_with_locales', locals: {'name' => 'Jane'} %> diff --git a/actionpack/test/fixtures/test/render_to_string_test.erb b/actionpack/test/fixtures/test/render_to_string_test.erb deleted file mode 100644 index 6e267e8634..0000000000 --- a/actionpack/test/fixtures/test/render_to_string_test.erb +++ /dev/null @@ -1 +0,0 @@ -The value of foo is: ::<%= @foo %>:: diff --git a/actionpack/test/fixtures/test/render_two_partials.html.erb b/actionpack/test/fixtures/test/render_two_partials.html.erb deleted file mode 100644 index 3db6025860..0000000000 --- a/actionpack/test/fixtures/test/render_two_partials.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -<%= render :partial => 'partial', :locals => {'first' => '1'} %> -<%= render :partial => 'partial', :locals => {'second' => '2'} %> diff --git a/actionpack/test/fixtures/test/using_layout_around_block.html.erb b/actionpack/test/fixtures/test/using_layout_around_block.html.erb deleted file mode 100644 index 3d6661df9a..0000000000 --- a/actionpack/test/fixtures/test/using_layout_around_block.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render(:layout => "layout_for_partial", :locals => { :name => "David" }) do %>Inside from block<% end %>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/with_html_partial.html.erb b/actionpack/test/fixtures/test/with_html_partial.html.erb deleted file mode 100644 index d84d909d64..0000000000 --- a/actionpack/test/fixtures/test/with_html_partial.html.erb +++ /dev/null @@ -1 +0,0 @@ -<strong><%= render :partial => "partial_only_html" %></strong> diff --git a/actionpack/test/fixtures/test/with_partial.html.erb b/actionpack/test/fixtures/test/with_partial.html.erb deleted file mode 100644 index 7502364cf5..0000000000 --- a/actionpack/test/fixtures/test/with_partial.html.erb +++ /dev/null @@ -1 +0,0 @@ -<strong><%= render :partial => "partial_only" %></strong> diff --git a/actionpack/test/fixtures/test/with_partial.text.erb b/actionpack/test/fixtures/test/with_partial.text.erb deleted file mode 100644 index 5f068ebf27..0000000000 --- a/actionpack/test/fixtures/test/with_partial.text.erb +++ /dev/null @@ -1 +0,0 @@ -**<%= render :partial => "partial_only" %>** diff --git a/actionpack/test/fixtures/test/with_xml_template.html.erb b/actionpack/test/fixtures/test/with_xml_template.html.erb deleted file mode 100644 index e54a7cd001..0000000000 --- a/actionpack/test/fixtures/test/with_xml_template.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render :template => "test/greeting", :formats => :xml %> diff --git a/actionpack/test/journey/router/utils_test.rb b/actionpack/test/journey/router/utils_test.rb index 584fd56a5c..9b2b85ec73 100644 --- a/actionpack/test/journey/router/utils_test.rb +++ b/actionpack/test/journey/router/utils_test.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'abstract_unit' module ActionDispatch @@ -20,6 +21,10 @@ module ActionDispatch assert_equal "a/b c+d", Utils.unescape_uri("a%2Fb%20c+d") end + def test_uri_unescape_with_utf8_string + assert_equal "Šašinková", Utils.unescape_uri("%C5%A0a%C5%A1inkov%C3%A1".force_encoding(Encoding::US_ASCII)) + end + def test_normalize_path_not_greedy assert_equal "/foo%20bar%20baz", Utils.normalize_path("/foo%20bar%20baz") end diff --git a/actionpack/test/routing/helper_test.rb b/actionpack/test/routing/helper_test.rb index 0028aaa629..09ca7ff73b 100644 --- a/actionpack/test/routing/helper_test.rb +++ b/actionpack/test/routing/helper_test.rb @@ -26,6 +26,20 @@ module ActionDispatch x.new.pond_duck_path Duck.new end end + + def test_path_deprecation + rs = ::ActionDispatch::Routing::RouteSet.new + rs.draw do + resources :ducks + end + + x = Class.new { + include rs.url_helpers(false) + } + assert_deprecated do + assert_equal '/ducks', x.new.ducks_path + end + end end end end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 03ac155848..552a902349 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,36 @@ +* Add I18n support for input/textarea placeholder text. + + Placeholder I18n follows the same convention as `label` I18n. + + *Alex Robbin* + +* Fix that render layout: 'messages/layout' should also be added to the dependency tracker tree. + + *DHH* + +* Add `PartialIteration` object used when rendering collections. + + The iteration object is available as the local variable + `#{template_name}_iteration` when rendering partials with collections. + + It gives access to the `size` of the collection being iterated over, + the current `index` and two convenience methods `first?` and `last?`. + + *Joel Junström*, *Lucas Uyezu* + +* Return an absolute instead of relative path from an asset url in the case + of the `asset_host` proc returning nil + + *Jolyon Pawlyn* + +* Fix `html_escape_once` to properly handle hex escape sequences (e.g. ᨫ) + + *John F. Douthat* + +* Added String support for min and max properties for date field helpers. + + *Todd Bealmear* + * The `highlight` helper now accepts a block to be used instead of the `highlighter` option. @@ -88,7 +121,7 @@ * Remove wrapping div with inline styles for hidden form fields. We are dropping HTML 4.01 and XHTML strict compliance since input tags directly - inside a form are valid HTML5, and the absense of inline styles help in validating + inside a form are valid HTML5, and the absence of inline styles help in validating for Content Security Policy. *Joost Baaij* diff --git a/actionview/RUNNING_UNIT_TESTS.rdoc b/actionview/RUNNING_UNIT_TESTS.rdoc index c408882827..6c4e5e983a 100644 --- a/actionview/RUNNING_UNIT_TESTS.rdoc +++ b/actionview/RUNNING_UNIT_TESTS.rdoc @@ -19,8 +19,8 @@ which can be further narrowed down to one test: == Dependency on Active Record and database setup Test cases in the test/activerecord/ directory depend on having -activerecord and sqlite installed. If Active Record is not in -actionview/../activerecord directory, or the sqlite rubygem is not installed, +activerecord and sqlite3 installed. If Active Record is not in +actionview/../activerecord directory, or the sqlite3 rubygem is not installed, these tests are skipped. Other tests are runnable from a fresh copy of actionview without any configuration. diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index e45dd04225..1ea00cff22 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -23,6 +23,7 @@ Gem::Specification.new do |s| s.add_dependency 'builder', '~> 3.1' s.add_dependency 'erubis', '~> 2.7.0' + s.add_dependency 'rails-deprecated_sanitizer' s.add_development_dependency 'actionpack', version s.add_development_dependency 'activemodel', version diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index 50712e0830..6a1837c6e2 100644 --- a/actionview/lib/action_view.rb +++ b/actionview/lib/action_view.rb @@ -86,7 +86,6 @@ module ActionView super ActionView::Helpers.eager_load! ActionView::Template.eager_load! - HTML.eager_load! end end diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index 900f96255e..86c55ffb51 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -66,15 +66,6 @@ module ActionView #:nodoc: # Headline: <%= headline %> # First name: <%= person.first_name %> # - # If you need to find out whether a certain local variable has been assigned a value in a particular render call, - # you need to use the following pattern: - # - # <% if local_assigns.has_key? :headline %> - # Headline: <%= headline %> - # <% end %> - # - # Testing using <tt>defined? headline</tt> will not work. This is an implementation restriction. - # # === Template caching # # By default, Rails will compile each template to a method in order to render it. When you alter a template, diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb index 0ccf2515c5..e34bdd4a46 100644 --- a/actionview/lib/action_view/dependency_tracker.rb +++ b/actionview/lib/action_view/dependency_tracker.rb @@ -53,6 +53,12 @@ module ActionView \s* # followed by optional spaces /x + # Part of any hash containing the :layout key + LAYOUT_HASH_KEY = / + (?:\blayout:|:layout\s*=>) # layout key in either old or new style hash syntax + \s* # followed by optional spaces + /x + # Matches: # partial: "comments/comment", collection: @all_comments => "comments/comment" # (object: @single_comment, partial: "comments/comment") => "comments/comment" @@ -65,9 +71,9 @@ module ActionView # topics => "topics/topic" # (message.topics) => "topics/topic" RENDER_ARGUMENTS = /\A - (?:\s*\(?\s*) # optional opening paren surrounded by spaces - (?:.*?#{PARTIAL_HASH_KEY})? # optional hash, up to the partial key declaration - (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest + (?:\s*\(?\s*) # optional opening paren surrounded by spaces + (?:.*?#{PARTIAL_HASH_KEY}|#{LAYOUT_HASH_KEY})? # optional hash, up to the partial or layout key declaration + (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest /xm def self.call(name, template) @@ -85,8 +91,8 @@ module ActionView attr_reader :name, :template private :name, :template - private + private def source template.source end diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 72d79735ae..1f103786cb 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -60,7 +60,7 @@ module ActionView def digest Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest| - logger.try :info, " Cache digest for #{template.inspect}: #{digest}" + logger.try :debug, " Cache digest for #{template.inspect}: #{digest}" end rescue ActionView::MissingTemplate logger.try :error, " Couldn't find template for digesting: #{name}" diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index 4a682ce4e2..9e8d005ec7 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -203,7 +203,6 @@ module ActionView request = self.request if respond_to?(:request) host = options[:host] host ||= config.asset_host if defined? config.asset_host - host ||= request.base_url if request && options[:protocol] == :request if host.respond_to?(:call) arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity @@ -214,6 +213,7 @@ module ActionView host = host % (Zlib.crc32(source) % 4) end + host ||= request.base_url if request && options[:protocol] == :request return unless host if host =~ URI_REGEXP @@ -231,7 +231,7 @@ module ActionView end end - # Computes the path to a javascript asset in the public javascripts directory. + # Computes the path to a JavaScript asset in the public javascripts directory. # If the +source+ filename has no extension, .js will be appended (except for explicit URIs) # Full paths from the document root will be passed through. # Used internally by +javascript_include_tag+ to build the script path. @@ -246,7 +246,7 @@ module ActionView end alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route - # Computes the full URL to a javascript asset in the public javascripts directory. + # Computes the full URL to a JavaScript asset in the public javascripts directory. # This will use +javascript_path+ internally, so most of their behaviors will be the same. def javascript_url(source, options = {}) url_to_asset(source, {type: :javascript}.merge!(options)) diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 2efb9612ac..27c7a26098 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -153,8 +153,8 @@ module ActionView # # Note that you cannot pass a <tt>Numeric</tt> value to <tt>time_ago_in_words</tt>. # - def time_ago_in_words(from_time, include_seconds_or_options = {}) - distance_of_time_in_words(from_time, Time.now, include_seconds_or_options) + def time_ago_in_words(from_time, options = {}) + distance_of_time_in_words(from_time, Time.now, options) end alias_method :distance_of_time_in_words_to_now, :time_ago_in_words diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index bd8e3b1973..0582cb3e12 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -482,7 +482,7 @@ module ActionView # Admin? : <%= permission_fields.check_box :admin %> # <% end %> # - # <%= f.submit %> + # <%= person_form.submit %> # <% end %> # # In this case, the checkbox field will be represented by an HTML +input+ @@ -1013,6 +1013,18 @@ module ActionView # date_field("user", "born_on", value: "1984-05-12") # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-05-12" /> # + # You can create values for the "min" and "max" attributes by passing + # instances of Date or Time to the options hash. + # + # date_field("user", "born_on", min: Date.today) + # # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" /> + # + # Alternatively, you can pass a String formatted as an ISO8601 date as the + # values for "min" and "max." + # + # date_field("user", "born_on", min: "2014-05-20") + # # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" /> + # def date_field(object_name, method, options = {}) Tags::DateField.new(object_name, method, self, options).render end @@ -1030,6 +1042,18 @@ module ActionView # time_field("task", "started_at") # # => <input id="task_started_at" name="task[started_at]" type="time" /> # + # You can create values for the "min" and "max" attributes by passing + # instances of Date or Time to the options hash. + # + # time_field("task", "started_at", min: Time.now) + # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" /> + # + # Alternatively, you can pass a String formatted as an ISO8601 time as the + # values for "min" and "max." + # + # time_field("task", "started_at", min: "01:00:00") + # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" /> + # def time_field(object_name, method, options = {}) Tags::TimeField.new(object_name, method, self, options).render end @@ -1047,6 +1071,18 @@ module ActionView # datetime_field("user", "born_on") # # => <input id="user_born_on" name="user[born_on]" type="datetime" value="1984-01-12T00:00:00.000+0000" /> # + # You can create values for the "min" and "max" attributes by passing + # instances of Date or Time to the options hash. + # + # datetime_field("user", "born_on", min: Date.today) + # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" /> + # + # Alternatively, you can pass a String formatted as an ISO8601 datetime + # with UTC offset as the values for "min" and "max." + # + # datetime_field("user", "born_on", min: "2014-05-20T00:00:00+0000") + # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" /> + # def datetime_field(object_name, method, options = {}) Tags::DatetimeField.new(object_name, method, self, options).render end @@ -1064,6 +1100,18 @@ module ActionView # datetime_local_field("user", "born_on") # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" value="1984-01-12T00:00:00" /> # + # You can create values for the "min" and "max" attributes by passing + # instances of Date or Time to the options hash. + # + # datetime_local_field("user", "born_on", min: Date.today) + # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" /> + # + # Alternatively, you can pass a String formatted as an ISO8601 datetime as + # the values for "min" and "max." + # + # datetime_local_field("user", "born_on", min: "2014-05-20T00:00:00") + # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" /> + # def datetime_local_field(object_name, method, options = {}) Tags::DatetimeLocalField.new(object_name, method, self, options).render end diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index 48f42947db..8ade7c6a74 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -14,81 +14,81 @@ module ActionView # # * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element. # - # select("post", "category", Post::CATEGORIES, {include_blank: true}) + # select("post", "category", Post::CATEGORIES, {include_blank: true}) # - # could become: + # could become: # - # <select name="post[category]"> - # <option></option> - # <option>joke</option> - # <option>poem</option> - # </select> + # <select name="post[category]"> + # <option></option> + # <option>joke</option> + # <option>poem</option> + # </select> # - # Another common case is a select tag for a <tt>belongs_to</tt>-associated object. + # Another common case is a select tag for a <tt>belongs_to</tt>-associated object. # - # Example with @post.person_id => 2: + # Example with <tt>@post.person_id => 2</tt>: # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'}) + # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'}) # - # could become: + # could become: # - # <select name="post[person_id]"> - # <option value="">None</option> - # <option value="1">David</option> - # <option value="2" selected="selected">Sam</option> - # <option value="3">Tobias</option> - # </select> + # <select name="post[person_id]"> + # <option value="">None</option> + # <option value="1">David</option> + # <option value="2" selected="selected">Sam</option> + # <option value="3">Tobias</option> + # </select> # # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string. # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'}) + # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'}) # - # could become: + # could become: # - # <select name="post[person_id]"> - # <option value="">Select Person</option> - # <option value="1">David</option> - # <option value="2">Sam</option> - # <option value="3">Tobias</option> - # </select> + # <select name="post[person_id]"> + # <option value="">Select Person</option> + # <option value="1">David</option> + # <option value="2">Sam</option> + # <option value="3">Tobias</option> + # </select> # - # Like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this - # option to be in the +html_options+ parameter. + # * <tt>:index</tt> - like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this + # option to be in the +html_options+ parameter. # - # select("album[]", "genre", %w[rap rock country], {}, { index: nil }) + # select("album[]", "genre", %w[rap rock country], {}, { index: nil }) # - # becomes: + # becomes: # - # <select name="album[][genre]" id="album__genre"> - # <option value="rap">rap</option> - # <option value="rock">rock</option> - # <option value="country">country</option> - # </select> + # <select name="album[][genre]" id="album__genre"> + # <option value="rap">rap</option> + # <option value="rock">rock</option> + # <option value="country">country</option> + # </select> # # * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output. # - # select("post", "category", Post::CATEGORIES, {disabled: 'restricted'}) + # select("post", "category", Post::CATEGORIES, {disabled: 'restricted'}) # - # could become: + # could become: # - # <select name="post[category]"> - # <option></option> - # <option>joke</option> - # <option>poem</option> - # <option disabled="disabled">restricted</option> - # </select> + # <select name="post[category]"> + # <option></option> + # <option>joke</option> + # <option>poem</option> + # <option disabled="disabled">restricted</option> + # </select> # - # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled. + # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled. # - # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: lambda{|category| category.archived? }}) + # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: lambda{|category| category.archived? }}) # - # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return: - # <select name="post[category_id]"> - # <option value="1" disabled="disabled">2008 stuff</option> - # <option value="2" disabled="disabled">Christmas</option> - # <option value="3">Jokes</option> - # <option value="4">Poems</option> - # </select> + # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return: + # <select name="post[category_id]"> + # <option value="1" disabled="disabled">2008 stuff</option> + # <option value="2" disabled="disabled">Christmas</option> + # <option value="3">Jokes</option> + # <option value="4">Poems</option> + # </select> # module FormOptionsHelper # ERB::Util can mask some helpers like textilize. Make sure to include them. @@ -152,11 +152,9 @@ module ActionView # To prevent this the helper generates an auxiliary hidden field before # every multiple select. The hidden field has the same name as multiple select and blank value. # - # This way, the client either sends only the hidden field (representing - # the deselected multiple select box), or both fields. Since the HTML specification - # says key/value pairs have to be sent in the same order they appear in the - # form, and parameters extraction gets the last occurrence of any repeated - # key in the query string, that works for ordinary forms. + # <b>Note:</b> The client either sends only the hidden field (representing + # the deselected multiple select box), or both fields. This means that the resulting array + # always contains a blank string. # # In case if you don't want the helper to generate this hidden field you can specify # <tt>include_hidden: false</tt> option. @@ -463,21 +461,7 @@ module ActionView end # Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but - # wraps them with <tt><optgroup></tt> tags. - # - # Parameters: - # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the - # <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a - # nested array of text-value pairs. See <tt>options_for_select</tt> for more info. - # Ex. ["North America",[["United States","US"],["Canada","CA"]]] - # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags, - # which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options - # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>. - # - # Options: - # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this - # prepends an option with a generic prompt - "Please select" - or the given prompt string. - # * <tt>:divider</tt> - the divider for the options groups. + # wraps them with <tt><optgroup></tt> tags: # # grouped_options = [ # ['North America', @@ -504,22 +488,36 @@ module ActionView # <option value="France">France</option> # </optgroup> # - # grouped_options = [ - # [['United States','US'], 'Canada'], - # ['Denmark','Germany','France'] - # ] - # grouped_options_for_select(grouped_options, nil, divider: '---------') + # Parameters: + # * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the + # <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a + # nested array of text-value pairs. See <tt>options_for_select</tt> for more info. + # Ex. ["North America",[["United States","US"],["Canada","CA"]]] + # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags, + # which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options + # as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>. # - # Possible output: - # <optgroup label="---------"> - # <option value="US">United States</option> - # <option value="Canada">Canada</option> - # </optgroup> - # <optgroup label="---------"> - # <option value="Denmark">Denmark</option> - # <option value="Germany">Germany</option> - # <option value="France">France</option> - # </optgroup> + # Options: + # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this + # prepends an option with a generic prompt - "Please select" - or the given prompt string. + # * <tt>:divider</tt> - the divider for the options groups. + # + # grouped_options = [ + # [['United States','US'], 'Canada'], + # ['Denmark','Germany','France'] + # ] + # grouped_options_for_select(grouped_options, nil, divider: '---------') + # + # Possible output: + # <optgroup label="---------"> + # <option value="US">United States</option> + # <option value="Canada">Canada</option> + # </optgroup> + # <optgroup label="---------"> + # <option value="Denmark">Denmark</option> + # <option value="Germany">Germany</option> + # <option value="France">France</option> + # </optgroup> # # <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to # wrap the output in an appropriate <tt><select></tt> tag. diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb index b0d9c7c7f9..f03362d0f5 100644 --- a/actionview/lib/action_view/helpers/output_safety_helper.rb +++ b/actionview/lib/action_view/helpers/output_safety_helper.rb @@ -17,7 +17,7 @@ module ActionView #:nodoc: stringish.to_s.html_safe end - # This method returns a html safe string similar to what <tt>Array#join</tt> + # This method returns an html safe string similar to what <tt>Array#join</tt> # would return. The array is flattened, and all items, including # the supplied separator, are html escaped unless they are html # safe, and the returned string is marked as html safe. diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index ebfc35a4c7..6cd6e858dd 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -13,13 +13,13 @@ module ActionView # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller. # * <tt>:text</tt> - Renders the text passed in out. # * <tt>:plain</tt> - Renders the text passed in out. Setting the content - # type as <tt>text/plain</tt>. + # type as <tt>text/plain</tt>. # * <tt>:html</tt> - Renders the html safe string passed in out, otherwise - # performs html escape on the string first. Setting the content type as - # <tt>text/html</tt>. + # performs html escape on the string first. Setting the content type as + # <tt>text/html</tt>. # * <tt>:body</tt> - Renders the text passed in, and inherits the content - # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt> - # object. + # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt> + # object. # # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter # as the locals hash. diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index 049af275b6..153c64d691 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/object/try' -require 'action_view/vendor/html-scanner' +require 'active_support/deprecation' +require 'rails-deprecated_sanitizer' module ActionView # = Action View Sanitize Helpers @@ -27,7 +28,29 @@ module ActionView # # <%= sanitize @article.body %> # - # Custom Use (only the mentioned tags and attributes are allowed, nothing else) + # Custom Use - Custom Scrubber + # (supply a Loofah::Scrubber that does the sanitization) + # + # scrubber can either wrap a block: + # scrubber = Loofah::Scrubber.new do |node| + # node.text = "dawn of cats" + # end + # + # or be a subclass of Loofah::Scrubber which responds to scrub: + # class KittyApocalypse < Loofah::Scrubber + # def scrub(node) + # node.text = "dawn of cats" + # end + # end + # scrubber = KittyApocalypse.new + # + # <%= sanitize @article.body, scrubber: scrubber %> + # + # A custom scrubber takes precedence over custom tags and attributes + # Learn more about scrubbers here: https://github.com/flavorjones/loofah + # + # Custom Use - tags and attributes + # (only the mentioned tags and attributes are allowed, nothing else) # # <%= sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) %> # @@ -65,9 +88,9 @@ module ActionView self.class.white_list_sanitizer.sanitize_css(style) end - # Strips all HTML tags from the +html+, including comments. This uses the - # html-scanner tokenizer and so its HTML parsing ability is limited by - # that of html-scanner. + # Strips all HTML tags from the +html+, including comments. This uses + # Nokogiri for tokenization (via Loofah) and so its HTML parsing ability + # is limited by that of Nokogiri. # # strip_tags("Strip <i>these</i> tags!") # # => Strip these tags! @@ -98,47 +121,42 @@ module ActionView module ClassMethods #:nodoc: attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer - def sanitized_protocol_separator - white_list_sanitizer.protocol_separator - end + [:protocol_separator, + :uri_attributes, + :bad_tags, + :allowed_css_properties, + :allowed_css_keywords, + :shorthand_css_properties, + :allowed_protocols].each do |meth| + meth_name = "sanitized_#{meth}" + imp = lambda do |name| + ActiveSupport::Deprecation.warn("#{name} is deprecated and has no effect.") + end - def sanitized_uri_attributes - white_list_sanitizer.uri_attributes + define_method(meth_name) { imp.(meth_name) } + define_method("#{meth_name}=") { |value| imp.("#{meth_name}=") } end - def sanitized_bad_tags - white_list_sanitizer.bad_tags + # Vendors the full, link and white list sanitizers. + # This uses html-scanner for the HTML sanitization. + # In the next Rails version this will use Rails::Html::Sanitizer instead. + # To get this new behavior now, in your Gemfile, add: + # + # gem 'rails-html-sanitizer' + # + def sanitizer_vendor + Rails::DeprecatedSanitizer end def sanitized_allowed_tags - white_list_sanitizer.allowed_tags + sanitizer_vendor.white_list_sanitizer.allowed_tags end def sanitized_allowed_attributes - white_list_sanitizer.allowed_attributes - end - - def sanitized_allowed_css_properties - white_list_sanitizer.allowed_css_properties - end - - def sanitized_allowed_css_keywords - white_list_sanitizer.allowed_css_keywords - end - - def sanitized_shorthand_css_properties - white_list_sanitizer.shorthand_css_properties + sanitizer_vendor.white_list_sanitizer.allowed_attributes end - def sanitized_allowed_protocols - white_list_sanitizer.allowed_protocols - end - - def sanitized_protocol_separator=(value) - white_list_sanitizer.protocol_separator = value - end - - # Gets the HTML::FullSanitizer instance used by +strip_tags+. Replace with + # Gets the Rails::Html::FullSanitizer instance used by +strip_tags+. Replace with # any object that responds to +sanitize+. # # class Application < Rails::Application @@ -146,21 +164,21 @@ module ActionView # end # def full_sanitizer - @full_sanitizer ||= HTML::FullSanitizer.new + @full_sanitizer ||= sanitizer_vendor.full_sanitizer.new end - # Gets the HTML::LinkSanitizer instance used by +strip_links+. Replace with - # any object that responds to +sanitize+. + # Gets the Rails::Html::LinkSanitizer instance used by +strip_links+. + # Replace with any object that responds to +sanitize+. # # class Application < Rails::Application # config.action_view.link_sanitizer = MySpecialSanitizer.new # end # def link_sanitizer - @link_sanitizer ||= HTML::LinkSanitizer.new + @link_sanitizer ||= sanitizer_vendor.link_sanitizer.new end - # Gets the HTML::WhiteListSanitizer instance used by sanitize and +sanitize_css+. + # Gets the Rails::Html::WhiteListSanitizer instance used by sanitize and +sanitize_css+. # Replace with any object that responds to +sanitize+. # # class Application < Rails::Application @@ -168,87 +186,27 @@ module ActionView # end # def white_list_sanitizer - @white_list_sanitizer ||= HTML::WhiteListSanitizer.new - end - - # Adds valid HTML attributes that the +sanitize+ helper checks for URIs. - # - # class Application < Rails::Application - # config.action_view.sanitized_uri_attributes = 'lowsrc', 'target' - # end - # - def sanitized_uri_attributes=(attributes) - HTML::WhiteListSanitizer.uri_attributes.merge(attributes) + @white_list_sanitizer ||= sanitizer_vendor.white_list_sanitizer.new end - # Adds to the Set of 'bad' tags for the +sanitize+ helper. - # - # class Application < Rails::Application - # config.action_view.sanitized_bad_tags = 'embed', 'object' - # end - # - def sanitized_bad_tags=(attributes) - HTML::WhiteListSanitizer.bad_tags.merge(attributes) - end - - # Adds to the Set of allowed tags for the +sanitize+ helper. + # Replaces the allowed tags for the +sanitize+ helper. # # class Application < Rails::Application # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td' # end # - def sanitized_allowed_tags=(attributes) - HTML::WhiteListSanitizer.allowed_tags.merge(attributes) + def sanitized_allowed_tags=(tags) + sanitizer_vendor.white_list_sanitizer.allowed_tags = tags end - # Adds to the Set of allowed HTML attributes for the +sanitize+ helper. + # Replaces the allowed HTML attributes for the +sanitize+ helper. # # class Application < Rails::Application # config.action_view.sanitized_allowed_attributes = ['onclick', 'longdesc'] # end # def sanitized_allowed_attributes=(attributes) - HTML::WhiteListSanitizer.allowed_attributes.merge(attributes) - end - - # Adds to the Set of allowed CSS properties for the #sanitize and +sanitize_css+ helpers. - # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_css_properties = 'expression' - # end - # - def sanitized_allowed_css_properties=(attributes) - HTML::WhiteListSanitizer.allowed_css_properties.merge(attributes) - end - - # Adds to the Set of allowed CSS keywords for the +sanitize+ and +sanitize_css+ helpers. - # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_css_keywords = 'expression' - # end - # - def sanitized_allowed_css_keywords=(attributes) - HTML::WhiteListSanitizer.allowed_css_keywords.merge(attributes) - end - - # Adds to the Set of allowed shorthand CSS properties for the +sanitize+ and +sanitize_css+ helpers. - # - # class Application < Rails::Application - # config.action_view.sanitized_shorthand_css_properties = 'expression' - # end - # - def sanitized_shorthand_css_properties=(attributes) - HTML::WhiteListSanitizer.shorthand_css_properties.merge(attributes) - end - - # Adds to the Set of allowed protocols for the +sanitize+ helper. - # - # class Application < Rails::Application - # config.action_view.sanitized_allowed_protocols = 'ssh', 'feed' - # end - # - def sanitized_allowed_protocols=(attributes) - HTML::WhiteListSanitizer.allowed_protocols.merge(attributes) + sanitizer_vendor.white_list_sanitizer.allowed_attributes = attributes end end end diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 35444bcfb4..268558669e 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -9,6 +9,7 @@ module ActionView module TagHelper extend ActiveSupport::Concern include CaptureHelper + include OutputSafetyHelper BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer autoplay controls loop selected hidden scoped async diff --git a/actionview/lib/action_view/helpers/tags/datetime_field.rb b/actionview/lib/action_view/helpers/tags/datetime_field.rb index 25e7e05ec6..b2cee9d198 100644 --- a/actionview/lib/action_view/helpers/tags/datetime_field.rb +++ b/actionview/lib/action_view/helpers/tags/datetime_field.rb @@ -5,8 +5,8 @@ module ActionView def render options = @options.stringify_keys options["value"] ||= format_date(value(object)) - options["min"] = format_date(options["min"]) - options["max"] = format_date(options["max"]) + options["min"] = format_date(datetime_value(options["min"])) + options["max"] = format_date(datetime_value(options["max"])) @options = options super end @@ -16,6 +16,14 @@ module ActionView def format_date(value) value.try(:strftime, "%Y-%m-%dT%T.%L%z") end + + def datetime_value(value) + if value.is_a? String + DateTime.parse(value) rescue nil + else + value + end + end end end end diff --git a/actionview/lib/action_view/helpers/tags/placeholderable.rb b/actionview/lib/action_view/helpers/tags/placeholderable.rb new file mode 100644 index 0000000000..313aa725c9 --- /dev/null +++ b/actionview/lib/action_view/helpers/tags/placeholderable.rb @@ -0,0 +1,32 @@ +module ActionView + module Helpers + module Tags # :nodoc: + module Placeholderable # :nodoc: + def initialize(*) + super + + if tag_value = @options[:placeholder] + object_name = @object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1') + method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}" + + if object.respond_to?(:to_model) + key = object.class.model_name.i18n_key + i18n_default = ["#{key}.#{method_and_value}".to_sym, ""] + end + + i18n_default ||= "" + placeholder = I18n.t("#{object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.placeholder").presence + + placeholder ||= if object && object.class.respond_to?(:human_attribute_name) + object.class.human_attribute_name(method_and_value) + end + + placeholder ||= @method_name.humanize + + @options[:placeholder] = placeholder + end + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb index 00881d9978..180900cc8d 100644 --- a/actionview/lib/action_view/helpers/tags/select.rb +++ b/actionview/lib/action_view/helpers/tags/select.rb @@ -3,7 +3,7 @@ module ActionView module Tags # :nodoc: class Select < Base # :nodoc: def initialize(object_name, method_name, template_object, choices, options, html_options) - @choices = block_given? ? template_object.capture { yield } : choices + @choices = block_given? ? template_object.capture { yield || "" } : choices @choices = @choices.to_a if @choices.is_a?(Range) @html_options = html_options diff --git a/actionview/lib/action_view/helpers/tags/text_area.rb b/actionview/lib/action_view/helpers/tags/text_area.rb index 9ee83ee7c2..69038c1498 100644 --- a/actionview/lib/action_view/helpers/tags/text_area.rb +++ b/actionview/lib/action_view/helpers/tags/text_area.rb @@ -1,7 +1,11 @@ +require 'action_view/helpers/tags/placeholderable' + module ActionView module Helpers module Tags # :nodoc: class TextArea < Base # :nodoc: + include Placeholderable + def render options = @options.stringify_keys add_default_name_and_id(options) diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb index e0b80d81c2..5c576a20ca 100644 --- a/actionview/lib/action_view/helpers/tags/text_field.rb +++ b/actionview/lib/action_view/helpers/tags/text_field.rb @@ -1,7 +1,11 @@ +require 'action_view/helpers/tags/placeholderable' + module ActionView module Helpers module Tags # :nodoc: class TextField < Base # :nodoc: + include Placeholderable + def render options = @options.stringify_keys options["size"] = options["maxlength"] unless options.key?("size") diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 17ec6a40bf..1d50ea2ff5 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -1,4 +1,5 @@ require 'action_view/helpers/tag_helper' +require 'active_support/core_ext/string/access' require 'i18n/exceptions' module ActionView diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index 6c8d9cb5bf..9047dbdd85 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -13,11 +13,11 @@ module ActionView end def render_template(event) - return unless logger.info? - message = " Rendered #{from_rails_root(event.payload[:identifier])}" - message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] - message << " (#{event.duration.round(1)}ms)" - info(message) + info do + message = " Rendered #{from_rails_root(event.payload[:identifier])}" + message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] + message << " (#{event.duration.round(1)}ms)" + end end alias :render_partial :render_template alias :render_collection :render_template diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 5fff6b0771..ea687d9cca 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -66,10 +66,7 @@ module ActionView def self.get(details) if details[:formats] details = details.dup - syms = Set.new Mime::SET.symbols - details[:formats] = details[:formats].select { |v| - syms.include? v - } + details[:formats] &= Mime::SET.symbols end @details_keys[details] ||= new end diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index 73c19a0ae2..1f122f9bc6 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -29,8 +29,9 @@ module ActionView def extract_details(options) @lookup_context.registered_details.each_with_object({}) do |key, details| - next unless value = options[key] - details[key] = Array(value) + value = options[key] + + details[key] = Array(value) if value end end @@ -41,6 +42,7 @@ module ActionView def prepend_formats(formats) formats = Array(formats) return if formats.empty? || @lookup_context.html_fallback_for_js + @lookup_context.formats = formats | @lookup_context.formats end end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 36f17f01fd..0407632435 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -1,6 +1,33 @@ require 'thread_safe' module ActionView + class PartialIteration + # The number of iterations that will be done by the partial. + attr_reader :size + + # The current iteration of the partial. + attr_reader :index + + def initialize(size) + @size = size + @index = 0 + end + + # Check if this is the first iteration of the partial. + def first? + index == 0 + end + + # Check if this is the last iteration of the partial. + def last? + index == size - 1 + end + + def iterate! # :nodoc: + @index += 1 + end + end + # = Action View Partials # # There's also a convenience method for rendering sub templates within the current controller that depends on a @@ -56,8 +83,12 @@ module ActionView # <%= render partial: "ad", collection: @advertisements %> # # This will render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. An - # iteration counter will automatically be made available to the template with a name of the form - # +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+. + # iteration object will automatically be made available to the template with a name of the form + # +partial_name_iteration+. The iteration object has knowledge about which index the current object has in + # the collection and the total size of the collection. The iteration object also has two convenience methods, + # +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+. + # For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's + # +index+ method. # # The <tt>:as</tt> option may be used when rendering partials. # @@ -281,6 +312,8 @@ module ActionView end end + private + def render_collection return nil if @collection.blank? @@ -322,25 +355,27 @@ module ActionView # respond to +to_partial_path+ in order to setup the path. def setup(context, options, block) @view = context - partial = options[:partial] - @options = options - @locals = options[:locals] || {} @block = block + + @locals = options[:locals] || {} @details = extract_details(options) prepend_formats(options[:formats]) + partial = options[:partial] + if String === partial @object = options[:object] + @collection = collection_from_options @path = partial - @collection = collection else @object = partial + @collection = collection_from_object || collection_from_options - if @collection = collection_from_object || collection + if @collection paths = @collection_data = @collection.map { |o| partial_path(o) } - @path = paths.uniq.size == 1 ? paths.first : nil + @path = paths.uniq.one? ? paths.first : nil else @path = partial_path end @@ -352,7 +387,7 @@ module ActionView end if @path - @variable, @variable_counter = retrieve_variable(@path, as) + @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as) @template_keys = retrieve_template_keys else paths.map! { |path| retrieve_variable(path, as).unshift(path) } @@ -361,7 +396,7 @@ module ActionView self end - def collection + def collection_from_options if @options.key?(:collection) collection = @options[:collection] collection.respond_to?(:to_ary) ? collection.to_ary : [] @@ -373,9 +408,7 @@ module ActionView end def find_partial - if path = @path - find_template(path, @template_keys) - end + find_template(@path, @template_keys) if @path end def find_template(path, locals) @@ -385,19 +418,22 @@ module ActionView def collection_with_template view, locals, template = @view, @locals, @template - as, counter = @variable, @variable_counter + as, counter, iteration = @variable, @variable_counter, @variable_iteration if layout = @options[:layout] layout = find_template(layout, @template_keys) end - index = -1 + partial_iteration = PartialIteration.new(@collection.size) + locals[iteration] = partial_iteration + @collection.map do |object| - locals[as] = object - locals[counter] = (index += 1) + locals[as] = object + locals[counter] = partial_iteration.index content = template.render(view, locals) content = layout.render(view, locals) { content } if layout + partial_iteration.iterate! content end end @@ -407,16 +443,20 @@ module ActionView cache = {} keys = @locals.keys - index = -1 + partial_iteration = PartialIteration.new(@collection.size) + @collection.map do |object| - index += 1 - path, as, counter = collection_data[index] + index = partial_iteration.index + path, as, counter, iteration = collection_data[index] - locals[as] = object - locals[counter] = index + locals[as] = object + locals[counter] = index + locals[iteration] = partial_iteration template = (cache[path] ||= find_template(path, keys + [as, counter])) - template.render(view, locals) + content = template.render(view, locals) + partial_iteration.iterate! + content end end @@ -466,8 +506,11 @@ module ActionView def retrieve_template_keys keys = @locals.keys - keys << @variable if @object || @collection - keys << @variable_counter if @collection + keys << @variable if @object || @collection + if @collection + keys << @variable_counter + keys << @variable_iteration + end keys end @@ -477,8 +520,11 @@ module ActionView raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/ $1.to_sym end - variable_counter = :"#{variable}_counter" if @collection - [variable, variable_counter] + if @collection + variable_counter = :"#{variable}_counter" + variable_iteration = :"#{variable}_iteration" + end + [variable, variable_counter, variable_iteration] end IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " + diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index be17097428..f3a48ecfa0 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -6,19 +6,18 @@ module ActionView @view = context @details = extract_details(options) template = determine_template(options) - context = @lookup_context prepend_formats(template.formats) - unless context.rendered_format - context.rendered_format = template.formats.first || formats.first - end + @lookup_context.rendered_format ||= (template.formats.first || formats.first) render_template(template, options[:layout], options[:locals]) end + private + # Determine the template to be rendered using the given options. - def determine_template(options) #:nodoc: + def determine_template(options) keys = options.fetch(:locals, {}).keys if options.key?(:body) diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index c92d090cce..81d5836a8c 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -35,12 +35,13 @@ module ActionView module ClassMethods def view_context_class @view_context_class ||= begin - routes = respond_to?(:_routes) && _routes + include_path_helpers = supports_path? + routes = respond_to?(:_routes) && _routes helpers = respond_to?(:_helpers) && _helpers Class.new(ActionView::Base) do if routes - include routes.url_helpers + include routes.url_helpers(include_path_helpers) include routes.mounted_helpers end diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index 881a123572..75febb8652 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -82,7 +82,9 @@ module ActionView when nil super({:only_path => true}) when Hash - super({ :only_path => options[:host].nil? }.merge!(options.symbolize_keys)) + options = options.symbolize_keys + options[:only_path] = options[:host].nil? unless options.key?(:only_path) + super(options) when :back _back_url when Symbol diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index 9e8e6f43d5..7edfc436a6 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -3,6 +3,8 @@ require 'action_controller' require 'action_controller/test_case' require 'action_view' +require 'rails-dom-testing' + module ActionView # = Action View Test Case class TestCase < ActiveSupport::TestCase @@ -34,6 +36,7 @@ module ActionView extend ActiveSupport::Concern include ActionDispatch::Assertions, ActionDispatch::TestProcess + include Rails::Dom::Testing::Assertions include ActionController::TemplateAssertions include ActionView::Context @@ -99,7 +102,9 @@ module ActionView def setup_with_controller @controller = ActionView::TestCase::TestController.new @request = @controller.request - @output_buffer = ActiveSupport::SafeBuffer.new + # empty string ensures buffer has UTF-8 encoding as + # new without arguments returns ASCII-8BIT encoded buffer like String#new + @output_buffer = ActiveSupport::SafeBuffer.new '' @rendered = '' make_test_case_available_to_view! @@ -151,11 +156,10 @@ module ActionView private - # Support the selector assertions - # # Need to experiment if this priority is the best one: rendered => output_buffer - def response_from_page - HTML::Document.new(@rendered.blank? ? @output_buffer : @rendered).root + def document_root_element + @html_document ||= Nokogiri::HTML::Document.parse(@rendered.blank? ? @output_buffer : @rendered) + @html_document.root end def say_no_to_protect_against_forgery! @@ -236,7 +240,8 @@ module ActionView :@test_passed, :@view, :@view_context_class, - :@_subscribers + :@_subscribers, + :@html_document ] def _user_defined_ivars @@ -259,7 +264,7 @@ module ActionView def method_missing(selector, *args) if @controller.respond_to?(:_routes) && - ( @controller._routes.named_routes.helpers.include?(selector) || + ( @controller._routes.named_routes.route_defined?(selector) || @controller._routes.mounted_helpers.method_defined?(selector) ) @controller.__send__(selector, *args) else diff --git a/actionview/lib/action_view/vendor/html-scanner.rb b/actionview/lib/action_view/vendor/html-scanner.rb deleted file mode 100644 index 775b827529..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner.rb +++ /dev/null @@ -1,20 +0,0 @@ -$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/html-scanner" - -module HTML - extend ActiveSupport::Autoload - - eager_autoload do - autoload :CDATA, 'html/node' - autoload :Document, 'html/document' - autoload :FullSanitizer, 'html/sanitizer' - autoload :LinkSanitizer, 'html/sanitizer' - autoload :Node, 'html/node' - autoload :Sanitizer, 'html/sanitizer' - autoload :Selector, 'html/selector' - autoload :Tag, 'html/node' - autoload :Text, 'html/node' - autoload :Tokenizer, 'html/tokenizer' - autoload :Version, 'html/version' - autoload :WhiteListSanitizer, 'html/sanitizer' - end -end diff --git a/actionview/lib/action_view/vendor/html-scanner/html/document.rb b/actionview/lib/action_view/vendor/html-scanner/html/document.rb deleted file mode 100644 index 386820300a..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner/html/document.rb +++ /dev/null @@ -1,68 +0,0 @@ -require 'html/tokenizer' -require 'html/node' -require 'html/selector' -require 'html/sanitizer' - -module HTML #:nodoc: - # A top-level HTML document. You give it a body of text, and it will parse that - # text into a tree of nodes. - class Document #:nodoc: - - # The root of the parsed document. - attr_reader :root - - # Create a new Document from the given text. - def initialize(text, strict=false, xml=false) - tokenizer = Tokenizer.new(text) - @root = Node.new(nil) - node_stack = [ @root ] - while token = tokenizer.next - node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token, strict) - - node_stack.last.children << node unless node.tag? && node.closing == :close - if node.tag? - if node_stack.length > 1 && node.closing == :close - if node_stack.last.name == node.name - if node_stack.last.children.empty? - node_stack.last.children << Text.new(node_stack.last, node.line, node.position, "") - end - node_stack.pop - else - open_start = node_stack.last.position - 20 - open_start = 0 if open_start < 0 - close_start = node.position - 20 - close_start = 0 if close_start < 0 - msg = <<EOF.strip -ignoring attempt to close #{node_stack.last.name} with #{node.name} - opened at byte #{node_stack.last.position}, line #{node_stack.last.line} - closed at byte #{node.position}, line #{node.line} - attributes at open: #{node_stack.last.attributes.inspect} - text around open: #{text[open_start,40].inspect} - text around close: #{text[close_start,40].inspect} -EOF - strict ? raise(msg) : warn(msg) - end - elsif !node.childless?(xml) && node.closing != :close - node_stack.push node - end - end - end - end - - # Search the tree for (and return) the first node that matches the given - # conditions. The conditions are interpreted differently for different node - # types, see HTML::Text#find and HTML::Tag#find. - def find(conditions) - @root.find(conditions) - end - - # Search the tree for (and return) all nodes that match the given - # conditions. The conditions are interpreted differently for different node - # types, see HTML::Text#find and HTML::Tag#find. - def find_all(conditions) - @root.find_all(conditions) - end - - end - -end diff --git a/actionview/lib/action_view/vendor/html-scanner/html/node.rb b/actionview/lib/action_view/vendor/html-scanner/html/node.rb deleted file mode 100644 index 27f0f2f6f8..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner/html/node.rb +++ /dev/null @@ -1,532 +0,0 @@ -require 'strscan' - -module HTML #:nodoc: - - class Conditions < Hash #:nodoc: - def initialize(hash) - super() - hash = { :content => hash } unless Hash === hash - hash = keys_to_symbols(hash) - hash.each do |k,v| - case k - when :tag, :content then - # keys are valid, and require no further processing - when :attributes then - hash[k] = keys_to_strings(v) - when :parent, :child, :ancestor, :descendant, :sibling, :before, - :after - hash[k] = Conditions.new(v) - when :children - hash[k] = v = keys_to_symbols(v) - v.each do |key,value| - case key - when :count, :greater_than, :less_than - # keys are valid, and require no further processing - when :only - v[key] = Conditions.new(value) - else - raise "illegal key #{key.inspect} => #{value.inspect}" - end - end - else - raise "illegal key #{k.inspect} => #{v.inspect}" - end - end - update hash - end - - private - - def keys_to_strings(hash) - Hash[hash.keys.map {|k| [k.to_s, hash[k]]}] - end - - def keys_to_symbols(hash) - Hash[hash.keys.map do |k| - raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym) - [k.to_sym, hash[k]] - end] - end - end - - # The base class of all nodes, textual and otherwise, in an HTML document. - class Node #:nodoc: - # The array of children of this node. Not all nodes have children. - attr_reader :children - - # The parent node of this node. All nodes have a parent, except for the - # root node. - attr_reader :parent - - # The line number of the input where this node was begun - attr_reader :line - - # The byte position in the input where this node was begun - attr_reader :position - - # Create a new node as a child of the given parent. - def initialize(parent, line=0, pos=0) - @parent = parent - @children = [] - @line, @position = line, pos - end - - # Returns a textual representation of the node. - def to_s - @children.join() - end - - # Returns false (subclasses must override this to provide specific matching - # behavior.) +conditions+ may be of any type. - def match(conditions) - false - end - - # Search the children of this node for the first node for which #find - # returns non +nil+. Returns the result of the #find call that succeeded. - def find(conditions) - conditions = validate_conditions(conditions) - @children.each do |child| - node = child.find(conditions) - return node if node - end - nil - end - - # Search for all nodes that match the given conditions, and return them - # as an array. - def find_all(conditions) - conditions = validate_conditions(conditions) - - matches = [] - matches << self if match(conditions) - @children.each do |child| - matches.concat child.find_all(conditions) - end - matches - end - - # Returns +false+. Subclasses may override this if they define a kind of - # tag. - def tag? - false - end - - def validate_conditions(conditions) - Conditions === conditions ? conditions : Conditions.new(conditions) - end - - def ==(node) - return false unless self.class == node.class && children.size == node.children.size - - equivalent = true - - children.size.times do |i| - equivalent &&= children[i] == node.children[i] - end - - equivalent - end - - class <<self - def parse(parent, line, pos, content, strict=true) - if content !~ /^<\S/ - Text.new(parent, line, pos, content) - else - scanner = StringScanner.new(content) - - unless scanner.skip(/</) - if strict - raise "expected <" - else - return Text.new(parent, line, pos, content) - end - end - - if scanner.skip(/!\[CDATA\[/) - unless scanner.skip_until(/\]\]>/) - if strict - raise "expected ]]> (got #{scanner.rest.inspect} for #{content})" - else - scanner.skip_until(/\Z/) - end - end - - return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, '')) - end - - closing = ( scanner.scan(/\//) ? :close : nil ) - return Text.new(parent, line, pos, content) unless name = scanner.scan(/[^\s!>\/]+/) - name.downcase! - - unless closing - scanner.skip(/\s*/) - attributes = {} - while attr = scanner.scan(/[-\w:]+/) - value = true - if scanner.scan(/\s*=\s*/) - if delim = scanner.scan(/['"]/) - value = "" - while text = scanner.scan(/[^#{delim}\\]+|./) - case text - when "\\" then - value << text - break if scanner.eos? - value << scanner.getch - when delim - break - else value << text - end - end - else - value = scanner.scan(/[^\s>\/]+/) - end - end - attributes[attr.downcase] = value - scanner.skip(/\s*/) - end - - closing = ( scanner.scan(/\//) ? :self : nil ) - end - - unless scanner.scan(/\s*>/) - if strict - raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})" - else - # throw away all text until we find what we're looking for - scanner.skip_until(/>/) or scanner.terminate - end - end - - Tag.new(parent, line, pos, name, attributes, closing) - end - end - end - end - - # A node that represents text, rather than markup. - class Text < Node #:nodoc: - - attr_reader :content - - # Creates a new text node as a child of the given parent, with the given - # content. - def initialize(parent, line, pos, content) - super(parent, line, pos) - @content = content - end - - # Returns the content of this node. - def to_s - @content - end - - # Returns +self+ if this node meets the given conditions. Text nodes support - # conditions of the following kinds: - # - # * if +conditions+ is a string, it must be a substring of the node's - # content - # * if +conditions+ is a regular expression, it must match the node's - # content - # * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that - # is either a string or a regexp, and which is interpreted as described - # above. - def find(conditions) - match(conditions) && self - end - - # Returns non-+nil+ if this node meets the given conditions, or +nil+ - # otherwise. See the discussion of #find for the valid conditions. - def match(conditions) - case conditions - when String - @content == conditions - when Regexp - @content =~ conditions - when Hash - conditions = validate_conditions(conditions) - - # Text nodes only have :content, :parent, :ancestor - unless (conditions.keys - [:content, :parent, :ancestor]).empty? - return false - end - - match(conditions[:content]) - else - nil - end - end - - def ==(node) - return false unless super - content == node.content - end - end - - # A CDATA node is simply a text node with a specialized way of displaying - # itself. - class CDATA < Text #:nodoc: - def to_s - "<![CDATA[#{super}]]>" - end - end - - # A Tag is any node that represents markup. It may be an opening tag, a - # closing tag, or a self-closing tag. It has a name, and may have a hash of - # attributes. - class Tag < Node #:nodoc: - - # Either +nil+, <tt>:close</tt>, or <tt>:self</tt> - attr_reader :closing - - # Either +nil+, or a hash of attributes for this node. - attr_reader :attributes - - # The name of this tag. - attr_reader :name - - # Create a new node as a child of the given parent, using the given content - # to describe the node. It will be parsed and the node name, attributes and - # closing status extracted. - def initialize(parent, line, pos, name, attributes, closing) - super(parent, line, pos) - @name = name - @attributes = attributes - @closing = closing - end - - # A convenience for obtaining an attribute of the node. Returns +nil+ if - # the node has no attributes. - def [](attr) - @attributes ? @attributes[attr] : nil - end - - # Returns non-+nil+ if this tag can contain child nodes. - def childless?(xml = false) - return false if xml && @closing.nil? - !@closing.nil? || - @name =~ /^(img|br|hr|link|meta|area|base|basefont| - col|frame|input|isindex|param)$/ox - end - - # Returns a textual representation of the node - def to_s - if @closing == :close - "</#{@name}>" - else - s = "<#{@name}" - @attributes.each do |k,v| - s << " #{k}" - s << "=\"#{v}\"" if String === v - end - s << " /" if @closing == :self - s << ">" - @children.each { |child| s << child.to_s } - s << "</#{@name}>" if @closing != :self && !@children.empty? - s - end - end - - # If either the node or any of its children meet the given conditions, the - # matching node is returned. Otherwise, +nil+ is returned. (See the - # description of the valid conditions in the +match+ method.) - def find(conditions) - match(conditions) && self || super - end - - # Returns +true+, indicating that this node represents an HTML tag. - def tag? - true - end - - # Returns +true+ if the node meets any of the given conditions. The - # +conditions+ parameter must be a hash of any of the following keys - # (all are optional): - # - # * <tt>:tag</tt>: the node name must match the corresponding value - # * <tt>:attributes</tt>: a hash. The node's values must match the - # corresponding values in the hash. - # * <tt>:parent</tt>: a hash. The node's parent must match the - # corresponding hash. - # * <tt>:child</tt>: a hash. At least one of the node's immediate children - # must meet the criteria described by the hash. - # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must - # meet the criteria described by the hash. - # * <tt>:descendant</tt>: a hash. At least one of the node's descendants - # must meet the criteria described by the hash. - # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must - # meet the criteria described by the hash. - # * <tt>:after</tt>: a hash. The node must be after any sibling meeting - # the criteria described by the hash, and at least one sibling must match. - # * <tt>:before</tt>: a hash. The node must be before any sibling meeting - # the criteria described by the hash, and at least one sibling must match. - # * <tt>:children</tt>: a hash, for counting children of a node. Accepts the - # keys: - # ** <tt>:count</tt>: either a number or a range which must equal (or - # include) the number of children that match. - # ** <tt>:less_than</tt>: the number of matching children must be less than - # this number. - # ** <tt>:greater_than</tt>: the number of matching children must be - # greater than this number. - # ** <tt>:only</tt>: another hash consisting of the keys to use - # to match on the children, and only matching children will be - # counted. - # - # Conditions are matched using the following algorithm: - # - # * if the condition is a string, it must be a substring of the value. - # * if the condition is a regexp, it must match the value. - # * if the condition is a number, the value must match number.to_s. - # * if the condition is +true+, the value must not be +nil+. - # * if the condition is +false+ or +nil+, the value must be +nil+. - # - # Usage: - # - # # test if the node is a "span" tag - # node.match tag: "span" - # - # # test if the node's parent is a "div" - # node.match parent: { tag: "div" } - # - # # test if any of the node's ancestors are "table" tags - # node.match ancestor: { tag: "table" } - # - # # test if any of the node's immediate children are "em" tags - # node.match child: { tag: "em" } - # - # # test if any of the node's descendants are "strong" tags - # node.match descendant: { tag: "strong" } - # - # # test if the node has between 2 and 4 span tags as immediate children - # node.match children: { count: 2..4, only: { tag: "span" } } - # - # # get funky: test to see if the node is a "div", has a "ul" ancestor - # # and an "li" parent (with "class" = "enum"), and whether or not it has - # # a "span" descendant that contains # text matching /hello world/: - # node.match tag: "div", - # ancestor: { tag: "ul" }, - # parent: { tag: "li", - # attributes: { class: "enum" } }, - # descendant: { tag: "span", - # child: /hello world/ } - def match(conditions) - conditions = validate_conditions(conditions) - # check content of child nodes - if conditions[:content] - if children.empty? - return false unless match_condition("", conditions[:content]) - else - return false unless children.find { |child| child.match(conditions[:content]) } - end - end - - # test the name - return false unless match_condition(@name, conditions[:tag]) if conditions[:tag] - - # test attributes - (conditions[:attributes] || {}).each do |key, value| - return false unless match_condition(self[key], value) - end - - # test parent - return false unless parent.match(conditions[:parent]) if conditions[:parent] - - # test children - return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child] - - # test ancestors - if conditions[:ancestor] - return false unless catch :found do - p = self - throw :found, true if p.match(conditions[:ancestor]) while p = p.parent - end - end - - # test descendants - if conditions[:descendant] - return false unless children.find do |child| - # test the child - child.match(conditions[:descendant]) || - # test the child's descendants - child.match(:descendant => conditions[:descendant]) - end - end - - # count children - if opts = conditions[:children] - matches = children.select do |c| - (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?)) - end - - matches = matches.select { |c| c.match(opts[:only]) } if opts[:only] - opts.each do |key, value| - next if key == :only - case key - when :count - if Integer === value - return false if matches.length != value - else - return false unless value.include?(matches.length) - end - when :less_than - return false unless matches.length < value - when :greater_than - return false unless matches.length > value - else raise "unknown count condition #{key}" - end - end - end - - # test siblings - if conditions[:sibling] || conditions[:before] || conditions[:after] - siblings = parent ? parent.children : [] - self_index = siblings.index(self) - - if conditions[:sibling] - return false unless siblings.detect do |s| - s != self && s.match(conditions[:sibling]) - end - end - - if conditions[:before] - return false unless siblings[self_index+1..-1].detect do |s| - s != self && s.match(conditions[:before]) - end - end - - if conditions[:after] - return false unless siblings[0,self_index].detect do |s| - s != self && s.match(conditions[:after]) - end - end - end - - true - end - - def ==(node) - return false unless super - return false unless closing == node.closing && self.name == node.name - attributes == node.attributes - end - - private - # Match the given value to the given condition. - def match_condition(value, condition) - case condition - when String - value && value == condition - when Regexp - value && value.match(condition) - when Numeric - value == condition.to_s - when true - !value.nil? - when false, nil - value.nil? - else - false - end - end - end -end diff --git a/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb deleted file mode 100644 index ed34eecf55..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb +++ /dev/null @@ -1,188 +0,0 @@ -require 'set' -require 'cgi' -require 'active_support/core_ext/module/attribute_accessors' - -module HTML - class Sanitizer - def sanitize(text, options = {}) - validate_options(options) - return text unless sanitizeable?(text) - tokenize(text, options).join - end - - def sanitizeable?(text) - !(text.nil? || text.empty? || !text.index("<")) - end - - protected - def tokenize(text, options) - tokenizer = HTML::Tokenizer.new(text) - result = [] - while token = tokenizer.next - node = Node.parse(nil, 0, 0, token, false) - process_node node, result, options - end - result - end - - def process_node(node, result, options) - result << node.to_s - end - - def validate_options(options) - if options[:tags] && !options[:tags].is_a?(Enumerable) - raise ArgumentError, "You should pass :tags as an Enumerable" - end - - if options[:attributes] && !options[:attributes].is_a?(Enumerable) - raise ArgumentError, "You should pass :attributes as an Enumerable" - end - end - end - - class FullSanitizer < Sanitizer - def sanitize(text, options = {}) - result = super - # strip any comments, and if they have a newline at the end (ie. line with - # only a comment) strip that too - result = result.gsub(/<!--(.*?)-->[\n]?/m, "") if (result && result =~ /<!--(.*?)-->[\n]?/m) - # Recurse - handle all dirty nested tags - result == text ? result : sanitize(result, options) - end - - def process_node(node, result, options) - result << node.to_s if node.class == HTML::Text - end - end - - class LinkSanitizer < FullSanitizer - cattr_accessor :included_tags, :instance_writer => false - self.included_tags = Set.new(%w(a href)) - - def sanitizeable?(text) - !(text.nil? || text.empty? || !((text.index("<a") || text.index("<href")) && text.index(">"))) - end - - protected - def process_node(node, result, options) - result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name) - end - end - - class WhiteListSanitizer < Sanitizer - [:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags, - :allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr| - class_attribute attr, :instance_writer => false - end - - # A regular expression of the valid characters used to separate protocols like - # the ':' in 'http://foo.com' - self.protocol_separator = /:|(�*58)|(p)|(�*3a)|(%|%)3A/i - - # Specifies a Set of HTML attributes that can have URIs. - self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc)) - - # Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed - # to just escaping harmless tags like <font> - self.bad_tags = Set.new(%w(script)) - - # Specifies the default Set of tags that the #sanitize helper will allow unscathed. - self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub - sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr - acronym a img blockquote del ins)) - - # Specifies the default Set of html attributes that the #sanitize helper will leave - # in the allowed tag. - self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr)) - - # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept. - self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto - feed svn urn aim rsync tag ssh sftp rtsp afs)) - - # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept. - self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse - border-color border-left-color border-right-color border-top-color clear color cursor direction display - elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height - overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation - speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space - width)) - - # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept. - self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center - collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal - nowrap olive pointer purple red right solid silver teal top transparent underline white yellow)) - - # Specifies the default Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers. - self.shorthand_css_properties = Set.new(%w(background border margin padding)) - - # Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute - def sanitize_css(style) - # disallow urls - style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ') - - # gauntlet - if style !~ /\A([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*\z/ || - style !~ /\A(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*\z/ - return '' - end - - clean = [] - style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val| - if allowed_css_properties.include?(prop.downcase) - clean << prop + ': ' + val + ';' - elsif shorthand_css_properties.include?(prop.split('-')[0].downcase) - unless val.split().any? do |keyword| - !allowed_css_keywords.include?(keyword) && - keyword !~ /\A(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)\z/ - end - clean << prop + ': ' + val + ';' - end - end - end - clean.join(' ') - end - - protected - def tokenize(text, options) - options[:parent] = [] - options[:attributes] ||= allowed_attributes - options[:tags] ||= allowed_tags - super - end - - def process_node(node, result, options) - result << case node - when HTML::Tag - if node.closing == :close - options[:parent].shift - else - options[:parent].unshift node.name - end - - process_attributes_for node, options - - options[:tags].include?(node.name) ? node : nil - else - bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "<") - end - end - - def process_attributes_for(node, options) - return unless node.attributes - node.attributes.keys.each do |attr_name| - value = node.attributes[attr_name].to_s - - if !options[:attributes].include?(attr_name) || contains_bad_protocols?(attr_name, value) - node.attributes.delete(attr_name) - else - node.attributes[attr_name] = attr_name == 'style' ? sanitize_css(value) : CGI::escapeHTML(CGI::unescapeHTML(value)) - end - end - end - - def contains_bad_protocols?(attr_name, value) - uri_attributes.include?(attr_name) && - (value =~ /(^[^\/:]*):|(�*58)|(p)|(�*3a)|(%|%)3A/i && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip)) - end - end -end diff --git a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb deleted file mode 100644 index dfdd724b9b..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb +++ /dev/null @@ -1,830 +0,0 @@ -#-- -# Copyright (c) 2006 Assaf Arkin (http://labnotes.org) -# Under MIT and/or CC By license. -#++ - -module HTML - - # Selects HTML elements using CSS 2 selectors. - # - # The +Selector+ class uses CSS selector expressions to match and select - # HTML elements. - # - # For example: - # selector = HTML::Selector.new "form.login[action=/login]" - # creates a new selector that matches any +form+ element with the class - # +login+ and an attribute +action+ with the value <tt>/login</tt>. - # - # === Matching Elements - # - # Use the #match method to determine if an element matches the selector. - # - # For simple selectors, the method returns an array with that element, - # or +nil+ if the element does not match. For complex selectors (see below) - # the method returns an array with all matched elements, of +nil+ if no - # match found. - # - # For example: - # if selector.match(element) - # puts "Element is a login form" - # end - # - # === Selecting Elements - # - # Use the #select method to select all matching elements starting with - # one element and going through all children in depth-first order. - # - # This method returns an array of all matching elements, an empty array - # if no match is found - # - # For example: - # selector = HTML::Selector.new "input[type=text]" - # matches = selector.select(element) - # matches.each do |match| - # puts "Found text field with name #{match.attributes['name']}" - # end - # - # === Expressions - # - # Selectors can match elements using any of the following criteria: - # * <tt>name</tt> -- Match an element based on its name (tag name). - # For example, <tt>p</tt> to match a paragraph. You can use <tt>*</tt> - # to match any element. - # * <tt>#</tt><tt>id</tt> -- Match an element based on its identifier (the - # <tt>id</tt> attribute). For example, <tt>#</tt><tt>page</tt>. - # * <tt>.class</tt> -- Match an element based on its class name, all - # class names if more than one specified. - # * <tt>[attr]</tt> -- Match an element that has the specified attribute. - # * <tt>[attr=value]</tt> -- Match an element that has the specified - # attribute and value. (More operators are supported see below) - # * <tt>:pseudo-class</tt> -- Match an element based on a pseudo class, - # such as <tt>:nth-child</tt> and <tt>:empty</tt>. - # * <tt>:not(expr)</tt> -- Match an element that does not match the - # negation expression. - # - # When using a combination of the above, the element name comes first - # followed by identifier, class names, attributes, pseudo classes and - # negation in any order. Do not separate these parts with spaces! - # Space separation is used for descendant selectors. - # - # For example: - # selector = HTML::Selector.new "form.login[action=/login]" - # The matched element must be of type +form+ and have the class +login+. - # It may have other classes, but the class +login+ is required to match. - # It must also have an attribute called +action+ with the value - # <tt>/login</tt>. - # - # This selector will match the following element: - # <form class="login form" method="post" action="/login"> - # but will not match the element: - # <form method="post" action="/logout"> - # - # === Attribute Values - # - # Several operators are supported for matching attributes: - # * <tt>name</tt> -- The element must have an attribute with that name. - # * <tt>name=value</tt> -- The element must have an attribute with that - # name and value. - # * <tt>name^=value</tt> -- The attribute value must start with the - # specified value. - # * <tt>name$=value</tt> -- The attribute value must end with the - # specified value. - # * <tt>name*=value</tt> -- The attribute value must contain the - # specified value. - # * <tt>name~=word</tt> -- The attribute value must contain the specified - # word (space separated). - # * <tt>name|=word</tt> -- The attribute value must start with specified - # word. - # - # For example, the following two selectors match the same element: - # #my_id - # [id=my_id] - # and so do the following two selectors: - # .my_class - # [class~=my_class] - # - # === Alternatives, siblings, children - # - # Complex selectors use a combination of expressions to match elements: - # * <tt>expr1 expr2</tt> -- Match any element against the second expression - # if it has some parent element that matches the first expression. - # * <tt>expr1 > expr2</tt> -- Match any element against the second expression - # if it is the child of an element that matches the first expression. - # * <tt>expr1 + expr2</tt> -- Match any element against the second expression - # if it immediately follows an element that matches the first expression. - # * <tt>expr1 ~ expr2</tt> -- Match any element against the second expression - # that comes after an element that matches the first expression. - # * <tt>expr1, expr2</tt> -- Match any element against the first expression, - # or against the second expression. - # - # Since children and sibling selectors may match more than one element given - # the first element, the #match method may return more than one match. - # - # === Pseudo classes - # - # Pseudo classes were introduced in CSS 3. They are most often used to select - # elements in a given position: - # * <tt>:root</tt> -- Match the element only if it is the root element - # (no parent element). - # * <tt>:empty</tt> -- Match the element only if it has no child elements, - # and no text content. - # * <tt>:content(string)</tt> -- Match the element only if it has <tt>string</tt> - # as its text content (ignoring leading and trailing whitespace). - # * <tt>:only-child</tt> -- Match the element if it is the only child (element) - # of its parent element. - # * <tt>:only-of-type</tt> -- Match the element if it is the only child (element) - # of its parent element and its type. - # * <tt>:first-child</tt> -- Match the element if it is the first child (element) - # of its parent element. - # * <tt>:first-of-type</tt> -- Match the element if it is the first child (element) - # of its parent element of its type. - # * <tt>:last-child</tt> -- Match the element if it is the last child (element) - # of its parent element. - # * <tt>:last-of-type</tt> -- Match the element if it is the last child (element) - # of its parent element of its type. - # * <tt>:nth-child(b)</tt> -- Match the element if it is the b-th child (element) - # of its parent element. The value <tt>b</tt> specifies its index, starting with 1. - # * <tt>:nth-child(an+b)</tt> -- Match the element if it is the b-th child (element) - # in each group of <tt>a</tt> child elements of its parent element. - # * <tt>:nth-child(-an+b)</tt> -- Match the element if it is the first child (element) - # in each group of <tt>a</tt> child elements, up to the first <tt>b</tt> child - # elements of its parent element. - # * <tt>:nth-child(odd)</tt> -- Match element in the odd position (i.e. first, third). - # Same as <tt>:nth-child(2n+1)</tt>. - # * <tt>:nth-child(even)</tt> -- Match element in the even position (i.e. second, - # fourth). Same as <tt>:nth-child(2n+2)</tt>. - # * <tt>:nth-of-type(..)</tt> -- As above, but only counts elements of its type. - # * <tt>:nth-last-child(..)</tt> -- As above, but counts from the last child. - # * <tt>:nth-last-of-type(..)</tt> -- As above, but counts from the last child and - # only elements of its type. - # * <tt>:not(selector)</tt> -- Match the element only if the element does not - # match the simple selector. - # - # As you can see, <tt>:nth-child</tt> pseudo class and its variant can get quite - # tricky and the CSS specification doesn't do a much better job explaining it. - # But after reading the examples and trying a few combinations, it's easy to - # figure out. - # - # For example: - # table tr:nth-child(odd) - # Selects every second row in the table starting with the first one. - # - # div p:nth-child(4) - # Selects the fourth paragraph in the +div+, but not if the +div+ contains - # other elements, since those are also counted. - # - # div p:nth-of-type(4) - # Selects the fourth paragraph in the +div+, counting only paragraphs, and - # ignoring all other elements. - # - # div p:nth-of-type(-n+4) - # Selects the first four paragraphs, ignoring all others. - # - # And you can always select an element that matches one set of rules but - # not another using <tt>:not</tt>. For example: - # p:not(.post) - # Matches all paragraphs that do not have the class <tt>.post</tt>. - # - # === Substitution Values - # - # You can use substitution with identifiers, class names and element values. - # A substitution takes the form of a question mark (<tt>?</tt>) and uses the - # next value in the argument list following the CSS expression. - # - # The substitution value may be a string or a regular expression. All other - # values are converted to strings. - # - # For example: - # selector = HTML::Selector.new "#?", /^\d+$/ - # matches any element whose identifier consists of one or more digits. - # - # See http://www.w3.org/TR/css3-selectors/ - class Selector - - - # An invalid selector. - class InvalidSelectorError < StandardError #:nodoc: - end - - - class << self - - # :call-seq: - # Selector.for_class(cls) => selector - # - # Creates a new selector for the given class name. - def for_class(cls) - self.new([".?", cls]) - end - - - # :call-seq: - # Selector.for_id(id) => selector - # - # Creates a new selector for the given id. - def for_id(id) - self.new(["#?", id]) - end - - end - - - # :call-seq: - # Selector.new(string, [values ...]) => selector - # - # Creates a new selector from a CSS 2 selector expression. - # - # The first argument is the selector expression. All other arguments - # are used for value substitution. - # - # Throws InvalidSelectorError is the selector expression is invalid. - def initialize(selector, *values) - raise ArgumentError, "CSS expression cannot be empty" if selector.empty? - @source = "" - values = values[0] if values.size == 1 && values[0].is_a?(Array) - - # We need a copy to determine if we failed to parse, and also - # preserve the original pass by-ref statement. - statement = selector.strip.dup - - # Create a simple selector, along with negation. - simple_selector(statement, values).each { |name, value| instance_variable_set("@#{name}", value) } - - @alternates = [] - @depends = nil - - # Alternative selector. - if statement.sub!(/^\s*,\s*/, "") - second = Selector.new(statement, values) - @alternates << second - # If there are alternate selectors, we group them in the top selector. - if alternates = second.instance_variable_get(:@alternates) - second.instance_variable_set(:@alternates, []) - @alternates.concat alternates - end - @source << " , " << second.to_s - # Sibling selector: create a dependency into second selector that will - # match element immediately following this one. - elsif statement.sub!(/^\s*\+\s*/, "") - second = next_selector(statement, values) - @depends = lambda do |element, first| - if element = next_element(element) - second.match(element, first) - end - end - @source << " + " << second.to_s - # Adjacent selector: create a dependency into second selector that will - # match all elements following this one. - elsif statement.sub!(/^\s*~\s*/, "") - second = next_selector(statement, values) - @depends = lambda do |element, first| - matches = [] - while element = next_element(element) - if subset = second.match(element, first) - if first && !subset.empty? - matches << subset.first - break - else - matches.concat subset - end - end - end - matches.empty? ? nil : matches - end - @source << " ~ " << second.to_s - # Child selector: create a dependency into second selector that will - # match a child element of this one. - elsif statement.sub!(/^\s*>\s*/, "") - second = next_selector(statement, values) - @depends = lambda do |element, first| - matches = [] - element.children.each do |child| - if child.tag? && subset = second.match(child, first) - if first && !subset.empty? - matches << subset.first - break - else - matches.concat subset - end - end - end - matches.empty? ? nil : matches - end - @source << " > " << second.to_s - # Descendant selector: create a dependency into second selector that - # will match all descendant elements of this one. Note, - elsif statement =~ /^\s+\S+/ && statement != selector - second = next_selector(statement, values) - @depends = lambda do |element, first| - matches = [] - stack = element.children.reverse - while node = stack.pop - next unless node.tag? - if subset = second.match(node, first) - if first && !subset.empty? - matches << subset.first - break - else - matches.concat subset - end - elsif children = node.children - stack.concat children.reverse - end - end - matches.empty? ? nil : matches - end - @source << " " << second.to_s - else - # The last selector is where we check that we parsed - # all the parts. - unless statement.empty? || statement.strip.empty? - raise ArgumentError, "Invalid selector: #{statement}" - end - end - end - - - # :call-seq: - # match(element, first?) => array or nil - # - # Matches an element against the selector. - # - # For a simple selector this method returns an array with the - # element if the element matches, nil otherwise. - # - # For a complex selector (sibling and descendant) this method - # returns an array with all matching elements, nil if no match is - # found. - # - # Use +first_only=true+ if you are only interested in the first element. - # - # For example: - # if selector.match(element) - # puts "Element is a login form" - # end - def match(element, first_only = false) - # Match element if no element name or element name same as element name - if matched = (!@tag_name || @tag_name == element.name) - # No match if one of the attribute matches failed - for attr in @attributes - if element.attributes[attr[0]] !~ attr[1] - matched = false - break - end - end - end - - # Pseudo class matches (nth-child, empty, etc). - if matched - for pseudo in @pseudo - unless pseudo.call(element) - matched = false - break - end - end - end - - # Negation. Same rules as above, but we fail if a match is made. - if matched && @negation - for negation in @negation - if negation[:tag_name] == element.name - matched = false - else - for attr in negation[:attributes] - if element.attributes[attr[0]] =~ attr[1] - matched = false - break - end - end - end - if matched - for pseudo in negation[:pseudo] - if pseudo.call(element) - matched = false - break - end - end - end - break unless matched - end - end - - # If element matched but depends on another element (child, - # sibling, etc), apply the dependent matches instead. - if matched && @depends - matches = @depends.call(element, first_only) - else - matches = matched ? [element] : nil - end - - # If this selector is part of the group, try all the alternative - # selectors (unless first_only). - if !first_only || !matches - @alternates.each do |alternate| - break if matches && first_only - if subset = alternate.match(element, first_only) - if matches - matches.concat subset - else - matches = subset - end - end - end - end - - matches - end - - - # :call-seq: - # select(root) => array - # - # Selects and returns an array with all matching elements, beginning - # with one node and traversing through all children depth-first. - # Returns an empty array if no match is found. - # - # The root node may be any element in the document, or the document - # itself. - # - # For example: - # selector = HTML::Selector.new "input[type=text]" - # matches = selector.select(element) - # matches.each do |match| - # puts "Found text field with name #{match.attributes['name']}" - # end - def select(root) - matches = [] - stack = [root] - while node = stack.pop - if node.tag? && subset = match(node, false) - subset.each do |match| - matches << match unless matches.any? { |item| item.equal?(match) } - end - elsif children = node.children - stack.concat children.reverse - end - end - matches - end - - - # Similar to #select but returns the first matching element. Returns +nil+ - # if no element matches the selector. - def select_first(root) - stack = [root] - while node = stack.pop - if node.tag? && subset = match(node, true) - return subset.first if !subset.empty? - elsif children = node.children - stack.concat children.reverse - end - end - nil - end - - - def to_s #:nodoc: - @source - end - - - # Returns the next element after this one. Skips sibling text nodes. - # - # With the +name+ argument, returns the next element with that name, - # skipping other sibling elements. - def next_element(element, name = nil) - if siblings = element.parent.children - found = false - siblings.each do |node| - if node.equal?(element) - found = true - elsif found && node.tag? - return node if (name.nil? || node.name == name) - end - end - end - nil - end - - - protected - - - # Creates a simple selector given the statement and array of - # substitution values. - # - # Returns a hash with the values +tag_name+, +attributes+, - # +pseudo+ (classes) and +negation+. - # - # Called the first time with +can_negate+ true to allow - # negation. Called a second time with false since negation - # cannot be negated. - def simple_selector(statement, values, can_negate = true) - tag_name = nil - attributes = [] - pseudo = [] - negation = [] - - # Element name. (Note that in negation, this can come at - # any order, but for simplicity we allow if only first). - statement.sub!(/^(\*|[[:alpha:]][\w\-]*)/) do |match| - match.strip! - tag_name = match.downcase unless match == "*" - @source << match - "" # Remove - end - - # Get identifier, class, attribute name, pseudo or negation. - while true - # Element identifier. - next if statement.sub!(/^#(\?|[\w\-]+)/) do - id = $1 - if id == "?" - id = values.shift - end - @source << "##{id}" - id = Regexp.new("^#{Regexp.escape(id.to_s)}$") unless id.is_a?(Regexp) - attributes << ["id", id] - "" # Remove - end - - # Class name. - next if statement.sub!(/^\.([\w\-]+)/) do - class_name = $1 - @source << ".#{class_name}" - class_name = Regexp.new("(^|\s)#{Regexp.escape(class_name)}($|\s)") unless class_name.is_a?(Regexp) - attributes << ["class", class_name] - "" # Remove - end - - # Attribute value. - next if statement.sub!(/^\[\s*([[:alpha:]][\w\-:]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do - name, equality, value = $1, $2, $3 - if value == "?" - value = values.shift - else - # Handle single and double quotes. - value.strip! - if (value[0] == ?" || value[0] == ?') && value[0] == value[-1] - value = value[1..-2] - end - end - @source << "[#{name}#{equality}'#{value}']" - attributes << [name.downcase.strip, attribute_match(equality, value)] - "" # Remove - end - - # Root element only. - next if statement.sub!(/^:root/) do - pseudo << lambda do |element| - element.parent.nil? || !element.parent.tag? - end - @source << ":root" - "" # Remove - end - - # Nth-child including last and of-type. - next if statement.sub!(/^:nth-(last-)?(child|of-type)\((odd|even|(\d+|\?)|(-?\d*|\?)?n([+\-]\d+|\?)?)\)/) do |match| - reverse = $1 == "last-" - of_type = $2 == "of-type" - @source << ":nth-#{$1}#{$2}(" - case $3 - when "odd" - pseudo << nth_child(2, 1, of_type, reverse) - @source << "odd)" - when "even" - pseudo << nth_child(2, 2, of_type, reverse) - @source << "even)" - when /^(\d+|\?)$/ # b only - b = ($1 == "?" ? values.shift : $1).to_i - pseudo << nth_child(0, b, of_type, reverse) - @source << "#{b})" - when /^(-?\d*|\?)?n([+\-]\d+|\?)?$/ - a = ($1 == "?" ? values.shift : - $1 == "" ? 1 : $1 == "-" ? -1 : $1).to_i - b = ($2 == "?" ? values.shift : $2).to_i - pseudo << nth_child(a, b, of_type, reverse) - @source << (b >= 0 ? "#{a}n+#{b})" : "#{a}n#{b})") - else - raise ArgumentError, "Invalid nth-child #{match}" - end - "" # Remove - end - # First/last child (of type). - next if statement.sub!(/^:(first|last)-(child|of-type)/) do - reverse = $1 == "last" - of_type = $2 == "of-type" - pseudo << nth_child(0, 1, of_type, reverse) - @source << ":#{$1}-#{$2}" - "" # Remove - end - # Only child (of type). - next if statement.sub!(/^:only-(child|of-type)/) do - of_type = $1 == "of-type" - pseudo << only_child(of_type) - @source << ":only-#{$1}" - "" # Remove - end - - # Empty: no child elements or meaningful content (whitespaces - # are ignored). - next if statement.sub!(/^:empty/) do - pseudo << lambda do |element| - empty = true - for child in element.children - if child.tag? || !child.content.strip.empty? - empty = false - break - end - end - empty - end - @source << ":empty" - "" # Remove - end - # Content: match the text content of the element, stripping - # leading and trailing spaces. - next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do - content = $1 - if content == "?" - content = values.shift - elsif (content[0] == ?" || content[0] == ?') && content[0] == content[-1] - content = content[1..-2] - end - @source << ":content('#{content}')" - content = Regexp.new("^#{Regexp.escape(content.to_s)}$") unless content.is_a?(Regexp) - pseudo << lambda do |element| - text = "" - for child in element.children - unless child.tag? - text << child.content - end - end - text.strip =~ content - end - "" # Remove - end - - # Negation. Create another simple selector to handle it. - if statement.sub!(/^:not\(\s*/, "") - raise ArgumentError, "Double negatives are not missing feature" unless can_negate - @source << ":not(" - negation << simple_selector(statement, values, false) - raise ArgumentError, "Negation not closed" unless statement.sub!(/^\s*\)/, "") - @source << ")" - next - end - - # No match: moving on. - break - end - - # Return hash. The keys are mapped to instance variables. - {:tag_name=>tag_name, :attributes=>attributes, :pseudo=>pseudo, :negation=>negation} - end - - - # Create a regular expression to match an attribute value based - # on the equality operator (=, ^=, |=, etc). - def attribute_match(equality, value) - regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s) - case equality - when "=" then - # Match the attribute value in full - Regexp.new("^#{regexp}$") - when "~=" then - # Match a space-separated word within the attribute value - Regexp.new("(^|\s)#{regexp}($|\s)") - when "^=" - # Match the beginning of the attribute value - Regexp.new("^#{regexp}") - when "$=" - # Match the end of the attribute value - Regexp.new("#{regexp}$") - when "*=" - # Match substring of the attribute value - regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp) - when "|=" then - # Match the first space-separated item of the attribute value - Regexp.new("^#{regexp}($|\s)") - else - raise InvalidSelectorError, "Invalid operation/value" unless value.empty? - # Match all attributes values (existence check) - // - end - end - - - # Returns a lambda that can match an element against the nth-child - # pseudo class, given the following arguments: - # * +a+ -- Value of a part. - # * +b+ -- Value of b part. - # * +of_type+ -- True to test only elements of this type (of-type). - # * +reverse+ -- True to count in reverse order (last-). - def nth_child(a, b, of_type, reverse) - # a = 0 means select at index b, if b = 0 nothing selected - return lambda { |element| false } if a == 0 && b == 0 - # a < 0 and b < 0 will never match against an index - return lambda { |element| false } if a < 0 && b < 0 - b = a + b + 1 if b < 0 # b < 0 just picks last element from each group - b -= 1 unless b == 0 # b == 0 is same as b == 1, otherwise zero based - lambda do |element| - # Element must be inside parent element. - return false unless element.parent && element.parent.tag? - index = 0 - # Get siblings, reverse if counting from last. - siblings = element.parent.children - siblings = siblings.reverse if reverse - # Match element name if of-type, otherwise ignore name. - name = of_type ? element.name : nil - found = false - for child in siblings - # Skip text nodes/comments. - if child.tag? && (name == nil || child.name == name) - if a == 0 - # Shortcut when a == 0 no need to go past count - if index == b - found = child.equal?(element) - break - end - elsif a < 0 - # Only look for first b elements - break if index > b - if child.equal?(element) - found = (index % a) == 0 - break - end - else - # Otherwise, break if child found and count == an+b - if child.equal?(element) - found = (index % a) == b - break - end - end - index += 1 - end - end - found - end - end - - - # Creates a only child lambda. Pass +of-type+ to only look at - # elements of its type. - def only_child(of_type) - lambda do |element| - # Element must be inside parent element. - return false unless element.parent && element.parent.tag? - name = of_type ? element.name : nil - other = false - for child in element.parent.children - # Skip text nodes/comments. - if child.tag? && (name == nil || child.name == name) - unless child.equal?(element) - other = true - break - end - end - end - !other - end - end - - - # Called to create a dependent selector (sibling, descendant, etc). - # Passes the remainder of the statement that will be reduced to zero - # eventually, and array of substitution values. - # - # This method is called from four places, so it helps to put it here - # for reuse. The only logic deals with the need to detect comma - # separators (alternate) and apply them to the selector group of the - # top selector. - def next_selector(statement, values) - second = Selector.new(statement, values) - # If there are alternate selectors, we group them in the top selector. - if alternates = second.instance_variable_get(:@alternates) - second.instance_variable_set(:@alternates, []) - @alternates.concat alternates - end - second - end - - end - - - # See HTML::Selector.new - def self.selector(statement, *values) - Selector.new(statement, *values) - end - - - class Tag - - def select(selector, *values) - selector = HTML::Selector.new(selector, values) - selector.select(self) - end - - end - -end diff --git a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb deleted file mode 100644 index adf4e45930..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb +++ /dev/null @@ -1,107 +0,0 @@ -require 'strscan' - -module HTML #:nodoc: - - # A simple HTML tokenizer. It simply breaks a stream of text into tokens, where each - # token is a string. Each string represents either "text", or an HTML element. - # - # This currently assumes valid XHTML, which means no free < or > characters. - # - # Usage: - # - # tokenizer = HTML::Tokenizer.new(text) - # while token = tokenizer.next - # p token - # end - class Tokenizer #:nodoc: - - # The current (byte) position in the text - attr_reader :position - - # The current line number - attr_reader :line - - # Create a new Tokenizer for the given text. - def initialize(text) - text.encode! - @scanner = StringScanner.new(text) - @position = 0 - @line = 0 - @current_line = 1 - end - - # Returns the next token in the sequence, or +nil+ if there are no more tokens in - # the stream. - def next - return nil if @scanner.eos? - @position = @scanner.pos - @line = @current_line - if @scanner.check(/<\S/) - update_current_line(scan_tag) - else - update_current_line(scan_text) - end - end - - private - - # Treat the text at the current position as a tag, and scan it. Supports - # comments, doctype tags, and regular tags, and ignores less-than and - # greater-than characters within quoted strings. - def scan_tag - tag = @scanner.getch - if @scanner.scan(/!--/) # comment - tag << @scanner.matched - tag << (@scanner.scan_until(/--\s*>/) || @scanner.scan_until(/\Z/)) - elsif @scanner.scan(/!\[CDATA\[/) - tag << @scanner.matched - tag << (@scanner.scan_until(/\]\]>/) || @scanner.scan_until(/\Z/)) - elsif @scanner.scan(/!/) # doctype - tag << @scanner.matched - tag << consume_quoted_regions - else - tag << consume_quoted_regions - end - tag - end - - # Scan all text up to the next < character and return it. - def scan_text - "#{@scanner.getch}#{@scanner.scan(/[^<]*/)}" - end - - # Counts the number of newlines in the text and updates the current line - # accordingly. - def update_current_line(text) - text.scan(/\r?\n/) { @current_line += 1 } - end - - # Skips over quoted strings, so that less-than and greater-than characters - # within the strings are ignored. - def consume_quoted_regions - text = "" - loop do - match = @scanner.scan_until(/['"<>]/) or break - - delim = @scanner.matched - if delim == "<" - match = match.chop - @scanner.pos -= 1 - end - - text << match - break if delim == "<" || delim == ">" - - # consume the quoted region - while match = @scanner.scan_until(/[\\#{delim}]/) - text << match - break if @scanner.matched == delim - break if @scanner.eos? - text << @scanner.getch # skip the escaped character - end - end - text - end - end - -end diff --git a/actionview/lib/action_view/vendor/html-scanner/html/version.rb b/actionview/lib/action_view/vendor/html-scanner/html/version.rb deleted file mode 100644 index 6d645c3e14..0000000000 --- a/actionview/lib/action_view/vendor/html-scanner/html/version.rb +++ /dev/null @@ -1,11 +0,0 @@ -module HTML #:nodoc: - module Version #:nodoc: - - MAJOR = 0 - MINOR = 5 - TINY = 3 - - STRING = [ MAJOR, MINOR, TINY ].join(".") - - end -end diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index 7c71fdabd1..d60712255b 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -338,3 +338,5 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +require 'mocha/setup' # FIXME: stop using mocha diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index ab7b961ed2..b3b51ae583 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -536,6 +536,14 @@ class TestController < ApplicationController render :partial => "customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer end + def partial_collection_with_iteration + render partial: "customer_iteration", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ] + end + + def partial_collection_with_as_and_iteration + render partial: "customer_iteration_with_as", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ], as: :client + end + def partial_collection_with_counter render :partial => "customer_counter", :collection => [ Customer.new("david"), Customer.new("mary") ] end @@ -839,7 +847,7 @@ class RenderTest < ActionController::TestCase def test_render_text_with_nil get :render_text_with_nil assert_response 200 - assert_equal ' ', @response.body + assert_equal '', @response.body end # :ported: @@ -1027,7 +1035,7 @@ class RenderTest < ActionController::TestCase def test_rendering_nothing_on_layout get :rendering_nothing_on_layout - assert_equal " ", @response.body + assert_equal '', @response.body end def test_render_to_string_doesnt_break_assigns @@ -1237,6 +1245,16 @@ class RenderTest < ActionController::TestCase assert_equal "david david davidmary mary mary", @response.body end + def test_partial_collection_with_iteration + get :partial_collection_with_iteration + assert_equal "3-0: david-first3-1: mary3-2: christine-last", @response.body + end + + def test_partial_collection_with_as_and_iteration + get :partial_collection_with_as_and_iteration + assert_equal "3-0: david-first3-1: mary3-2: christine-last", @response.body + end + def test_partial_collection_with_counter get :partial_collection_with_counter assert_equal "david0mary1", @response.body diff --git a/actionview/test/active_record_unit.rb b/actionview/test/active_record_unit.rb index 95fbb112c0..cca55c9af4 100644 --- a/actionview/test/active_record_unit.rb +++ b/actionview/test/active_record_unit.rb @@ -57,7 +57,7 @@ class ActiveRecordTestConnector end end - # Load actionpack sqlite tables + # Load actionpack sqlite3 tables def load_schema File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(';').each do |sql| ActiveRecord::Base.connection.execute(sql) unless sql.blank? diff --git a/actionview/test/activerecord/controller_runtime_test.rb b/actionview/test/activerecord/controller_runtime_test.rb index 368bec1c70..b5bfcf12a3 100644 --- a/actionview/test/activerecord/controller_runtime_test.rb +++ b/actionview/test/activerecord/controller_runtime_test.rb @@ -8,8 +8,6 @@ ActionController::Base.send :include, ActiveRecord::Railties::ControllerRuntime class ControllerRuntimeLogSubscriberTest < ActionController::TestCase class LogSubscriberController < ActionController::Base - respond_to :html - def show render :inline => "<%= Project.all %>" end @@ -21,7 +19,7 @@ class ControllerRuntimeLogSubscriberTest < ActionController::TestCase def create ActiveRecord::LogSubscriber.runtime += 100 project = Project.last - respond_with(project, location: url_for(action: :show)) + redirect_to "/" end def redirect diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb index b03fe66e23..0d97ddb2f6 100644 --- a/actionview/test/activerecord/polymorphic_routes_test.rb +++ b/actionview/test/activerecord/polymorphic_routes_test.rb @@ -158,34 +158,38 @@ class PolymorphicRoutesTest < ActionController::TestCase def test_with_nil with_test_routes do - assert_raise ArgumentError, "Nil location provided. Can't build URI." do + exception = assert_raise ArgumentError do polymorphic_url(nil) end + assert_equal "Nil location provided. Can't build URI.", exception.message end end def test_with_empty_list with_test_routes do - assert_raise ArgumentError, "Nil location provided. Can't build URI." do + exception = assert_raise ArgumentError do polymorphic_url([]) end + assert_equal "Nil location provided. Can't build URI.", exception.message end end def test_with_nil_id with_test_routes do - assert_raise ArgumentError, "Nil location provided. Can't build URI." do + exception = assert_raise ArgumentError do polymorphic_url({ :id => nil }) end + assert_equal "Nil location provided. Can't build URI.", exception.message end end def test_with_nil_in_list with_test_routes do - assert_raise ArgumentError, "Nil location provided. Can't build URI." do + exception = assert_raise ArgumentError do @series.save polymorphic_url([nil, @series]) end + assert_equal "Nil location provided. Can't build URI.", exception.message end end diff --git a/actionview/test/fixtures/actionpack/test/_customer_iteration.erb b/actionview/test/fixtures/actionpack/test/_customer_iteration.erb new file mode 100644 index 0000000000..fb530b04a7 --- /dev/null +++ b/actionview/test/fixtures/actionpack/test/_customer_iteration.erb @@ -0,0 +1 @@ +<%= customer_iteration_iteration.size %>-<%= customer_iteration_iteration.index %>: <%= customer_iteration.name %><%= '-first' if customer_iteration_iteration.first? %><%= '-last' if customer_iteration_iteration.last? %>
\ No newline at end of file diff --git a/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb b/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb new file mode 100644 index 0000000000..57297d0564 --- /dev/null +++ b/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb @@ -0,0 +1 @@ +<%= client_iteration.size %>-<%= client_iteration.index %>: <%= client.name %><%= '-first' if client_iteration.first? %><%= '-last' if client_iteration.last? %>
\ No newline at end of file diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 343681b5a9..d789a5ca27 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -546,6 +546,14 @@ class AssetTagHelperTest < ActionView::TestCase assert_equal "http://cdn.example.com/images/file.png", image_path("file.png") end + def test_image_url_with_asset_host_proc_returning_nil + @controller.config.asset_host = Proc.new { nil } + @controller.request = Struct.new(:base_url, :script_name).new("http://www.example.com", nil) + + assert_equal "/images/rails.png", image_path("rails.png") + assert_equal "http://www.example.com/images/rails.png", image_url("rails.png") + end + def test_caching_image_path_with_caching_and_proc_asset_host_using_request @controller.config.asset_host = Proc.new do |source, request| if request.ssl? diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb index 63b5ac0fab..68b44c4f0d 100644 --- a/actionview/test/template/atom_feed_helper_test.rb +++ b/actionview/test/template/atom_feed_helper_test.rb @@ -254,7 +254,7 @@ class AtomFeedTest < ActionController::TestCase def test_self_url_should_default_to_current_request_url with_restful_routing(:scrolls) do get :index, :id => "defaults" - assert_select "link[rel=self][href=http://www.nextangle.com/scrolls?id=defaults]" + assert_select "link[rel=self][href=\"http://www.nextangle.com/scrolls?id=defaults\"]" end end @@ -318,22 +318,22 @@ class AtomFeedTest < ActionController::TestCase with_restful_routing(:scrolls) do get :index, :id => "feed_with_xhtml_content" assert_match %r{xmlns="http://www.w3.org/1999/xhtml"}, @response.body - assert_select "summary div p", :text => "Something Boring" - assert_select "summary div p", :text => "after 2" + assert_select "summary", :text => /Something Boring/ + assert_select "summary", :text => /after 2/ end end def test_feed_entry_type_option_default_to_text_html with_restful_routing(:scrolls) do get :index, :id => 'defaults' - assert_select "entry link[rel=alternate][type=text/html]" + assert_select "entry link[rel=alternate][type=\"text/html\"]" end end def test_feed_entry_type_option_specified with_restful_routing(:scrolls) do get :index, :id => 'entry_type_options' - assert_select "entry link[rel=alternate][type=text/xml]" + assert_select "entry link[rel=alternate][type=\"text/xml\"]" end end diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index b86ae910c4..0cdb130710 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -1652,9 +1652,9 @@ class DateHelperTest < ActionView::TestCase concat f.date_select(:written_on) end - expected = "<select id='post_written_on_1i' name='post[written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n" - expected << "<select id='post_written_on_2i' name='post[written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n" - expected << "<select id='post_written_on_3i' name='post[written_on(3i)]'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n" + expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n} + expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n} + expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n} assert_dom_equal(expected, output_buffer) end @@ -1668,9 +1668,9 @@ class DateHelperTest < ActionView::TestCase concat f.date_select(:written_on) end - expected = "<select id='post_#{id}_written_on_1i' name='post[#{id}][written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n" - expected << "<select id='post_#{id}_written_on_2i' name='post[#{id}][written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n" - expected << "<select id='post_#{id}_written_on_3i' name='post[#{id}][written_on(3i)]'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n" + expected = %{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n} + expected << %{<select id="post_#{id}_written_on_2i" name="post[#{id}][written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n} + expected << %{<select id="post_#{id}_written_on_3i" name="post[#{id}][written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n} assert_dom_equal(expected, output_buffer) end @@ -1684,9 +1684,10 @@ class DateHelperTest < ActionView::TestCase concat f.date_select(:written_on) end - expected = "<select id='post_#{id}_written_on_1i' name='post[#{id}][written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n" - expected << "<select id='post_#{id}_written_on_2i' name='post[#{id}][written_on(2i)]'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n" - expected << "<select id='post_#{id}_written_on_3i' name='post[#{id}][written_on(3i)]'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n" + + expected = %{<select id="post_#{id}_written_on_1i" name="post[#{id}][written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n} + expected << %{<select id="post_#{id}_written_on_2i" name="post[#{id}][written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n} + expected << %{<select id="post_#{id}_written_on_3i" name="post[#{id}][written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n} assert_dom_equal(expected, output_buffer) end @@ -2374,11 +2375,11 @@ class DateHelperTest < ActionView::TestCase concat f.datetime_select(:updated_at, {}, :class => 'selector') end - expected = "<select id='post_updated_at_1i' name='post[updated_at(1i)]' class='selector'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n" - expected << "<select id='post_updated_at_2i' name='post[updated_at(2i)]' class='selector'>\n<option value='1'>January</option>\n<option value='2'>February</option>\n<option value='3'>March</option>\n<option value='4'>April</option>\n<option value='5'>May</option>\n<option selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n" - expected << "<select id='post_updated_at_3i' name='post[updated_at(3i)]' class='selector'>\n<option value='1'>1</option>\n<option value='2'>2</option>\n<option value='3'>3</option>\n<option value='4'>4</option>\n<option value='5'>5</option>\n<option value='6'>6</option>\n<option value='7'>7</option>\n<option value='8'>8</option>\n<option value='9'>9</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n" - expected << " — <select id='post_updated_at_4i' name='post[updated_at(4i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option selected='selected' value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n</select>\n" - expected << " : <select id='post_updated_at_5i' name='post[updated_at(5i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n<option value='32'>32</option>\n<option value='33'>33</option>\n<option value='34'>34</option>\n<option selected='selected' value='35'>35</option>\n<option value='36'>36</option>\n<option value='37'>37</option>\n<option value='38'>38</option>\n<option value='39'>39</option>\n<option value='40'>40</option>\n<option value='41'>41</option>\n<option value='42'>42</option>\n<option value='43'>43</option>\n<option value='44'>44</option>\n<option value='45'>45</option>\n<option value='46'>46</option>\n<option value='47'>47</option>\n<option value='48'>48</option>\n<option value='49'>49</option>\n<option value='50'>50</option>\n<option value='51'>51</option>\n<option value='52'>52</option>\n<option value='53'>53</option>\n<option value='54'>54</option>\n<option value='55'>55</option>\n<option value='56'>56</option>\n<option value='57'>57</option>\n<option value='58'>58</option>\n<option value='59'>59</option>\n</select>\n" + expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n} + expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]" class="selector">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option selected="selected" value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n} + expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]" class="selector">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option selected="selected" value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n} + expected << %{ — <select id="post_updated_at_4i" name="post[updated_at(4i)]" class="selector">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option selected="selected" value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n</select>\n} + expected << %{ : <select id="post_updated_at_5i" name="post[updated_at(5i)]" class="selector">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option selected="selected" value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n</select>\n} assert_dom_equal expected, output_buffer end diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb index 6c780f2297..bb375076c6 100644 --- a/actionview/test/template/dependency_tracker_test.rb +++ b/actionview/test/template/dependency_tracker_test.rb @@ -60,6 +60,21 @@ class ERBTrackerTest < Minitest::Test assert_equal ["messages/message123"], tracker.dependencies end + def test_dependency_of_template_partial_with_layout + skip # FIXME: Needs to be fixed properly, right now we can only match one dependency per line. Need multiple! + template = FakeTemplate.new("<%# render partial: 'messages/show', layout: 'messages/layout' %>", :erb) + tracker = make_tracker("multiple/_dependencies", template) + + assert_equal ["messages/layout", "messages/show"], tracker.dependencies + end + + def test_dependency_of_template_layout_standalone + template = FakeTemplate.new("<%# render layout: 'messages/layout' do %>", :erb) + tracker = make_tracker("messages/layout", template) + + assert_equal ["messages/layout"], tracker.dependencies + end + def test_finds_dependency_in_correct_directory template = FakeTemplate.new("<%# render(message.topic) %>", :erb) tracker = make_tracker("messages/_message", template) diff --git a/actionview/test/template/erb_util_test.rb b/actionview/test/template/erb_util_test.rb index 9bacbba908..3bb84cbc50 100644 --- a/actionview/test/template/erb_util_test.rb +++ b/actionview/test/template/erb_util_test.rb @@ -92,6 +92,7 @@ class ErbUtilTest < ActiveSupport::TestCase def test_html_escape_once assert_equal '1 <>&"' 2 & 3', html_escape_once('1 <>&"\' 2 & 3') + assert_equal " ' ' λ λ " ' < > ", html_escape_once(" ' ' λ λ \" ' < > ") end def test_html_escape_once_returns_unsafe_strings_when_passed_unsafe_strings diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb index 5e991d87ad..b193d387c3 100644 --- a/actionview/test/template/form_collections_helper_test.rb +++ b/actionview/test/template/form_collections_helper_test.rb @@ -185,8 +185,8 @@ class FormCollectionsHelperTest < ActionView::TestCase p.collection_radio_buttons :category_id, collection, :id, :name end - assert_select 'input#post_category_id_1[type=radio][value=1]' - assert_select 'input#post_category_id_2[type=radio][value=2]' + assert_select 'input#post_category_id_1[type=radio][value="1"]' + assert_select 'input#post_category_id_2[type=radio][value="2"]' assert_select 'label[for=post_category_id_1]', 'Category 1' assert_select 'label[for=post_category_id_2]', 'Category 2' @@ -203,36 +203,36 @@ class FormCollectionsHelperTest < ActionView::TestCase collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_check_boxes :user, :category_ids, collection, :id, :name - assert_select 'input#user_category_ids_1[type=checkbox][value=1]' - assert_select 'input#user_category_ids_2[type=checkbox][value=2]' + assert_select 'input#user_category_ids_1[type=checkbox][value="1"]' + assert_select 'input#user_category_ids_2[type=checkbox][value="2"]' end test 'collection check boxes generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_check_boxes :user, :category_ids, collection, :id, :name - assert_select "input[type=hidden][name='user[category_ids][]'][value=]", :count => 1 + assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 1 end test 'collection check boxes generates a hidden field using the given :name in :html_options' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_check_boxes :user, :category_ids, collection, :id, :name, {}, {name: "user[other_category_ids][]"} - assert_select "input[type=hidden][name='user[other_category_ids][]'][value=]", :count => 1 + assert_select "input[type=hidden][name='user[other_category_ids][]'][value='']", :count => 1 end test 'collection check boxes generates a hidden field with index if it was provided' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_check_boxes :user, :category_ids, collection, :id, :name, { index: 322 } - assert_select "input[type=hidden][name='user[322][category_ids][]'][value=]", count: 1 + assert_select "input[type=hidden][name='user[322][category_ids][]'][value='']", count: 1 end test 'collection check boxes does not generate a hidden field if include_hidden option is false' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_check_boxes :user, :category_ids, collection, :id, :name, include_hidden: false - assert_select "input[type=hidden][name='user[category_ids][]'][value=]", :count => 0 + assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 0 end test 'collection check boxes accepts a collection and generate a series of checkboxes with labels for label method' do @@ -260,8 +260,8 @@ class FormCollectionsHelperTest < ActionView::TestCase collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]] with_collection_check_boxes :user, :active, collection, :first, :second - assert_select 'input[type=checkbox][value=1].foo' - assert_select 'input[type=checkbox][value=2].bar' + assert_select 'input[type=checkbox][value="1"].foo' + assert_select 'input[type=checkbox][value="2"].bar' end test 'collection check boxes sets the label class defined inside the block' do @@ -286,27 +286,27 @@ class FormCollectionsHelperTest < ActionView::TestCase collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => [1, 3] - assert_select 'input[type=checkbox][value=1][checked=checked]' - assert_select 'input[type=checkbox][value=3][checked=checked]' - assert_no_select 'input[type=checkbox][value=2][checked=checked]' + assert_select 'input[type=checkbox][value="1"][checked=checked]' + assert_select 'input[type=checkbox][value="3"][checked=checked]' + assert_no_select 'input[type=checkbox][value="2"][checked=checked]' end test 'collection check boxes accepts selected string values as :checked option' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => ['1', '3'] - assert_select 'input[type=checkbox][value=1][checked=checked]' - assert_select 'input[type=checkbox][value=3][checked=checked]' - assert_no_select 'input[type=checkbox][value=2][checked=checked]' + assert_select 'input[type=checkbox][value="1"][checked=checked]' + assert_select 'input[type=checkbox][value="3"][checked=checked]' + assert_no_select 'input[type=checkbox][value="2"][checked=checked]' end test 'collection check boxes accepts a single checked value' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => 3 - assert_select 'input[type=checkbox][value=3][checked=checked]' - assert_no_select 'input[type=checkbox][value=1][checked=checked]' - assert_no_select 'input[type=checkbox][value=2][checked=checked]' + assert_select 'input[type=checkbox][value="3"][checked=checked]' + assert_no_select 'input[type=checkbox][value="1"][checked=checked]' + assert_no_select 'input[type=checkbox][value="2"][checked=checked]' end test 'collection check boxes accepts selected values as :checked option and override the model values' do @@ -317,71 +317,71 @@ class FormCollectionsHelperTest < ActionView::TestCase p.collection_check_boxes :category_ids, collection, :first, :last, :checked => [1, 3] end - assert_select 'input[type=checkbox][value=1][checked=checked]' - assert_select 'input[type=checkbox][value=3][checked=checked]' - assert_no_select 'input[type=checkbox][value=2][checked=checked]' + assert_select 'input[type=checkbox][value="1"][checked=checked]' + assert_select 'input[type=checkbox][value="3"][checked=checked]' + assert_no_select 'input[type=checkbox][value="2"][checked=checked]' end test 'collection check boxes accepts multiple disabled items' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => [1, 3] - assert_select 'input[type=checkbox][value=1][disabled=disabled]' - assert_select 'input[type=checkbox][value=3][disabled=disabled]' - assert_no_select 'input[type=checkbox][value=2][disabled=disabled]' + assert_select 'input[type=checkbox][value="1"][disabled=disabled]' + assert_select 'input[type=checkbox][value="3"][disabled=disabled]' + assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]' end test 'collection check boxes accepts single disabled item' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => 1 - assert_select 'input[type=checkbox][value=1][disabled=disabled]' - assert_no_select 'input[type=checkbox][value=3][disabled=disabled]' - assert_no_select 'input[type=checkbox][value=2][disabled=disabled]' + assert_select 'input[type=checkbox][value="1"][disabled=disabled]' + assert_no_select 'input[type=checkbox][value="3"][disabled=disabled]' + assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]' end test 'collection check boxes accepts a proc to disabled items' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => proc { |i| i.first == 1 } - assert_select 'input[type=checkbox][value=1][disabled=disabled]' - assert_no_select 'input[type=checkbox][value=3][disabled=disabled]' - assert_no_select 'input[type=checkbox][value=2][disabled=disabled]' + assert_select 'input[type=checkbox][value="1"][disabled=disabled]' + assert_no_select 'input[type=checkbox][value="3"][disabled=disabled]' + assert_no_select 'input[type=checkbox][value="2"][disabled=disabled]' end test 'collection check boxes accepts multiple readonly items' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => [1, 3] - assert_select 'input[type=checkbox][value=1][readonly=readonly]' - assert_select 'input[type=checkbox][value=3][readonly=readonly]' - assert_no_select 'input[type=checkbox][value=2][readonly=readonly]' + assert_select 'input[type=checkbox][value="1"][readonly=readonly]' + assert_select 'input[type=checkbox][value="3"][readonly=readonly]' + assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]' end test 'collection check boxes accepts single readonly item' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => 1 - assert_select 'input[type=checkbox][value=1][readonly=readonly]' - assert_no_select 'input[type=checkbox][value=3][readonly=readonly]' - assert_no_select 'input[type=checkbox][value=2][readonly=readonly]' + assert_select 'input[type=checkbox][value="1"][readonly=readonly]' + assert_no_select 'input[type=checkbox][value="3"][readonly=readonly]' + assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]' end test 'collection check boxes accepts a proc to readonly items' do collection = (1..3).map{|i| [i, "Category #{i}"] } with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => proc { |i| i.first == 1 } - assert_select 'input[type=checkbox][value=1][readonly=readonly]' - assert_no_select 'input[type=checkbox][value=3][readonly=readonly]' - assert_no_select 'input[type=checkbox][value=2][readonly=readonly]' + assert_select 'input[type=checkbox][value="1"][readonly=readonly]' + assert_no_select 'input[type=checkbox][value="3"][readonly=readonly]' + assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]' end test 'collection check boxes accepts html options' do collection = [[1, 'Category 1'], [2, 'Category 2']] with_collection_check_boxes :user, :category_ids, collection, :first, :last, {}, :class => 'check' - assert_select 'input.check[type=checkbox][value=1]' - assert_select 'input.check[type=checkbox][value=2]' + assert_select 'input.check[type=checkbox][value="1"]' + assert_select 'input.check[type=checkbox][value="2"]' end test 'collection check boxes with fields for' do @@ -390,8 +390,8 @@ class FormCollectionsHelperTest < ActionView::TestCase p.collection_check_boxes :category_ids, collection, :id, :name end - assert_select 'input#post_category_ids_1[type=checkbox][value=1]' - assert_select 'input#post_category_ids_2[type=checkbox][value=2]' + assert_select 'input#post_category_ids_1[type=checkbox][value="1"]' + assert_select 'input#post_category_ids_2[type=checkbox][value="2"]' assert_select 'label[for=post_category_ids_1]', 'Category 1' assert_select 'label[for=post_category_ids_2]', 'Category 2' diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 48073225cb..d944214961 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -59,6 +59,35 @@ class FormHelperTest < ActionView::TestCase } } + I18n.backend.store_translations 'placeholder', { + activemodel: { + attributes: { + post: { + cost: "Total cost" + }, + :"post/cost" => { + uk: "Pounds" + } + } + }, + helpers: { + placeholder: { + post: { + title: "What is this about?", + written_on: { + spanish: "Escrito en" + }, + comments: { + body: "Write body here" + } + }, + tag: { + value: "Tag" + } + } + } + } + @post = Post.new @comment = Comment.new def @post.errors() @@ -297,6 +326,68 @@ class FormHelperTest < ActionView::TestCase ) end + def test_text_field_placeholder_without_locales + with_locale :placeholder do + assert_dom_equal('<input id="post_body" name="post[body]" placeholder="Body" type="text" value="Back to the hill and over it again!" />', text_field(:post, :body, placeholder: true)) + end + end + + def test_text_field_placeholder_with_locales + with_locale :placeholder do + assert_dom_equal('<input id="post_title" name="post[title]" placeholder="What is this about?" type="text" value="Hello World" />', text_field(:post, :title, placeholder: true)) + end + end + + def test_text_field_placeholder_with_human_attribute_name + with_locale :placeholder do + assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Total cost" type="text" />', text_field(:post, :cost, placeholder: true)) + end + end + + def test_text_field_placeholder_with_human_attribute_name_and_value + with_locale :placeholder do + assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Pounds" type="text" />', text_field(:post, :cost, placeholder: "uk")) + end + end + + def test_text_field_placeholder_with_locales_and_value + with_locale :placeholder do + assert_dom_equal('<input id="post_written_on" name="post[written_on]" placeholder="Escrito en" type="text" value="2004-06-15" />', text_field(:post, :written_on, placeholder: "spanish")) + end + end + + def test_text_field_placeholder_with_locales_and_nested_attributes + with_locale :placeholder do + form_for(@post, html: { id: 'create-post' }) do |f| + f.fields_for(:comments) do |cf| + concat cf.text_field(:body, placeholder: true) + end + end + + expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do + '<input id="post_comments_attributes_0_body" name="post[comments_attributes][0][body]" placeholder="Write body here" type="text" />' + end + + assert_dom_equal expected, output_buffer + end + end + + def test_text_field_placeholder_with_locales_fallback_and_nested_attributes + with_locale :placeholder do + form_for(@post, html: { id: 'create-post' }) do |f| + f.fields_for(:tags) do |cf| + concat cf.text_field(:value, placeholder: true) + end + end + + expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do + '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" placeholder="Tag" type="text" value="new tag" />' + end + + assert_dom_equal expected, output_buffer + end + end + def test_text_field assert_dom_equal( '<input id="post_title" name="post[title]" type="text" value="Hello World" />', @@ -665,6 +756,83 @@ class FormHelperTest < ActionView::TestCase ) end + def test_text_area_placeholder_without_locales + with_locale :placeholder do + assert_dom_equal( + %{<textarea id="post_body" name="post[body]" placeholder="Body">\nBack to the hill and over it again!</textarea>}, + text_area(:post, :body, placeholder: true) + ) + end + end + + def test_text_area_placeholder_with_locales + with_locale :placeholder do + assert_dom_equal( + %{<textarea id="post_title" name="post[title]" placeholder="What is this about?">\nHello World</textarea>}, + text_area(:post, :title, placeholder: true) + ) + end + end + + def test_text_area_placeholder_with_human_attribute_name + with_locale :placeholder do + assert_dom_equal( + %{<textarea id="post_cost" name="post[cost]" placeholder="Total cost">\n</textarea>}, + text_area(:post, :cost, placeholder: true) + ) + end + end + + def test_text_area_placeholder_with_human_attribute_name_and_value + with_locale :placeholder do + assert_dom_equal( + %{<textarea id="post_cost" name="post[cost]" placeholder="Pounds">\n</textarea>}, + text_area(:post, :cost, placeholder: "uk") + ) + end + end + + def test_text_area_placeholder_with_locales_and_value + with_locale :placeholder do + assert_dom_equal( + %{<textarea id="post_written_on" name="post[written_on]" placeholder="Escrito en">\n2004-06-15</textarea>}, + text_area(:post, :written_on, placeholder: "spanish") + ) + end + end + + def test_text_area_placeholder_with_locales_and_nested_attributes + with_locale :placeholder do + form_for(@post, html: { id: 'create-post' }) do |f| + f.fields_for(:comments) do |cf| + concat cf.text_area(:body, placeholder: true) + end + end + + expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do + %{<textarea id="post_comments_attributes_0_body" name="post[comments_attributes][0][body]" placeholder="Write body here">\n</textarea>} + end + + assert_dom_equal expected, output_buffer + end + end + + def test_text_area_placeholder_with_locales_fallback_and_nested_attributes + with_locale :placeholder do + form_for(@post, html: { id: 'create-post' }) do |f| + f.fields_for(:tags) do |cf| + concat cf.text_area(:value, placeholder: true) + end + end + + expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do + %{<textarea id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" placeholder="Tag">\nnew tag</textarea>} + end + + assert_dom_equal expected, output_buffer + end + end + def test_text_area assert_dom_equal( %{<textarea id="post_body" name="post[body]">\nBack to the hill and over it again!</textarea>}, @@ -694,6 +862,13 @@ class FormHelperTest < ActionView::TestCase ) end + def test_text_area_with_value_before_type_cast + assert_dom_equal( + %{<textarea id="post_id" name="post[id]">\n123</textarea>}, + text_area("post", "id") + ) + end + def test_text_area_with_html_entities @post.body = "The HTML Entity for & is &" assert_dom_equal( @@ -776,6 +951,22 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal(expected, date_field("post", "written_on")) end + def test_date_field_with_string_values_for_min_and_max + expected = %{<input id="post_written_on" max="2010-08-15" min="2000-06-15" name="post[written_on]" type="date" value="2004-06-15" />} + @post.written_on = DateTime.new(2004, 6, 15) + min_value = "2000-06-15" + max_value = "2010-08-15" + assert_dom_equal(expected, date_field("post", "written_on", min: min_value, max: max_value)) + end + + def test_date_field_with_invalid_string_values_for_min_and_max + expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "foo" + max_value = "bar" + assert_dom_equal(expected, date_field("post", "written_on", min: min_value, max: max_value)) + end + def test_time_field expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="00:00:00.000" />} assert_dom_equal(expected, time_field("post", "written_on")) @@ -811,6 +1002,22 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal(expected, time_field("post", "written_on")) end + def test_time_field_with_string_values_for_min_and_max + expected = %{<input id="post_written_on" max="10:25:00.000" min="20:45:30.000" name="post[written_on]" type="time" value="01:02:03.000" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "20:45:30.000" + max_value = "10:25:00.000" + assert_dom_equal(expected, time_field("post", "written_on", min: min_value, max: max_value)) + end + + def test_time_field_with_invalid_string_values_for_min_and_max + expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="01:02:03.000" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "foo" + max_value = "bar" + assert_dom_equal(expected, time_field("post", "written_on", min: min_value, max: max_value)) + end + def test_datetime_field expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T00:00:00.000+0000" />} assert_dom_equal(expected, datetime_field("post", "written_on")) @@ -852,6 +1059,22 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal(expected, datetime_field("post", "written_on")) end + def test_datetime_field_with_string_values_for_min_and_max + expected = %{<input id="post_written_on" max="2010-08-15T10:25:00.000+0000" min="2000-06-15T20:45:30.000+0000" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "2000-06-15T20:45:30.000+0000" + max_value = "2010-08-15T10:25:00.000+0000" + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) + end + + def test_datetime_field_with_invalid_string_values_for_min_and_max + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "foo" + max_value = "bar" + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) + end + def test_datetime_local_field expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T00:00:00" />} assert_dom_equal(expected, datetime_local_field("post", "written_on")) @@ -887,6 +1110,22 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal(expected, datetime_local_field("post", "written_on")) end + def test_datetime_local_field_with_string_values_for_min_and_max + expected = %{<input id="post_written_on" max="2010-08-15T10:25:00" min="2000-06-15T20:45:30" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "2000-06-15T20:45:30" + max_value = "2010-08-15T10:25:00" + assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value)) + end + + def test_datetime_local_field_with_invalid_string_values_for_min_and_max + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} + @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) + min_value = "foo" + max_value = "bar" + assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value)) + end + def test_month_field expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />} assert_dom_equal(expected, month_field("post", "written_on")) diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb index fbafb7aa08..d25fa3706f 100644 --- a/actionview/test/template/form_options_helper_test.rb +++ b/actionview/test/template/form_options_helper_test.rb @@ -591,6 +591,19 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_under_fields_for_with_block_without_options + @post = Post.new + + output_buffer = fields_for :post, @post do |f| + concat(f.select(:category) {}) + end + + assert_dom_equal( + "<select id=\"post_category\" name=\"post[category]\"></select>", + output_buffer + ) + end + def test_select_with_multiple_to_add_hidden_input output_buffer = select(:post, :category, "", {}, :multiple => true) assert_dom_equal( diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index 18c739674a..771e3fefc3 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -632,6 +632,6 @@ class FormTagHelperTest < ActionView::TestCase private def root_elem(rendered_content) - HTML::Document.new(rendered_content).root.children[0] + Nokogiri::HTML::DocumentFragment.parse(rendered_content).children.first # extract from nodeset end end diff --git a/actionview/test/template/html-scanner/cdata_node_test.rb b/actionview/test/template/html-scanner/cdata_node_test.rb deleted file mode 100644 index 9b58174641..0000000000 --- a/actionview/test/template/html-scanner/cdata_node_test.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'abstract_unit' - -class CDATANodeTest < ActiveSupport::TestCase - def setup - @node = HTML::CDATA.new(nil, 0, 0, "<p>howdy</p>") - end - - def test_to_s - assert_equal "<![CDATA[<p>howdy</p>]]>", @node.to_s - end - - def test_content - assert_equal "<p>howdy</p>", @node.content - end -end diff --git a/actionview/test/template/html-scanner/document_test.rb b/actionview/test/template/html-scanner/document_test.rb deleted file mode 100644 index 17f045d549..0000000000 --- a/actionview/test/template/html-scanner/document_test.rb +++ /dev/null @@ -1,148 +0,0 @@ -require 'abstract_unit' - -class DocumentTest < ActiveSupport::TestCase - def test_handle_doctype - doc = nil - assert_nothing_raised do - doc = HTML::Document.new <<-HTML.strip - <!DOCTYPE "blah" "blah" "blah"> - <html> - </html> - HTML - end - assert_equal 3, doc.root.children.length - assert_equal %{<!DOCTYPE "blah" "blah" "blah">}, doc.root.children[0].content - assert_match %r{\s+}m, doc.root.children[1].content - assert_equal "html", doc.root.children[2].name - end - - def test_find_img - doc = HTML::Document.new <<-HTML.strip - <html> - <body> - <p><img src="hello.gif"></p> - </body> - </html> - HTML - assert doc.find(:tag=>"img", :attributes=>{"src"=>"hello.gif"}) - end - - def test_find_all - doc = HTML::Document.new <<-HTML.strip - <html> - <body> - <p class="test"><img src="hello.gif"></p> - <div class="foo"> - <p class="test">something</p> - <p>here is <em class="test">more</em></p> - </div> - </body> - </html> - HTML - all = doc.find_all :attributes => { :class => "test" } - assert_equal 3, all.length - assert_equal [ "p", "p", "em" ], all.map { |n| n.name } - end - - def test_find_with_text - doc = HTML::Document.new <<-HTML.strip - <html> - <body> - <p>Some text</p> - </body> - </html> - HTML - assert doc.find(:content => "Some text") - assert doc.find(:tag => "p", :child => { :content => "Some text" }) - assert doc.find(:tag => "p", :child => "Some text") - assert doc.find(:tag => "p", :content => "Some text") - end - - def test_parse_xml - assert_nothing_raised { HTML::Document.new("<tags><tag/></tags>", true, true) } - assert_nothing_raised { HTML::Document.new("<outer><link>something</link></outer>", true, true) } - end - - def test_parse_document - doc = HTML::Document.new(<<-HTML) - <div> - <h2>blah</h2> - <table> - </table> - </div> - HTML - assert_not_nil doc.find(:tag => "div", :children => { :count => 1, :only => { :tag => "table" } }) - end - - def test_tag_nesting_nothing_to_s - doc = HTML::Document.new("<tag></tag>") - assert_equal "<tag></tag>", doc.root.to_s - end - - def test_tag_nesting_space_to_s - doc = HTML::Document.new("<tag> </tag>") - assert_equal "<tag> </tag>", doc.root.to_s - end - - def test_tag_nesting_text_to_s - doc = HTML::Document.new("<tag>text</tag>") - assert_equal "<tag>text</tag>", doc.root.to_s - end - - def test_tag_nesting_tag_to_s - doc = HTML::Document.new("<tag><nested /></tag>") - assert_equal "<tag><nested /></tag>", doc.root.to_s - end - - def test_parse_cdata - doc = HTML::Document.new(<<-HTML) -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> - <head> - <title><![CDATA[<br>]]></title> - </head> - <body> - <p>this document has <br> for a title</p> - </body> -</html> -HTML - - assert_nil doc.find(:tag => "title", :descendant => { :tag => "br" }) - assert doc.find(:tag => "title", :child => "<br>") - end - - def test_find_empty_tag - doc = HTML::Document.new("<div id='map'></div>") - assert_nil doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /./) - assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /\A\Z/) - assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /^$/) - assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => "") - assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => nil) - end - - def test_parse_invalid_document - assert_nothing_raised do - HTML::Document.new("<html> - <table> - <tr> - <td style=\"color: #FFFFFF; height: 17px; onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" style=\"cursor:pointer; height: 17px;\"; nowrap onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" onmouseout=\"this.bgColor='#0066cc'; this.style.color='#FFFFFF'\" onmouseover=\"this.bgColor='#ffffff'; this.style.color='#0033cc'\">About Us</td> - </tr> - </table> - </html>") - end - end - - def test_invalid_document_raises_exception_when_strict - assert_raise RuntimeError do - HTML::Document.new("<html> - <table> - <tr> - <td style=\"color: #FFFFFF; height: 17px; onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" style=\"cursor:pointer; height: 17px;\"; nowrap onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" onmouseout=\"this.bgColor='#0066cc'; this.style.color='#FFFFFF'\" onmouseover=\"this.bgColor='#ffffff'; this.style.color='#0033cc'\">About Us</td> - </tr> - </table> - </html>", true) - end - end - -end diff --git a/actionview/test/template/html-scanner/node_test.rb b/actionview/test/template/html-scanner/node_test.rb deleted file mode 100644 index 5b5d092036..0000000000 --- a/actionview/test/template/html-scanner/node_test.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'abstract_unit' - -class NodeTest < ActiveSupport::TestCase - - class MockNode - def initialize(matched, value) - @matched = matched - @value = value - end - - def find(conditions) - @matched && self - end - - def to_s - @value.to_s - end - end - - def setup - @node = HTML::Node.new("parent") - @node.children.concat [MockNode.new(false,1), MockNode.new(true,"two"), MockNode.new(false,:three)] - end - - def test_match - assert !@node.match("foo") - end - - def test_tag - assert !@node.tag? - end - - def test_to_s - assert_equal "1twothree", @node.to_s - end - - def test_find - assert_equal "two", @node.find('blah').to_s - end - - def test_parse_strict - s = "<b foo='hello'' bar='baz'>" - assert_raise(RuntimeError) { HTML::Node.parse(nil,0,0,s) } - end - - def test_parse_relaxed - s = "<b foo='hello'' bar='baz'>" - node = nil - assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) } - assert node.attributes.has_key?("foo") - assert !node.attributes.has_key?("bar") - end - - def test_to_s_with_boolean_attrs - s = "<b foo bar>" - node = HTML::Node.parse(nil,0,0,s) - assert node.attributes.has_key?("foo") - assert node.attributes.has_key?("bar") - assert "<b foo bar>", node.to_s - end - - def test_parse_with_unclosed_tag - s = "<span onmouseover='bang'" - node = nil - assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) } - assert node.attributes.has_key?("onmouseover") - end - - def test_parse_with_valid_cdata_section - s = "<![CDATA[<span>contents</span>]]>" - node = nil - assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) } - assert_kind_of HTML::CDATA, node - assert_equal '<span>contents</span>', node.content - end - - def test_parse_strict_with_unterminated_cdata_section - s = "<![CDATA[neverending..." - assert_raise(RuntimeError) { HTML::Node.parse(nil,0,0,s) } - end - - def test_parse_relaxed_with_unterminated_cdata_section - s = "<![CDATA[neverending..." - node = nil - assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) } - assert_kind_of HTML::CDATA, node - assert_equal 'neverending...', node.content - end -end diff --git a/actionview/test/template/html-scanner/sanitizer_test.rb b/actionview/test/template/html-scanner/sanitizer_test.rb deleted file mode 100644 index b1c1b83807..0000000000 --- a/actionview/test/template/html-scanner/sanitizer_test.rb +++ /dev/null @@ -1,330 +0,0 @@ -require 'abstract_unit' - -class SanitizerTest < ActionController::TestCase - def setup - @sanitizer = nil # used by assert_sanitizer - end - - def test_strip_tags_with_quote - sanitizer = HTML::FullSanitizer.new - string = '<" <img src="trollface.gif" onload="alert(1)"> hi' - - assert_equal ' hi', sanitizer.sanitize(string) - end - - def test_strip_tags - sanitizer = HTML::FullSanitizer.new - assert_equal("<<<bad html", sanitizer.sanitize("<<<bad html")) - assert_equal("<<", sanitizer.sanitize("<<<bad html>")) - assert_equal("Dont touch me", sanitizer.sanitize("Dont touch me")) - assert_equal("This is a test.", sanitizer.sanitize("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>")) - assert_equal("Weirdos", sanitizer.sanitize("Wei<<a>a onclick='alert(document.cookie);'</a>/>rdos")) - assert_equal("This is a test.", sanitizer.sanitize("This is a test.")) - assert_equal( - %{This is a test.\n\n\nIt no longer contains any HTML.\n}, sanitizer.sanitize( - %{<title>This is <b>a <a href="" target="_blank">test</a></b>.</title>\n\n<!-- it has a comment -->\n\n<p>It no <b>longer <strong>contains <em>any <strike>HTML</strike></em>.</strong></b></p>\n})) - assert_equal "This has a here.", sanitizer.sanitize("This has a <!-- comment --> here.") - assert_equal "This has a here.", sanitizer.sanitize("This has a <![CDATA[<section>]]> here.") - assert_equal "This has an unclosed ", sanitizer.sanitize("This has an unclosed <![CDATA[<section>]] here...") - [nil, '', ' '].each { |blank| assert_equal blank, sanitizer.sanitize(blank) } - assert_nothing_raised { sanitizer.sanitize("This is a frozen string with no tags".freeze) } - end - - def test_strip_links - sanitizer = HTML::LinkSanitizer.new - assert_equal "Dont touch me", sanitizer.sanitize("Dont touch me") - assert_equal "on my mind\nall day long", sanitizer.sanitize("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>") - assert_equal "0wn3d", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>") - assert_equal "Magic", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic") - assert_equal "FrrFox", sanitizer.sanitize("<href onlclick='steal()'>FrrFox</a></href>") - assert_equal "My mind\nall <b>day</b> long", sanitizer.sanitize("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>") - assert_equal "all <b>day</b> long", sanitizer.sanitize("<<a>a href='hello'>all <b>day</b> long<</A>/a>") - - assert_equal "<a<a", sanitizer.sanitize("<a<a") - end - - def test_sanitize_form - assert_sanitized "<form action=\"/foo/bar\" method=\"post\"><input></form>", '' - end - - def test_sanitize_plaintext - raw = "<plaintext><span>foo</span></plaintext>" - assert_sanitized raw, "<span>foo</span>" - end - - def test_sanitize_script - assert_sanitized "a b c<script language=\"Javascript\">blah blah blah</script>d e f", "a b cd e f" - end - - def test_sanitize_js_handlers - raw = %{onthis="do that" <a href="#" onclick="hello" name="foo" onbogus="remove me">hello</a>} - assert_sanitized raw, %{onthis="do that" <a name="foo" href="#">hello</a>} - end - - def test_sanitize_javascript_href - raw = %{href="javascript:bang" <a href="javascript:bang" name="hello">foo</a>, <span href="javascript:bang">bar</span>} - assert_sanitized raw, %{href="javascript:bang" <a name="hello">foo</a>, <span>bar</span>} - end - - def test_sanitize_image_src - raw = %{src="javascript:bang" <img src="javascript:bang" width="5">foo</img>, <span src="javascript:bang">bar</span>} - assert_sanitized raw, %{src="javascript:bang" <img width="5">foo</img>, <span>bar</span>} - end - - HTML::WhiteListSanitizer.allowed_tags.each do |tag_name| - define_method "test_should_allow_#{tag_name}_tag" do - assert_sanitized "start <#{tag_name} title=\"1\" onclick=\"foo\">foo <bad>bar</bad> baz</#{tag_name}> end", %(start <#{tag_name} title="1">foo bar baz</#{tag_name}> end) - end - end - - def test_should_allow_anchors - assert_sanitized %(<a href="foo" onclick="bar"><script>baz</script></a>), %(<a href="foo"></a>) - end - - # RFC 3986, sec 4.2 - def test_allow_colons_in_path_component - assert_sanitized("<a href=\"./this:that\">foo</a>") - end - - %w(src width height alt).each do |img_attr| - define_method "test_should_allow_image_#{img_attr}_attribute" do - assert_sanitized %(<img #{img_attr}="foo" onclick="bar" />), %(<img #{img_attr}="foo" />) - end - end - - def test_should_handle_non_html - assert_sanitized 'abc' - end - - def test_should_handle_blank_text - assert_sanitized nil - assert_sanitized '' - end - - def test_should_allow_custom_tags - text = "<u>foo</u>" - sanitizer = HTML::WhiteListSanitizer.new - assert_equal(text, sanitizer.sanitize(text, :tags => %w(u))) - end - - def test_should_allow_only_custom_tags - text = "<u>foo</u> with <i>bar</i>" - sanitizer = HTML::WhiteListSanitizer.new - assert_equal("<u>foo</u> with bar", sanitizer.sanitize(text, :tags => %w(u))) - end - - def test_should_allow_custom_tags_with_attributes - text = %(<blockquote cite="http://example.com/">foo</blockquote>) - sanitizer = HTML::WhiteListSanitizer.new - assert_equal(text, sanitizer.sanitize(text)) - end - - def test_should_allow_custom_tags_with_custom_attributes - text = %(<blockquote foo="bar">Lorem ipsum</blockquote>) - sanitizer = HTML::WhiteListSanitizer.new - assert_equal(text, sanitizer.sanitize(text, :attributes => ['foo'])) - end - - def test_should_raise_argument_error_if_tags_is_not_enumerable - sanitizer = HTML::WhiteListSanitizer.new - e = assert_raise(ArgumentError) do - sanitizer.sanitize('', :tags => 'foo') - end - - assert_equal "You should pass :tags as an Enumerable", e.message - end - - def test_should_raise_argument_error_if_attributes_is_not_enumerable - sanitizer = HTML::WhiteListSanitizer.new - e = assert_raise(ArgumentError) do - sanitizer.sanitize('', :attributes => 'foo') - end - - assert_equal "You should pass :attributes as an Enumerable", e.message - end - - [%w(img src), %w(a href)].each do |(tag, attr)| - define_method "test_should_strip_#{attr}_attribute_in_#{tag}_with_bad_protocols" do - assert_sanitized %(<#{tag} #{attr}="javascript:bang" title="1">boo</#{tag}>), %(<#{tag} title="1">boo</#{tag}>) - end - end - - def test_should_flag_bad_protocols - sanitizer = HTML::WhiteListSanitizer.new - %w(about chrome data disk hcp help javascript livescript lynxcgi lynxexec ms-help ms-its mhtml mocha opera res resource shell vbscript view-source vnd.ms.radio wysiwyg).each do |proto| - assert sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://bad") - end - end - - def test_should_accept_good_protocols_ignoring_case - sanitizer = HTML::WhiteListSanitizer.new - HTML::WhiteListSanitizer.allowed_protocols.each do |proto| - assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto.capitalize}://good") - end - end - - def test_should_accept_good_protocols_ignoring_space - sanitizer = HTML::WhiteListSanitizer.new - HTML::WhiteListSanitizer.allowed_protocols.each do |proto| - assert !sanitizer.send(:contains_bad_protocols?, 'src', " #{proto}://good") - end - end - - def test_should_accept_good_protocols - sanitizer = HTML::WhiteListSanitizer.new - HTML::WhiteListSanitizer.allowed_protocols.each do |proto| - assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://good") - end - end - - def test_should_reject_hex_codes_in_protocol - assert_sanitized %(<a href="%6A%61%76%61%73%63%72%69%70%74%3A%61%6C%65%72%74%28%22%58%53%53%22%29">1</a>), "<a>1</a>" - assert @sanitizer.send(:contains_bad_protocols?, 'src', "%6A%61%76%61%73%63%72%69%70%74%3A%61%6C%65%72%74%28%22%58%53%53%22%29") - end - - def test_should_block_script_tag - assert_sanitized %(<SCRIPT\nSRC=http://ha.ckers.org/xss.js></SCRIPT>), "" - end - - [%(<IMG SRC="javascript:alert('XSS');">), - %(<IMG SRC=javascript:alert('XSS')>), - %(<IMG SRC=JaVaScRiPt:alert('XSS')>), - %(<IMG """><SCRIPT>alert("XSS")</SCRIPT>">), - %(<IMG SRC=javascript:alert("XSS")>), - %(<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>), - %(<IMG SRC=javascript:alert('XSS')>), - %(<IMG SRC=javascript:alert('XSS')>), - %(<IMG SRC=javascript:alert('XSS')>), - %(<IMG SRC="jav\tascript:alert('XSS');">), - %(<IMG SRC="jav	ascript:alert('XSS');">), - %(<IMG SRC="jav
ascript:alert('XSS');">), - %(<IMG SRC="jav
ascript:alert('XSS');">), - %(<IMG SRC="  javascript:alert('XSS');">), - %(<IMG SRC="javascript:alert('XSS');">), - %(<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>)].each_with_index do |img_hack, i| - define_method "test_should_not_fall_for_xss_image_hack_#{i+1}" do - assert_sanitized img_hack, "<img>" - end - end - - def test_should_sanitize_tag_broken_up_by_null - assert_sanitized %(<SCR\0IPT>alert(\"XSS\")</SCR\0IPT>), "alert(\"XSS\")" - end - - def test_should_sanitize_invalid_script_tag - assert_sanitized %(<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>), "" - end - - def test_should_sanitize_script_tag_with_multiple_open_brackets - assert_sanitized %(<<SCRIPT>alert("XSS");//<</SCRIPT>), "<" - assert_sanitized %(<iframe src=http://ha.ckers.org/scriptlet.html\n<a), %(<a) - end - - def test_should_sanitize_unclosed_script - assert_sanitized %(<SCRIPT SRC=http://ha.ckers.org/xss.js?<B>), "<b>" - end - - def test_should_sanitize_half_open_scripts - assert_sanitized %(<IMG SRC="javascript:alert('XSS')"), "<img>" - end - - def test_should_not_fall_for_ridiculous_hack - img_hack = %(<IMG\nSRC\n=\n"\nj\na\nv\na\ns\nc\nr\ni\np\nt\n:\na\nl\ne\nr\nt\n(\n'\nX\nS\nS\n'\n)\n"\n>) - assert_sanitized img_hack, "<img>" - end - - def test_should_sanitize_attributes - assert_sanitized %(<SPAN title="'><script>alert()</script>">blah</SPAN>), %(<span title="#{CGI.escapeHTML "'><script>alert()</script>"}">blah</span>) - end - - def test_should_sanitize_illegal_style_properties - raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;) - expected = %(display: block; width: 100%; height: 100%; background-color: black; background-image: ; background-x: center; background-y: center;) - assert_equal expected, sanitize_css(raw) - end - - def test_should_sanitize_with_trailing_space - raw = "display:block; " - expected = "display: block;" - assert_equal expected, sanitize_css(raw) - end - - def test_should_sanitize_xul_style_attributes - raw = %(-moz-binding:url('http://ha.ckers.org/xssmoz.xml#xss')) - assert_equal '', sanitize_css(raw) - end - - def test_should_sanitize_invalid_tag_names - assert_sanitized(%(a b c<script/XSS src="http://ha.ckers.org/xss.js"></script>d e f), "a b cd e f") - end - - def test_should_sanitize_non_alpha_and_non_digit_characters_in_tags - assert_sanitized('<a onclick!#$%&()*~+-_.,:;?@[/|\]^`=alert("XSS")>foo</a>', "<a>foo</a>") - end - - def test_should_sanitize_invalid_tag_names_in_single_tags - assert_sanitized('<img/src="http://ha.ckers.org/xss.js"/>', "<img />") - end - - def test_should_sanitize_img_dynsrc_lowsrc - assert_sanitized(%(<img lowsrc="javascript:alert('XSS')" />), "<img />") - end - - def test_should_sanitize_div_background_image_unicode_encoded - raw = %(background-image:\0075\0072\006C\0028'\006a\0061\0076\0061\0073\0063\0072\0069\0070\0074\003a\0061\006c\0065\0072\0074\0028.1027\0058.1053\0053\0027\0029'\0029) - assert_equal '', sanitize_css(raw) - end - - def test_should_sanitize_div_style_expression - raw = %(width: expression(alert('XSS'));) - assert_equal '', sanitize_css(raw) - end - - def test_should_sanitize_across_newlines - raw = %(\nwidth:\nexpression(alert('XSS'));\n) - assert_equal '', sanitize_css(raw) - end - - def test_should_sanitize_img_vbscript - assert_sanitized %(<img src='vbscript:msgbox("XSS")' />), '<img />' - end - - def test_should_sanitize_cdata_section - assert_sanitized "<![CDATA[<span>section</span>]]>", "<![CDATA[<span>section</span>]]>" - end - - def test_should_sanitize_unterminated_cdata_section - assert_sanitized "<![CDATA[<span>neverending...", "<![CDATA[<span>neverending...]]>" - end - - def test_should_not_mangle_urls_with_ampersand - assert_sanitized %{<a href=\"http://www.domain.com?var1=1&var2=2\">my link</a>} - end - - def test_should_sanitize_neverending_attribute - assert_sanitized "<span class=\"\\", "<span class=\"\\\">" - end - - def test_x03a - assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>" - assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>" - assert_sanitized %(<a href="http://legit">), %(<a href="http://legit">) - assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>" - assert_sanitized %(<a href="javascript:alert('XSS');">), "<a>" - assert_sanitized %(<a href="http://legit">), %(<a href="http://legit">) - end - -protected - def assert_sanitized(input, expected = nil) - @sanitizer ||= HTML::WhiteListSanitizer.new - if input - assert_dom_equal expected || input, @sanitizer.sanitize(input) - else - assert_nil @sanitizer.sanitize(input) - end - end - - def sanitize_css(input) - (@sanitizer ||= HTML::WhiteListSanitizer.new).sanitize_css(input) - end -end diff --git a/actionview/test/template/html-scanner/tag_node_test.rb b/actionview/test/template/html-scanner/tag_node_test.rb deleted file mode 100644 index a29d2d43d7..0000000000 --- a/actionview/test/template/html-scanner/tag_node_test.rb +++ /dev/null @@ -1,243 +0,0 @@ -require 'abstract_unit' - -class TagNodeTest < ActiveSupport::TestCase - def test_open_without_attributes - node = tag("<tag>") - assert_equal "tag", node.name - assert_equal Hash.new, node.attributes - assert_nil node.closing - end - - def test_open_with_attributes - node = tag("<TAG1 foo=hey_ho x:bar=\"blah blah\" BAZ='blah blah blah' >") - assert_equal "tag1", node.name - assert_equal "hey_ho", node["foo"] - assert_equal "blah blah", node["x:bar"] - assert_equal "blah blah blah", node["baz"] - end - - def test_self_closing_without_attributes - node = tag("<tag/>") - assert_equal "tag", node.name - assert_equal Hash.new, node.attributes - assert_equal :self, node.closing - end - - def test_self_closing_with_attributes - node = tag("<tag a=b/>") - assert_equal "tag", node.name - assert_equal( { "a" => "b" }, node.attributes ) - assert_equal :self, node.closing - end - - def test_closing_without_attributes - node = tag("</tag>") - assert_equal "tag", node.name - assert_nil node.attributes - assert_equal :close, node.closing - end - - def test_bracket_op_when_no_attributes - node = tag("</tag>") - assert_nil node["foo"] - end - - def test_bracket_op_when_attributes - node = tag("<tag a=b/>") - assert_equal "b", node["a"] - end - - def test_attributes_with_escaped_quotes - node = tag("<tag a='b\\'c' b=\"bob \\\"float\\\"\">") - assert_equal "b\\'c", node["a"] - assert_equal "bob \\\"float\\\"", node["b"] - end - - def test_to_s - node = tag("<a b=c d='f' g=\"h 'i'\" />") - node = node.to_s - assert node.include?('a') - assert node.include?('b="c"') - assert node.include?('d="f"') - assert node.include?('g="h') - assert node.include?('i') - end - - def test_tag - assert tag("<tag>").tag? - end - - def test_match_tag_as_string - assert tag("<tag>").match(:tag => "tag") - assert !tag("<tag>").match(:tag => "b") - end - - def test_match_tag_as_regexp - assert tag("<tag>").match(:tag => /t.g/) - assert !tag("<tag>").match(:tag => /t[bqs]g/) - end - - def test_match_attributes_as_string - t = tag("<tag a=something b=else />") - assert t.match(:attributes => {"a" => "something"}) - assert t.match(:attributes => {"b" => "else"}) - end - - def test_match_attributes_as_regexp - t = tag("<tag a=something b=else />") - assert t.match(:attributes => {"a" => /^something$/}) - assert t.match(:attributes => {"b" => /e.*e/}) - assert t.match(:attributes => {"a" => /me..i/, "b" => /.ls.$/}) - end - - def test_match_attributes_as_number - t = tag("<tag a=15 b=3.1415 />") - assert t.match(:attributes => {"a" => 15}) - assert t.match(:attributes => {"b" => 3.1415}) - assert t.match(:attributes => {"a" => 15, "b" => 3.1415}) - end - - def test_match_attributes_exist - t = tag("<tag a=15 b=3.1415 />") - assert t.match(:attributes => {"a" => true}) - assert t.match(:attributes => {"b" => true}) - assert t.match(:attributes => {"a" => true, "b" => true}) - end - - def test_match_attributes_not_exist - t = tag("<tag a=15 b=3.1415 />") - assert t.match(:attributes => {"c" => false}) - assert t.match(:attributes => {"c" => nil}) - assert t.match(:attributes => {"a" => true, "c" => false}) - end - - def test_match_parent_success - t = tag("<tag a=15 b='hello'>", tag("<foo k='value'>")) - assert t.match(:parent => {:tag => "foo", :attributes => {"k" => /v.l/, "j" => false}}) - end - - def test_match_parent_fail - t = tag("<tag a=15 b='hello'>", tag("<foo k='value'>")) - assert !t.match(:parent => {:tag => /kafka/}) - end - - def test_match_child_success - t = tag("<tag x:k='something'>") - tag("<child v=john a=kelly>", t) - tag("<sib m=vaughn v=james>", t) - assert t.match(:child => { :tag => "sib", :attributes => {"v" => /j/}}) - assert t.match(:child => { :attributes => {"a" => "kelly"}}) - end - - def test_match_child_fail - t = tag("<tag x:k='something'>") - tag("<child v=john a=kelly>", t) - tag("<sib m=vaughn v=james>", t) - assert !t.match(:child => { :tag => "sib", :attributes => {"v" => /r/}}) - assert !t.match(:child => { :attributes => {"v" => false}}) - end - - def test_match_ancestor_success - t = tag("<tag x:k='something'>", tag("<parent v=john a=kelly>", tag("<grandparent m=vaughn v=james>"))) - assert t.match(:ancestor => {:tag => "parent", :attributes => {"a" => /ll/}}) - assert t.match(:ancestor => {:attributes => {"m" => "vaughn"}}) - end - - def test_match_ancestor_fail - t = tag("<tag x:k='something'>", tag("<parent v=john a=kelly>", tag("<grandparent m=vaughn v=james>"))) - assert !t.match(:ancestor => {:tag => /^parent/, :attributes => {"v" => /m/}}) - assert !t.match(:ancestor => {:attributes => {"v" => false}}) - end - - def test_match_descendant_success - tag("<grandchild m=vaughn v=james>", tag("<child v=john a=kelly>", t = tag("<tag x:k='something'>"))) - assert t.match(:descendant => {:tag => "child", :attributes => {"a" => /ll/}}) - assert t.match(:descendant => {:attributes => {"m" => "vaughn"}}) - end - - def test_match_descendant_fail - tag("<grandchild m=vaughn v=james>", tag("<child v=john a=kelly>", t = tag("<tag x:k='something'>"))) - assert !t.match(:descendant => {:tag => /^child/, :attributes => {"v" => /m/}}) - assert !t.match(:descendant => {:attributes => {"v" => false}}) - end - - def test_match_child_count - t = tag("<tag x:k='something'>") - tag("hello", t) - tag("<child v=john a=kelly>", t) - tag("<sib m=vaughn v=james>", t) - assert t.match(:children => { :count => 2 }) - assert t.match(:children => { :count => 2..4 }) - assert t.match(:children => { :less_than => 4 }) - assert t.match(:children => { :greater_than => 1 }) - assert !t.match(:children => { :count => 3 }) - end - - def test_conditions_as_strings - t = tag("<tag x:k='something'>") - assert t.match("tag" => "tag") - assert t.match("attributes" => { "x:k" => "something" }) - assert !t.match("tag" => "gat") - assert !t.match("attributes" => { "x:j" => "something" }) - end - - def test_attributes_as_symbols - t = tag("<child v=john a=kelly>") - assert t.match(:attributes => { :v => /oh/ }) - assert t.match(:attributes => { :a => /ll/ }) - end - - def test_match_sibling - t = tag("<tag x:k='something'>") - tag("hello", t) - tag("<span a=b>", t) - tag("world", t) - m = tag("<span k=r>", t) - tag("<span m=l>", t) - - assert m.match(:sibling => {:tag => "span", :attributes => {:a => true}}) - assert m.match(:sibling => {:tag => "span", :attributes => {:m => true}}) - assert !m.match(:sibling => {:tag => "span", :attributes => {:k => true}}) - end - - def test_match_sibling_before - t = tag("<tag x:k='something'>") - tag("hello", t) - tag("<span a=b>", t) - tag("world", t) - m = tag("<span k=r>", t) - tag("<span m=l>", t) - - assert m.match(:before => {:tag => "span", :attributes => {:m => true}}) - assert !m.match(:before => {:tag => "span", :attributes => {:a => true}}) - assert !m.match(:before => {:tag => "span", :attributes => {:k => true}}) - end - - def test_match_sibling_after - t = tag("<tag x:k='something'>") - tag("hello", t) - tag("<span a=b>", t) - tag("world", t) - m = tag("<span k=r>", t) - tag("<span m=l>", t) - - assert m.match(:after => {:tag => "span", :attributes => {:a => true}}) - assert !m.match(:after => {:tag => "span", :attributes => {:m => true}}) - assert !m.match(:after => {:tag => "span", :attributes => {:k => true}}) - end - - def test_tag_to_s - t = tag("<b x='foo'>") - tag("hello", t) - tag("<hr />", t) - assert_equal %(<b x="foo">hello<hr /></b>), t.to_s - end - - private - - def tag(content, parent=nil) - node = HTML::Node.parse(parent,0,0,content) - parent.children << node if parent - node - end -end diff --git a/actionview/test/template/html-scanner/text_node_test.rb b/actionview/test/template/html-scanner/text_node_test.rb deleted file mode 100644 index cbcb9e78f0..0000000000 --- a/actionview/test/template/html-scanner/text_node_test.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'abstract_unit' - -class TextNodeTest < ActiveSupport::TestCase - def setup - @node = HTML::Text.new(nil, 0, 0, "hello, howdy, aloha, annyeong") - end - - def test_to_s - assert_equal "hello, howdy, aloha, annyeong", @node.to_s - end - - def test_find_string - assert_equal @node, @node.find("hello, howdy, aloha, annyeong") - assert_equal false, @node.find("bogus") - end - - def test_find_regexp - assert_equal @node, @node.find(/an+y/) - assert_nil @node.find(/b/) - end - - def test_find_hash - assert_equal @node, @node.find(:content => /howdy/) - assert_nil @node.find(:content => /^howdy$/) - assert_equal false, @node.find(:content => "howdy") - end - - def test_find_other - assert_nil @node.find(:hello) - end - - def test_match_string - assert @node.match("hello, howdy, aloha, annyeong") - assert_equal false, @node.match("bogus") - end - - def test_match_regexp - assert_not_nil @node, @node.match(/an+y/) - assert_nil @node.match(/b/) - end - - def test_match_hash - assert_not_nil @node, @node.match(:content => "howdy") - assert_nil @node.match(:content => /^howdy$/) - end - - def test_match_other - assert_nil @node.match(:hello) - end -end diff --git a/actionview/test/template/html-scanner/tokenizer_test.rb b/actionview/test/template/html-scanner/tokenizer_test.rb deleted file mode 100644 index 1d59de23b6..0000000000 --- a/actionview/test/template/html-scanner/tokenizer_test.rb +++ /dev/null @@ -1,131 +0,0 @@ -require 'abstract_unit' - -class TokenizerTest < ActiveSupport::TestCase - - def test_blank - tokenize "" - assert_end - end - - def test_space - tokenize " " - assert_next " " - assert_end - end - - def test_tag_simple_open - tokenize "<tag>" - assert_next "<tag>" - assert_end - end - - def test_tag_simple_self_closing - tokenize "<tag />" - assert_next "<tag />" - assert_end - end - - def test_tag_simple_closing - tokenize "</tag>" - assert_next "</tag>" - end - - def test_tag_with_single_quoted_attribute - tokenize %{<tag a='hello'>x} - assert_next %{<tag a='hello'>} - end - - def test_tag_with_single_quoted_attribute_with_escape - tokenize %{<tag a='hello\\''>x} - assert_next %{<tag a='hello\\''>} - end - - def test_tag_with_double_quoted_attribute - tokenize %{<tag a="hello">x} - assert_next %{<tag a="hello">} - end - - def test_tag_with_double_quoted_attribute_with_escape - tokenize %{<tag a="hello\\"">x} - assert_next %{<tag a="hello\\"">} - end - - def test_tag_with_unquoted_attribute - tokenize %{<tag a=hello>x} - assert_next %{<tag a=hello>} - end - - def test_tag_with_lt_char_in_attribute - tokenize %{<tag a="x < y">x} - assert_next %{<tag a="x < y">} - end - - def test_tag_with_gt_char_in_attribute - tokenize %{<tag a="x > y">x} - assert_next %{<tag a="x > y">} - end - - def test_doctype_tag - tokenize %{<!DOCTYPE "blah" "blah" "blah">\n <html>} - assert_next %{<!DOCTYPE "blah" "blah" "blah">} - assert_next %{\n } - assert_next %{<html>} - end - - def test_cdata_tag - tokenize %{<![CDATA[<br>]]>} - assert_next %{<![CDATA[<br>]]>} - assert_end - end - - def test_unterminated_cdata_tag - tokenize %{<content:encoded><![CDATA[ neverending...} - assert_next %{<content:encoded>} - assert_next %{<![CDATA[ neverending...} - assert_end - end - - def test_less_than_with_space - tokenize %{original < hello > world} - assert_next %{original } - assert_next %{< hello > world} - end - - def test_less_than_without_matching_greater_than - tokenize %{hello <span onmouseover="gotcha"\n<b>foo</b>\nbar</span>} - assert_next %{hello } - assert_next %{<span onmouseover="gotcha"\n} - assert_next %{<b>} - assert_next %{foo} - assert_next %{</b>} - assert_next %{\nbar} - assert_next %{</span>} - assert_end - end - - def test_unterminated_comment - tokenize %{hello <!-- neverending...} - assert_next %{hello } - assert_next %{<!-- neverending...} - assert_end - end - - private - - def tokenize(text) - @tokenizer = HTML::Tokenizer.new(text) - end - - def assert_next(expected, message=nil) - token = @tokenizer.next - assert_equal expected, token, message - end - - def assert_sequence(*expected) - assert_next expected.shift until expected.empty? - end - - def assert_end(message=nil) - assert_nil @tokenizer.next, message - end -end diff --git a/actionview/test/template/partial_iteration_test.rb b/actionview/test/template/partial_iteration_test.rb new file mode 100644 index 0000000000..695f9f1bef --- /dev/null +++ b/actionview/test/template/partial_iteration_test.rb @@ -0,0 +1,33 @@ +require 'abstract_unit' +require 'action_view/renderer/partial_renderer' + +class PartialIterationTest < ActiveSupport::TestCase + def test_has_size_and_index + iteration = ActionView::PartialIteration.new 3 + assert_equal 0, iteration.index, "should be at the first index" + assert_equal 3, iteration.size, "should have the size" + end + + def test_first_is_true_when_current_is_at_the_first_index + iteration = ActionView::PartialIteration.new 3 + assert iteration.first?, "first when current is 0" + end + + def test_first_is_false_unless_current_is_at_the_first_index + iteration = ActionView::PartialIteration.new 3 + iteration.iterate! + assert !iteration.first?, "not first when current is 1" + end + + def test_last_is_true_when_current_is_at_the_last_index + iteration = ActionView::PartialIteration.new 3 + iteration.iterate! + iteration.iterate! + assert iteration.last?, "last when current is 2" + end + + def test_last_is_false_unless_current_is_at_the_last_index + iteration = ActionView::PartialIteration.new 3 + assert !iteration.last?, "not last when current is 0" + end +end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index 67f1aabbd2..85817119ba 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -256,7 +256,7 @@ module RenderTestCases end def test_render_partial_collection_without_as - assert_equal "local_inspector,local_inspector_counter", + assert_equal "local_inspector,local_inspector_counter,local_inspector_iteration", @view.render(:partial => "test/local_inspector", :collection => [ Customer.new("mary") ]) end @@ -324,11 +324,16 @@ module RenderTestCases @controller_view.render(customers, :greeting => "Hello") end + def test_render_partial_using_collection_without_path + assert_equal "hi good customer: david0", @controller_view.render([ GoodCustomer.new("david") ], greeting: "hi") + end + def test_render_partial_without_object_or_collection_does_not_generate_partial_name_local_variable exception = assert_raises ActionView::Template::Error do @controller_view.render("partial_name_local_variable") end - assert_match "undefined local variable or method `partial_name_local_variable'", exception.message + assert_instance_of NameError, exception.original_exception + assert_equal :partial_name_local_variable, exception.original_exception.name end # TODO: The reason for this test is unclear, improve documentation @@ -387,6 +392,14 @@ module RenderTestCases ActionView::Template.unregister_template_handler :foo end + def test_render_body + assert_equal 'some body', @view.render(body: 'some body') + end + + def test_render_plain + assert_equal 'some plaintext', @view.render(plain: 'some plaintext') + end + def test_render_knows_about_types_registered_when_extensions_are_checked_earlier_in_initialization ActionView::Template::Handlers.extensions ActionView::Template.register_template_handler :foo, CustomHandler diff --git a/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb index f7c8f36b78..e4be21be2c 100644 --- a/actionview/test/template/sanitize_helper_test.rb +++ b/actionview/test/template/sanitize_helper_test.rb @@ -1,19 +1,15 @@ require 'abstract_unit' -# The exhaustive tests are in test/template/html-scanner/sanitizer_test.rb -# This tests the that the helpers hook up correctly to the sanitizer classes. +# The exhaustive tests are in test/controller/html/sanitizer_test.rb. +# This tests that the helpers hook up correctly to the sanitizer classes. class SanitizeHelperTest < ActionView::TestCase tests ActionView::Helpers::SanitizeHelper def test_strip_links assert_equal "Dont touch me", strip_links("Dont touch me") - assert_equal "<a<a", strip_links("<a<a") assert_equal "on my mind\nall day long", strip_links("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>") - assert_equal "0wn3d", strip_links("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>") assert_equal "Magic", strip_links("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic") - assert_equal "FrrFox", strip_links("<href onlclick='steal()'>FrrFox</a></href>") assert_equal "My mind\nall <b>day</b> long", strip_links("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>") - assert_equal "all <b>day</b> long", strip_links("<<a>a href='hello'>all <b>day</b> long<</A>/a>") end def test_sanitize_form @@ -22,27 +18,15 @@ class SanitizeHelperTest < ActionView::TestCase def test_should_sanitize_illegal_style_properties raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;) - expected = %(display: block; width: 100%; height: 100%; background-color: black; background-image: ; background-x: center; background-y: center;) + expected = %(display: block; width: 100%; height: 100%; background-color: black; background-x: center; background-y: center;) assert_equal expected, sanitize_css(raw) end def test_strip_tags - assert_equal("<<<bad html", strip_tags("<<<bad html")) - assert_equal("<<", strip_tags("<<<bad html>")) assert_equal("Dont touch me", strip_tags("Dont touch me")) assert_equal("This is a test.", strip_tags("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>")) - assert_equal("Weirdos", strip_tags("Wei<<a>a onclick='alert(document.cookie);'</a>/>rdos")) - assert_equal("This is a test.", strip_tags("This is a test.")) - assert_equal( - %{This is a test.\n\n\nIt no longer contains any HTML.\n}, strip_tags( - %{<title>This is <b>a <a href="" target="_blank">test</a></b>.</title>\n\n<!-- it has a comment -->\n\n<p>It no <b>longer <strong>contains <em>any <strike>HTML</strike></em>.</strong></b></p>\n})) assert_equal "This has a here.", strip_tags("This has a <!-- comment --> here.") - [nil, '', ' '].each do |blank| - stripped = strip_tags(blank) - assert_equal blank, stripped - end assert_equal "", strip_tags("<script>") - assert_equal "something <img onerror=alert(1337)", ERB::Util.html_escape(strip_tags("something <img onerror=alert(1337)")) end def test_sanitize_is_marked_safe diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb index c78b6450f2..0ea669b3d0 100644 --- a/actionview/test/template/tag_helper_test.rb +++ b/actionview/test/template/tag_helper_test.rb @@ -123,6 +123,7 @@ class TagHelperTest < ActionView::TestCase def test_escape_once assert_equal '1 < 2 & 3', escape_once('1 < 2 & 3') + assert_equal " ' ' λ λ " ' < > ", escape_once(" ' ' λ λ \" ' < > ") end def test_tag_honors_html_safe_for_param_values diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index 4ee0930341..4582fa13ee 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'rails/engine' module ActionView @@ -223,7 +224,7 @@ module ActionView test "is able to use mounted routes" do with_routing do |set| - app = Class.new do + app = Class.new(Rails::Engine) do def self.routes @routes ||= ActionDispatch::Routing::RouteSet.new end diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb index db416a8de4..667f9002da 100644 --- a/actionview/test/template/text_helper_test.rb +++ b/actionview/test/template/text_helper_test.rb @@ -187,7 +187,9 @@ class TextHelperTest < ActionView::TestCase "This text is not changed because we supplied an empty phrase", highlight("This text is not changed because we supplied an empty phrase", nil) ) + end + def test_highlight_pending assert_equal ' ', highlight(' ', 'blank text is returned verbatim') end diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 35279a4558..e0678ae1f7 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -25,7 +25,7 @@ class UrlHelperTest < ActiveSupport::TestCase include routes.url_helpers include ActionView::Helpers::JavaScriptHelper - include ActionDispatch::Assertions::DomAssertions + include Rails::Dom::Testing::Assertions::DomAssertions include ActionView::Context include RenderERBUtils diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md new file mode 100644 index 0000000000..b04883413e --- /dev/null +++ b/activejob/CHANGELOG.md @@ -0,0 +1 @@ +* Started project.
\ No newline at end of file diff --git a/activejob/MIT-LICENSE b/activejob/MIT-LICENSE new file mode 100644 index 0000000000..8b1e97b776 --- /dev/null +++ b/activejob/MIT-LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2014 David Heinemeier Hansson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/activejob/README.md b/activejob/README.md new file mode 100644 index 0000000000..e48070bcfc --- /dev/null +++ b/activejob/README.md @@ -0,0 +1,141 @@ +# Active Job -- Make work happen later + +Active Job is a framework for declaring jobs and making them run on a variety +of queueing backends. These jobs can be everything from regularly scheduled +clean-ups, billing charges, or mailings. Anything that can be chopped up into +small units of work and run in parallel, really. + +It also serves as the backend for ActionMailer's #deliver_later functionality +that makes it easy to turn any mailing into a job for running later. That's +one of the most common jobs in a modern web application: Sending emails outside +of the request-response cycle, so the user doesn't have to wait on it. + +The main point is to ensure that all Rails apps will have a job infrastructure +in place, even if it's in the form of an "immediate runner". We can then have +framework features and other gems build on top of that, without having to worry +about API differences between Delayed Job and Resque. Picking your queuing +backend becomes more of an operational concern, then. And you'll be able to +switch between them without having to rewrite your jobs. + + +## Usage + +Set the queue adapter for Active Job: + +``` ruby +ActiveJob::Base.queue_adapter = :inline # default queue adapter +# Adapters currently supported: :backburner, :delayed_job, :qu, :que, :queue_classic, +# :resque, :sidekiq, :sneakers, :sucker_punch +``` + +Declare a job like so: + +```ruby +class MyJob < ActiveJob::Base + queue_as :my_jobs + + def perform(record) + record.do_work + end +end +``` + +Enqueue a job like so: + +```ruby +MyJob.enqueue record # Enqueue a job to be performed as soon the queueing system is free. +``` + +```ruby +MyJob.enqueue_at Date.tomorrow.noon, record # Enqueue a job to be performed tomorrow at noon. +``` + +```ruby +MyJob.enqueue_in 1.week, record # Enqueue a job to be performed 1 week from now. +``` + +That's it! + + +## GlobalID support + +Active Job supports [GlobalID serialization](https://github.com/rails/globalid/) for parameters. This makes it possible +to pass live Active Record objects to your job instead of class/id pairs, which +you then have to manually deserialize. Before, jobs would look like this: + +```ruby +class TrashableCleanupJob + def perform(trashable_class, trashable_id, depth) + trashable = trashable_class.constantize.find(trashable_id) + trashable.cleanup(depth) + end +end +``` + +Now you can simply do: + +```ruby +class TrashableCleanupJob + def perform(trashable, depth) + trashable.cleanup(depth) + end +end +``` + +This works with any class that mixes in GlobalID::Identification, which +by default has been mixed into Active Record classes. + + +## Supported queueing systems + +We currently have adapters for: + +* [Backburner](https://github.com/nesquena/backburner) +* [Delayed Job](https://github.com/collectiveidea/delayed_job) +* [Qu](https://github.com/bkeepers/qu) +* [Que](https://github.com/chanks/que) +* [QueueClassic](https://github.com/ryandotsmith/queue_classic) +* [Resque 1.x](https://github.com/resque/resque) +* [Sidekiq](https://github.com/mperham/sidekiq) +* [Sneakers](https://github.com/jondot/sneakers) +* [Sucker Punch](https://github.com/brandonhilkert/sucker_punch) + + +## Auxiliary gems + +* [activejob-stats](https://github.com/seuros/activejob-stats) + +## Download and installation + +The latest version of Active Job can be installed with RubyGems: + +``` + % [sudo] gem install activejob +``` + +Source code can be downloaded as part of the Rails project on GitHub + +* https://github.com/rails/rails/tree/master/activejob + +## License + +ActiveJob is released under the MIT license: + +* http://www.opensource.org/licenses/MIT + + +## Support + +API documentation is at + +* http://api.rubyonrails.org + +Bug reports can be filed for the Ruby on Rails project here: + +* https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: + +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core + + diff --git a/activejob/Rakefile b/activejob/Rakefile new file mode 100644 index 0000000000..e661622165 --- /dev/null +++ b/activejob/Rakefile @@ -0,0 +1,84 @@ +require 'rake/testtask' +require 'rubygems/package_task' + +dir = File.dirname(__FILE__) + +def run_without_aborting(*tasks) + errors = [] + + tasks.each do |task| + begin + Rake::Task[task].invoke + rescue Exception + errors << task + end + end + + abort "Errors running #{errors.join(', ')}" if errors.any? +end + +task default: :test + +ACTIVEJOB_ADAPTERS = %w(inline delayed_job qu que queue_classic resque sidekiq sneakers sucker_punch backburner) + +desc 'Run all adapter tests' +task :test do + tasks = ACTIVEJOB_ADAPTERS.map{|a| "test_#{a}" } + run_without_aborting(*tasks) +end + +namespace :test do + desc 'Run all adapter tests in isolation' + task :isolated do + tasks = ACTIVEJOB_ADAPTERS.map{|a| "isolated_test_#{a}" } + run_without_aborting(*tasks) + end +end + + +ACTIVEJOB_ADAPTERS.each do |adapter| + namespace :test do + Rake::TestTask.new(adapter => "#{adapter}:env") do |t| + t.description = "" + t.libs << 'test' + t.test_files = FileList['test/cases/**/*_test.rb'] + t.verbose = true + end + + namespace :isolated do + task adapter => "#{adapter}:env" do + Dir.glob("#{dir}/test/cases/**/*_test.rb").all? do |file| + sh(Gem.ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file) + end or raise 'Failures' + end + end + end + + namespace adapter do + task test: "test_#{adapter}" + task isolated_test: "isolated_test_#{adapter}" + + task(:env) { ENV['AJADAPTER'] = adapter } + end + + + desc "Run #{adapter} tests" + task "test_#{adapter}" => ["#{adapter}:env", "test:#{adapter}"] + + desc "Run #{adapter} tests in isolation" + task "isolated_test_#{adapter}" => ["#{adapter}:env", "test:isolated:#{adapter}"] +end + + +spec = eval(File.read('activejob.gemspec')) + +Gem::PackageTask.new(spec) do |p| + p.gem_spec = spec +end + +desc 'Release to rubygems' +task release: :package do + require 'rake/gemcutter' + Rake::Gemcutter::Tasks.new(spec).define + Rake::Task['gem:push'].invoke +end diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec new file mode 100644 index 0000000000..d609bb8fce --- /dev/null +++ b/activejob/activejob.gemspec @@ -0,0 +1,22 @@ +version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip + +Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = 'activejob' + s.version = version + s.summary = 'Job framework with pluggable queues.' + s.description = 'Declare job classes that can be run by a variety of queueing backends.' + + s.required_ruby_version = '>= 1.9.3' + + s.license = 'MIT' + + s.author = 'David Heinemeier Hansson' + s.email = 'david@loudthinking.com' + s.homepage = 'http://www.rubyonrails.org' + + s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.md', 'lib/**/*'] + s.require_path = 'lib' + + s.add_dependency 'globalid' +end diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb new file mode 100644 index 0000000000..ef92406725 --- /dev/null +++ b/activejob/lib/active_job.rb @@ -0,0 +1,33 @@ +#-- +# Copyright (c) 2014 David Heinemeier Hansson +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#++ + +require 'active_support' +require 'active_support/rails' +require 'active_job/version' +require 'global_id' + +module ActiveJob + extend ActiveSupport::Autoload + + autoload :Base +end
\ No newline at end of file diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb new file mode 100644 index 0000000000..369e716912 --- /dev/null +++ b/activejob/lib/active_job/arguments.rb @@ -0,0 +1,62 @@ +module ActiveJob + class DeserializationError < StandardError + attr_reader :original_exception + + def initialize(e) + super ("Error while trying to deserialize arguments: #{e.message}") + @original_exception = e + set_backtrace e.backtrace + end + end + + module Arguments + extend self + TYPE_WHITELIST = [ NilClass, Fixnum, Float, String, TrueClass, FalseClass, Bignum ] + + def serialize(arguments) + arguments.map { |argument| serialize_argument(argument) } + end + + def deserialize(arguments) + arguments.map { |argument| deserialize_argument(argument) } + end + + private + def serialize_argument(argument) + case argument + when GlobalID::Identification + argument.global_id.to_s + when *TYPE_WHITELIST + argument + when Array + serialize(argument) + when Hash + Hash[ argument.map { |key, value| [ serialize_hash_key(key), serialize_argument(value) ] } ] + else + raise "Unsupported argument type: #{argument.class.name}" + end + end + + def deserialize_argument(argument) + case argument + when Array + deserialize(argument) + when Hash + Hash[ argument.map { |key, value| [ key, deserialize_argument(value) ] } ].with_indifferent_access + else + GlobalID::Locator.locate(argument) || argument + end + rescue => e + raise DeserializationError.new(e) + end + + def serialize_hash_key(key) + case key + when String, Symbol + key.to_s + else + raise "Unsupported hash key type: #{key.class.name}" + end + end + end +end diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb new file mode 100644 index 0000000000..d5ba253016 --- /dev/null +++ b/activejob/lib/active_job/base.rb @@ -0,0 +1,22 @@ +require 'active_job/queue_adapter' +require 'active_job/queue_name' +require 'active_job/enqueuing' +require 'active_job/execution' +require 'active_job/callbacks' +require 'active_job/identifier' +require 'active_job/logging' + +module ActiveJob + class Base + extend QueueAdapter + + include QueueName + include Enqueuing + include Execution + include Callbacks + include Identifier + include Logging + + ActiveSupport.run_load_hooks(:active_job, self) + end +end diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb new file mode 100644 index 0000000000..af92031bc9 --- /dev/null +++ b/activejob/lib/active_job/callbacks.rb @@ -0,0 +1,144 @@ +require 'active_support/callbacks' + +module ActiveJob + # = Active Job Callbacks + # + # Active Job provides hooks during the lifecycle of a job. Callbacks allows you to trigger + # logic during the lifecycle of a job. Available callbacks: + # + # * <tt>before_enqueue</tt> + # * <tt>around_enqueue</tt> + # * <tt>after_enqueue</tt> + # * <tt>before_perform</tt> + # * <tt>around_perform</tt> + # * <tt>after_perform</tt> + # + module Callbacks + extend ActiveSupport::Concern + include ActiveSupport::Callbacks + + included do + define_callbacks :perform + define_callbacks :enqueue + end + + module ClassMethods + # Defines a callback that will get called right before the + # job's perform method is executed. + # + # class VideoProcessJob < ActiveJob::Base + # queue_as :default + # + # before_perform do |job| + # UserMailer.notify_video_started_processing(job.arguments.first) + # end + # + # def perform(video_id) + # Video.find(video_id).process + # end + # end + # + def before_perform(*filters, &blk) + set_callback(:perform, :before, *filters, &blk) + end + + # Defines a callback that will get called right after the + # job's perform method has finished. + # + # class VideoProcessJob < ActiveJob::Base + # queue_as :default + # + # after_perform do |job| + # UserMailer.notify_video_processed(job.arguments.first) + # end + # + # def perform(video_id) + # Video.find(video_id).process + # end + # end + # + def after_perform(*filters, &blk) + set_callback(:perform, :after, *filters, &blk) + end + + # Defines a callback that will get called around the job's perform method. + # + # class VideoProcessJob < ActiveJob::Base + # queue_as :default + # + # around_perform do |job, block| + # UserMailer.notify_video_started_processing(job.arguments.first) + # block.call + # UserMailer.notify_video_processed(job.arguments.first) + # end + # + # def perform(video_id) + # Video.find(video_id).process + # end + # end + # + def around_perform(*filters, &blk) + set_callback(:perform, :around, *filters, &blk) + end + + # Defines a callback that will get called right before the + # job is enqueued. + # + # class VideoProcessJob < ActiveJob::Base + # queue_as :default + # + # before_enqueue do |job| + # $statsd.increment "enqueue-video-job.try" + # end + # + # def perform(video_id) + # Video.find(video_id).process + # end + # end + # + def before_enqueue(*filters, &blk) + set_callback(:enqueue, :before, *filters, &blk) + end + + # Defines a callback that will get called right after the + # job is enqueued. + # + # class VideoProcessJob < ActiveJob::Base + # queue_as :default + # + # after_enqueue do |job| + # $statsd.increment "enqueue-video-job.success" + # end + # + # def perform(video_id) + # Video.find(video_id).process + # end + # end + # + def after_enqueue(*filters, &blk) + set_callback(:enqueue, :after, *filters, &blk) + end + + # Defines a callback that will get called before and after the + # job is enqueued. + # + # class VideoProcessJob < ActiveJob::Base + # queue_as :default + # + # around_enqueue do |job, block| + # $statsd.time "video-job.process" do + # block.call + # end + # end + # + # def perform(video_id) + # Video.find(video_id).process + # end + # end + # + def around_enqueue(*filters, &blk) + set_callback(:enqueue, :around, *filters, &blk) + end + end + end +end diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb new file mode 100644 index 0000000000..3d00d51867 --- /dev/null +++ b/activejob/lib/active_job/enqueuing.rb @@ -0,0 +1,71 @@ +require 'active_job/arguments' + +module ActiveJob + module Enqueuing + extend ActiveSupport::Concern + + module ClassMethods + # Push a job onto the queue. The arguments must be legal JSON types + # (string, int, float, nil, true, false, hash or array) or + # GlobalID::Identification instances. Arbitrary Ruby objects + # are not supported. + # + # Returns an instance of the job class queued with args available in + # Job#arguments. + def enqueue(*args) + new(args).tap do |job| + job.run_callbacks :enqueue do + queue_adapter.enqueue self, job.job_id, *Arguments.serialize(args) + end + end + end + + # Enqueue a job to be performed at +interval+ from now. + # + # enqueue_in(1.week, "mike") + # + # Returns an instance of the job class queued with args available in + # Job#arguments and the timestamp in Job#enqueue_at. + def enqueue_in(interval, *args) + enqueue_at interval.seconds.from_now, *args + end + + # Enqueue a job to be performed at an explicit point in time. + # + # enqueue_at(Date.tomorrow.midnight, "mike") + # + # Returns an instance of the job class queued with args available in + # Job#arguments and the timestamp in Job#enqueue_at. + def enqueue_at(timestamp, *args) + new(args).tap do |job| + job.enqueued_at = timestamp + + job.run_callbacks :enqueue do + queue_adapter.enqueue_at self, timestamp.to_f, job.job_id, *Arguments.serialize(args) + end + end + end + end + + included do + attr_accessor :arguments + attr_accessor :enqueued_at + end + + def initialize(arguments = nil) + @arguments = arguments + end + + def retry_now + self.class.enqueue(*arguments) + end + + def retry_in(interval) + self.class.enqueue_in interval, *arguments + end + + def retry_at(timestamp) + self.class.enqueue_at timestamp, *arguments + end + end +end diff --git a/activejob/lib/active_job/execution.rb b/activejob/lib/active_job/execution.rb new file mode 100644 index 0000000000..0e7b5bdd72 --- /dev/null +++ b/activejob/lib/active_job/execution.rb @@ -0,0 +1,33 @@ +require 'active_support/rescuable' +require 'active_job/arguments' + +module ActiveJob + module Execution + extend ActiveSupport::Concern + + included do + include ActiveSupport::Rescuable + end + + def execute(job_id, *serialized_args) + self.job_id = job_id + self.arguments = deserialize_arguments(serialized_args) + + run_callbacks :perform do + perform(*arguments) + end + rescue => exception + rescue_with_handler(exception) || raise(exception) + end + + def perform(*) + fail NotImplementedError + end + + private + def deserialize_arguments(serialized_args) + Arguments.deserialize(serialized_args) + end + + end +end diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb new file mode 100644 index 0000000000..c166020b28 --- /dev/null +++ b/activejob/lib/active_job/gem_version.rb @@ -0,0 +1,15 @@ +module ActiveJob + # Returns the version of the currently loaded ActiveJob as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 4 + MINOR = 2 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/activejob/lib/active_job/identifier.rb b/activejob/lib/active_job/identifier.rb new file mode 100644 index 0000000000..c7f522087d --- /dev/null +++ b/activejob/lib/active_job/identifier.rb @@ -0,0 +1,15 @@ +require 'active_job/arguments' + +module ActiveJob + module Identifier + extend ActiveSupport::Concern + + included do + attr_writer :job_id + end + + def job_id + @job_id ||= SecureRandom.uuid + end + end +end diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb new file mode 100644 index 0000000000..d9e544acf5 --- /dev/null +++ b/activejob/lib/active_job/logging.rb @@ -0,0 +1,90 @@ +require 'active_support/core_ext/string/filters' +require 'active_support/tagged_logging' +require 'active_support/logger' + +module ActiveJob + module Logging + extend ActiveSupport::Concern + + included do + cattr_accessor(:logger) { ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT)) } + + around_enqueue do |_, block, _| + tag_logger do + block.call + end + end + + around_perform do |job, block, _| + tag_logger(job.class.name, job.job_id) do + payload = {adapter: job.class.queue_adapter, job: job.class, args: job.arguments} + ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup) + ActiveSupport::Notifications.instrument("perform.active_job", payload) do + block.call + end + end + end + + before_enqueue do |job| + if job.enqueued_at + ActiveSupport::Notifications.instrument "enqueue_at.active_job", + adapter: job.class.queue_adapter, job: job.class, job_id: job.job_id, args: job.arguments, timestamp: job.enqueued_at + else + ActiveSupport::Notifications.instrument "enqueue.active_job", + adapter: job.class.queue_adapter, job: job.class, job_id: job.job_id, args: job.arguments + end + end + end + + private + def tag_logger(*tags) + if logger.respond_to?(:tagged) + tags.unshift "ActiveJob" unless logger_tagged_by_active_job? + ActiveJob::Base.logger.tagged(*tags){ yield } + else + yield + end + end + + def logger_tagged_by_active_job? + logger.formatter.current_tags.include?("ActiveJob") + end + + class LogSubscriber < ActiveSupport::LogSubscriber + def enqueue(event) + info "Enqueued #{event.payload[:job].name} (Job ID: #{event.payload[:job_id]}) to #{queue_name(event)}" + args_info(event) + end + + def enqueue_at(event) + info "Enqueued #{event.payload[:job].name} (Job ID: #{event.payload[:job_id]}) to #{queue_name(event)} at #{enqueued_at(event)}" + args_info(event) + end + + def perform_start(event) + info "Performing #{event.payload[:job].name} from #{queue_name(event)}" + args_info(event) + end + + def perform(event) + info "Performed #{event.payload[:job].name} from #{queue_name(event)} in #{event.duration.round(2).to_s}ms" + end + + private + def queue_name(event) + event.payload[:adapter].name.demodulize.remove('Adapter') + "(#{event.payload[:job].queue_name})" + end + + def args_info(event) + event.payload[:args].any? ? " with arguments: #{event.payload[:args].map(&:inspect).join(", ")}" : "" + end + + def enqueued_at(event) + Time.at(event.payload[:timestamp]).utc + end + + def logger + ActiveJob::Base.logger + end + end + end +end + +ActiveJob::Logging::LogSubscriber.attach_to :active_job diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb new file mode 100644 index 0000000000..8f2f8b86ea --- /dev/null +++ b/activejob/lib/active_job/queue_adapter.rb @@ -0,0 +1,24 @@ +require 'active_job/queue_adapters/inline_adapter' +require 'active_support/core_ext/string/inflections' + +module ActiveJob + module QueueAdapter + mattr_reader(:queue_adapter) { ActiveJob::QueueAdapters::InlineAdapter } + + def queue_adapter=(name_or_adapter) + @@queue_adapter = \ + case name_or_adapter + when Symbol, String + load_adapter(name_or_adapter) + when Class + name_or_adapter + end + end + + private + def load_adapter(name) + require "active_job/queue_adapters/#{name}_adapter" + "ActiveJob::QueueAdapters::#{name.to_s.camelize}Adapter".constantize + end + end +end
\ No newline at end of file diff --git a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb new file mode 100644 index 0000000000..7a6032e56b --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb @@ -0,0 +1,25 @@ +require 'backburner' + +module ActiveJob + module QueueAdapters + class BackburnerAdapter + class << self + def enqueue(job, *args) + Backburner::Worker.enqueue JobWrapper, [ job.name, *args ], queue: job.queue_name + end + + def enqueue_at(job, timestamp, *args) + raise NotImplementedError + end + end + + class JobWrapper + class << self + def perform(job_name, *args) + job_name.constantize.new.execute *args + end + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb new file mode 100644 index 0000000000..bfeaa836d2 --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb @@ -0,0 +1,23 @@ +require 'delayed_job' + +module ActiveJob + module QueueAdapters + class DelayedJobAdapter + class << self + def enqueue(job, *args) + JobWrapper.new.delay(queue: job.queue_name).perform(job, *args) + end + + def enqueue_at(job, timestamp, *args) + JobWrapper.new.delay(queue: job.queue_name, run_at: Time.at(timestamp)).perform(job, *args) + end + end + + class JobWrapper + def perform(job, *args) + job.new.execute *args + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb new file mode 100644 index 0000000000..50d14a321d --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb @@ -0,0 +1,15 @@ +module ActiveJob + module QueueAdapters + class InlineAdapter + class << self + def enqueue(job, *args) + job.new.execute *args + end + + def enqueue_at(*) + raise NotImplementedError.new("Use a queueing backend to enqueue jobs in the future. Read more at https://github.com/rails/activejob") + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/qu_adapter.rb b/activejob/lib/active_job/queue_adapters/qu_adapter.rb new file mode 100644 index 0000000000..cdf4ae4ce9 --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/qu_adapter.rb @@ -0,0 +1,30 @@ +require 'qu' + +module ActiveJob + module QueueAdapters + class QuAdapter + class << self + def enqueue(job, *args) + Qu::Payload.new(klass: JobWrapper, args: [job.name, *args]).tap do |payload| + payload.instance_variable_set(:@queue, job.queue_name) + end.push + end + + def enqueue_at(job, timestamp, *args) + raise NotImplementedError + end + end + + class JobWrapper < Qu::Job + def initialize(job_name, *args) + @job = job_name.constantize + @args = args + end + + def perform + @job.new.execute *@args + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/que_adapter.rb b/activejob/lib/active_job/queue_adapters/que_adapter.rb new file mode 100644 index 0000000000..15a607bcb6 --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/que_adapter.rb @@ -0,0 +1,23 @@ +require 'que' + +module ActiveJob + module QueueAdapters + class QueAdapter + class << self + def enqueue(job, *args) + JobWrapper.enqueue job.name, *args, queue: job.queue_name + end + + def enqueue_at(job, timestamp, *args) + raise NotImplementedError + end + end + + class JobWrapper < Que::Job + def run(job_name, *args) + job_name.constantize.new.execute *args + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb new file mode 100644 index 0000000000..c61e0e30db --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb @@ -0,0 +1,23 @@ +require 'queue_classic' + +module ActiveJob + module QueueAdapters + class QueueClassicAdapter + class << self + def enqueue(job, *args) + QC::Queue.new(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.name, *args) + end + + def enqueue_at(job, timestamp, *args) + raise NotImplementedError + end + end + + class JobWrapper + def self.perform(job_name, *args) + job_name.constantize.new.execute *args + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/resque_adapter.rb b/activejob/lib/active_job/queue_adapters/resque_adapter.rb new file mode 100644 index 0000000000..384aa0c4cc --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/resque_adapter.rb @@ -0,0 +1,41 @@ +require 'resque' +require 'active_support/core_ext/enumerable' +require 'active_support/core_ext/array/access' + +begin + require 'resque-scheduler' +rescue LoadError + begin + require 'resque_scheduler' + rescue LoadError + false + end +end + +module ActiveJob + module QueueAdapters + class ResqueAdapter + class << self + def enqueue(job, *args) + Resque.enqueue_to job.queue_name, JobWrapper, job.name, *args + end + + def enqueue_at(job, timestamp, *args) + unless Resque.respond_to?(:enqueue_at_with_queue) + raise NotImplementedError, "To be able to schedule jobs with Resque you need the " \ + "resque-scheduler gem. Please add it to your Gemfile and run bundle install" + end + Resque.enqueue_at_with_queue job.queue_name, timestamp, JobWrapper, job.name, *args + end + end + + class JobWrapper + class << self + def perform(job_name, *args) + job_name.constantize.new.execute *args + end + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb new file mode 100644 index 0000000000..f738a7d91c --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb @@ -0,0 +1,35 @@ +require 'sidekiq' + +module ActiveJob + module QueueAdapters + class SidekiqAdapter + class << self + def enqueue(job, *args) + #Sidekiq::Client does not support symbols as keys + Sidekiq::Client.push \ + 'class' => JobWrapper, + 'queue' => job.queue_name, + 'args' => [ job, *args ], + 'retry' => true + end + + def enqueue_at(job, timestamp, *args) + Sidekiq::Client.push \ + 'class' => JobWrapper, + 'queue' => job.queue_name, + 'args' => [ job, *args ], + 'retry' => true, + 'at' => timestamp + end + end + + class JobWrapper + include Sidekiq::Worker + + def perform(job_name, *args) + job_name.constantize.new.execute *args + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb new file mode 100644 index 0000000000..051a8c3bd7 --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb @@ -0,0 +1,34 @@ +require 'sneakers' +require 'thread' + +module ActiveJob + module QueueAdapters + class SneakersAdapter + @monitor = Monitor.new + + class << self + def enqueue(job, *args) + @monitor.synchronize do + JobWrapper.from_queue job.queue_name + JobWrapper.enqueue ActiveSupport::JSON.encode([ job.name, *args ]) + end + end + + def enqueue_at(job, timestamp, *args) + raise NotImplementedError + end + end + + class JobWrapper + include Sneakers::Worker + from_queue 'active_jobs_default' + + def work(msg) + job_name, *args = ActiveSupport::JSON.decode(msg) + job_name.constantize.new.execute *args + ack! + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb new file mode 100644 index 0000000000..64b9c3ca15 --- /dev/null +++ b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb @@ -0,0 +1,25 @@ +require 'sucker_punch' + +module ActiveJob + module QueueAdapters + class SuckerPunchAdapter + class << self + def enqueue(job, *args) + JobWrapper.new.async.perform job, *args + end + + def enqueue_at(job, timestamp, *args) + raise NotImplementedError + end + end + + class JobWrapper + include SuckerPunch::Job + + def perform(job, *args) + job.new.execute *args + end + end + end + end +end diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb new file mode 100644 index 0000000000..c2186d9fe9 --- /dev/null +++ b/activejob/lib/active_job/queue_name.rb @@ -0,0 +1,18 @@ +module ActiveJob + module QueueName + extend ActiveSupport::Concern + + module ClassMethods + mattr_accessor(:default_queue_name) { "default" } + + def queue_as(part_name) + self.queue_name = part_name.to_s.presence || default_queue_name + end + end + + included do + class_attribute :queue_name + self.queue_name = default_queue_name + end + end +end diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb new file mode 100644 index 0000000000..6538ac1b30 --- /dev/null +++ b/activejob/lib/active_job/railtie.rb @@ -0,0 +1,23 @@ +require 'global_id/railtie' +require 'active_job' + +module ActiveJob + # = Active Job Railtie + class Railtie < Rails::Railtie # :nodoc: + config.active_job = ActiveSupport::OrderedOptions.new + + initializer 'active_job.logger' do + ActiveSupport.on_load(:active_job) { self.logger = ::Rails.logger } + end + + initializer "active_job.set_configs" do |app| + options = app.config.active_job + options.queue_adapter ||= :inline + + ActiveSupport.on_load(:active_job) do + options.each { |k,v| send("#{k}=", v) } + end + end + + end +end diff --git a/activejob/lib/active_job/version.rb b/activejob/lib/active_job/version.rb new file mode 100644 index 0000000000..7e646fa3c4 --- /dev/null +++ b/activejob/lib/active_job/version.rb @@ -0,0 +1,8 @@ +require_relative 'gem_version' + +module ActiveJob + # Returns the version of the currently loaded ActiveJob as a <tt>Gem::Version</tt> + def self.version + gem_version + end +end diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb new file mode 100644 index 0000000000..78a9c27606 --- /dev/null +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -0,0 +1,22 @@ +require 'rails/generators/named_base' + +module Rails + module Generators # :nodoc: + class JobGenerator < Rails::Generators::NamedBase # :nodoc: + desc 'This generator creates an active job file at app/jobs' + + class_option :queue, type: :string, default: 'default', desc: 'The queue name for the generated job' + + def self.default_generator_root + File.dirname(__FILE__) + end + + check_class_collision suffix: 'Job' + + def create_job_file + template 'job.rb', File.join('app/jobs', class_path, "#{file_name}_job.rb") + end + + end + end +end diff --git a/activejob/lib/rails/generators/job/templates/job.rb b/activejob/lib/rails/generators/job/templates/job.rb new file mode 100644 index 0000000000..462c71d917 --- /dev/null +++ b/activejob/lib/rails/generators/job/templates/job.rb @@ -0,0 +1,9 @@ +<% module_namespacing do -%> +class <%= class_name %>Job < ActiveJob::Base + queue_as :<%= options[:queue] %> + + def perform(*args) + # Do something later + end +end +<% end -%> diff --git a/activejob/test/adapters/backburner.rb b/activejob/test/adapters/backburner.rb new file mode 100644 index 0000000000..65d05f850b --- /dev/null +++ b/activejob/test/adapters/backburner.rb @@ -0,0 +1,3 @@ +require 'support/backburner/inline' + +ActiveJob::Base.queue_adapter = :backburner
\ No newline at end of file diff --git a/activejob/test/adapters/delayed_job.rb b/activejob/test/adapters/delayed_job.rb new file mode 100644 index 0000000000..afd9c9deb7 --- /dev/null +++ b/activejob/test/adapters/delayed_job.rb @@ -0,0 +1,7 @@ +ActiveJob::Base.queue_adapter = :delayed_job + +$LOAD_PATH << File.dirname(__FILE__) + "/../support/delayed_job" + +Delayed::Worker.delay_jobs = false +Delayed::Worker.backend = :test + diff --git a/activejob/test/adapters/inline.rb b/activejob/test/adapters/inline.rb new file mode 100644 index 0000000000..e0092552c4 --- /dev/null +++ b/activejob/test/adapters/inline.rb @@ -0,0 +1 @@ +ActiveJob::Base.queue_adapter = :inline
\ No newline at end of file diff --git a/activejob/test/adapters/qu.rb b/activejob/test/adapters/qu.rb new file mode 100644 index 0000000000..7728c843b4 --- /dev/null +++ b/activejob/test/adapters/qu.rb @@ -0,0 +1,3 @@ +require 'qu-immediate' + +ActiveJob::Base.queue_adapter = :qu diff --git a/activejob/test/adapters/que.rb b/activejob/test/adapters/que.rb new file mode 100644 index 0000000000..640061bf54 --- /dev/null +++ b/activejob/test/adapters/que.rb @@ -0,0 +1,2 @@ +ActiveJob::Base.queue_adapter = :que +Que.mode = :sync diff --git a/activejob/test/adapters/queue_classic.rb b/activejob/test/adapters/queue_classic.rb new file mode 100644 index 0000000000..ad5ced3cc2 --- /dev/null +++ b/activejob/test/adapters/queue_classic.rb @@ -0,0 +1,2 @@ +require 'support/queue_classic/inline' +ActiveJob::Base.queue_adapter = :queue_classic diff --git a/activejob/test/adapters/resque.rb b/activejob/test/adapters/resque.rb new file mode 100644 index 0000000000..af7080358d --- /dev/null +++ b/activejob/test/adapters/resque.rb @@ -0,0 +1,2 @@ +ActiveJob::Base.queue_adapter = :resque +Resque.inline = true diff --git a/activejob/test/adapters/sidekiq.rb b/activejob/test/adapters/sidekiq.rb new file mode 100644 index 0000000000..cd9d2034de --- /dev/null +++ b/activejob/test/adapters/sidekiq.rb @@ -0,0 +1,2 @@ +require 'sidekiq/testing/inline' +ActiveJob::Base.queue_adapter = :sidekiq diff --git a/activejob/test/adapters/sneakers.rb b/activejob/test/adapters/sneakers.rb new file mode 100644 index 0000000000..204166a700 --- /dev/null +++ b/activejob/test/adapters/sneakers.rb @@ -0,0 +1,2 @@ +require 'support/sneakers/inline' +ActiveJob::Base.queue_adapter = :sneakers diff --git a/activejob/test/adapters/sucker_punch.rb b/activejob/test/adapters/sucker_punch.rb new file mode 100644 index 0000000000..d2d1712946 --- /dev/null +++ b/activejob/test/adapters/sucker_punch.rb @@ -0,0 +1,2 @@ +require 'sucker_punch/testing/inline' +ActiveJob::Base.queue_adapter = :sucker_punch diff --git a/activejob/test/cases/adapter_test.rb b/activejob/test/cases/adapter_test.rb new file mode 100644 index 0000000000..4fc235ae40 --- /dev/null +++ b/activejob/test/cases/adapter_test.rb @@ -0,0 +1,8 @@ +require 'helper' + +class AdapterTest < ActiveSupport::TestCase + test "should load #{ENV['AJADAPTER']} adapter" do + ActiveJob::Base.queue_adapter = ENV['AJADAPTER'].to_sym + assert_equal ActiveJob::Base.queue_adapter, "active_job/queue_adapters/#{ENV['AJADAPTER']}_adapter".classify.constantize + end +end diff --git a/activejob/test/cases/callbacks_test.rb b/activejob/test/cases/callbacks_test.rb new file mode 100644 index 0000000000..9a0657ee89 --- /dev/null +++ b/activejob/test/cases/callbacks_test.rb @@ -0,0 +1,22 @@ +require 'helper' +require 'jobs/callback_job' + +require 'active_support/core_ext/object/inclusion' + +class CallbacksTest < ActiveSupport::TestCase + test 'perform callbacks' do + performed_callback_job = CallbackJob.new.tap { |j| j.execute("A-JOB-ID") } + assert "CallbackJob ran before_perform".in? performed_callback_job.history + assert "CallbackJob ran after_perform".in? performed_callback_job.history + assert "CallbackJob ran around_perform_start".in? performed_callback_job.history + assert "CallbackJob ran around_perform_stop".in? performed_callback_job.history + end + + test 'enqueue callbacks' do + enqueued_callback_job = CallbackJob.enqueue + assert "CallbackJob ran before_enqueue".in? enqueued_callback_job.history + assert "CallbackJob ran after_enqueue".in? enqueued_callback_job.history + assert "CallbackJob ran around_enqueue_start".in? enqueued_callback_job.history + assert "CallbackJob ran around_enqueue_stop".in? enqueued_callback_job.history + end +end diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb new file mode 100644 index 0000000000..fc1b77744c --- /dev/null +++ b/activejob/test/cases/job_serialization_test.rb @@ -0,0 +1,15 @@ +require 'helper' +require 'jobs/gid_job' +require 'models/person' + +class JobSerializationTest < ActiveSupport::TestCase + setup do + JobBuffer.clear + @person = Person.find(5) + end + + test 'serialize job with gid' do + GidJob.enqueue @person + assert_equal "Person with ID: 5", JobBuffer.last_value + end +end diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb new file mode 100644 index 0000000000..888c183a0b --- /dev/null +++ b/activejob/test/cases/logging_test.rb @@ -0,0 +1,96 @@ +require 'helper' +require "active_support/log_subscriber/test_helper" +require 'active_support/core_ext/numeric/time' +require 'jobs/hello_job' +require 'jobs/logging_job' +require 'jobs/nested_job' + +class AdapterTest < ActiveSupport::TestCase + include ActiveSupport::LogSubscriber::TestHelper + include ActiveSupport::Logger::Severity + + class TestLogger < ActiveSupport::Logger + def initialize + @file = StringIO.new + super(@file) + end + + def messages + @file.rewind + @file.read + end + end + + def setup + super + JobBuffer.clear + @old_logger = ActiveJob::Base.logger + @logger = ActiveSupport::TaggedLogging.new(TestLogger.new) + set_logger @logger + ActiveJob::Logging::LogSubscriber.attach_to :active_job + end + + def teardown + super + ActiveJob::Logging::LogSubscriber.log_subscribers.pop + ActiveJob::Base.logger = @old_logger + end + + def set_logger(logger) + ActiveJob::Base.logger = logger + end + + + def test_uses_active_job_as_tag + HelloJob.enqueue "Cristian" + assert_match(/\[ActiveJob\]/, @logger.messages) + end + + def test_enqueue_job_logging + HelloJob.enqueue "Cristian" + assert_match(/Enqueued HelloJob \(Job ID: .*?\) to .*?:.*Cristian/, @logger.messages) + end + + def test_perform_job_logging + LoggingJob.enqueue "Dummy" + assert_match(/Performing LoggingJob from .*? with arguments:.*Dummy/, @logger.messages) + assert_match(/Dummy, here is it: Dummy/, @logger.messages) + assert_match(/Performed LoggingJob from .*? in .*ms/, @logger.messages) + end + + def test_perform_uses_job_name_job_logging + LoggingJob.enqueue "Dummy" + assert_match(/\[LoggingJob\]/, @logger.messages) + end + + def test_perform_uses_job_id_job_logging + LoggingJob.enqueue "Dummy" + assert_match(/\[LOGGING-JOB-ID\]/, @logger.messages) + end + + def test_perform_nested_jobs_logging + NestedJob.enqueue + assert_match(/\[LoggingJob\] \[.*?\]/, @logger.messages) + assert_match(/\[ActiveJob\] Enqueued NestedJob \(Job ID: .*\) to/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob from/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Enqueued LoggingJob \(Job ID: .*?\) to .* with arguments: "NestedJob"/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob from .* with arguments: "NestedJob"/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Dummy, here is it: NestedJob/, @logger.messages) + assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob from .* in/, @logger.messages) + assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob from .* in/, @logger.messages) + end + + def test_enqueue_at_job_logging + HelloJob.enqueue_at 1, "Cristian" + assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages) + rescue NotImplementedError + skip + end + + def test_enqueue_in_job_logging + HelloJob.enqueue_in 2, "Cristian" + assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages) + rescue NotImplementedError + skip + end +end diff --git a/activejob/test/cases/parameters_test.rb b/activejob/test/cases/parameters_test.rb new file mode 100644 index 0000000000..76e8a8059a --- /dev/null +++ b/activejob/test/cases/parameters_test.rb @@ -0,0 +1,77 @@ +require 'helper' +require 'active_job/arguments' +require 'models/person' +require 'active_support/core_ext/hash/indifferent_access' + +class ParameterSerializationTest < ActiveSupport::TestCase + test 'should make no change to regular values' do + assert_equal [ 1, "something" ], ActiveJob::Arguments.serialize([ 1, "something" ]) + end + + test 'should not allow complex objects' do + assert_equal [ nil ], ActiveJob::Arguments.serialize([ nil ]) + assert_equal [ 1 ], ActiveJob::Arguments.serialize([ 1 ]) + assert_equal [ 1.0 ], ActiveJob::Arguments.serialize([ 1.0 ]) + assert_equal [ 'a' ], ActiveJob::Arguments.serialize([ 'a' ]) + assert_equal [ true ], ActiveJob::Arguments.serialize([ true ]) + assert_equal [ false ], ActiveJob::Arguments.serialize([ false ]) + assert_equal [ { "a" => 1, "b" => 2 } ], ActiveJob::Arguments.serialize([ { a: 1, "b" => 2 } ]) + assert_equal [ [ 1 ] ], ActiveJob::Arguments.serialize([ [ 1 ] ]) + assert_equal [ 1_000_000_000_000_000_000_000 ], ActiveJob::Arguments.serialize([ 1_000_000_000_000_000_000_000 ]) + + err = assert_raises RuntimeError do + ActiveJob::Arguments.serialize([ 1, self ]) + end + assert_equal "Unsupported argument type: #{self.class.name}", err.message + end + + test 'should dive deep into arrays or hashes' do + assert_equal [ { "a" => Person.find(5).gid.to_s }.with_indifferent_access ], ActiveJob::Arguments.serialize([ { a: Person.find(5) } ]) + assert_equal [ [ Person.find(5).gid.to_s ] ], ActiveJob::Arguments.serialize([ [ Person.find(5) ] ]) + end + + test 'should dive deep into arrays or hashes and raise exception on complex objects' do + err = assert_raises RuntimeError do + ActiveJob::Arguments.serialize([ 1, [self] ]) + end + assert_equal "Unsupported argument type: #{self.class.name}", err.message + end + + test 'shoud dive deep into hashes and allow raise exception on not string/symbol keys' do + err = assert_raises RuntimeError do + ActiveJob::Arguments.serialize([ [ { 1 => 2 } ] ]) + end + assert_equal "Unsupported hash key type: Fixnum", err.message + end + + test 'should serialize records with global id' do + assert_equal [ Person.find(5).gid.to_s ], ActiveJob::Arguments.serialize([ Person.find(5) ]) + end + + test 'should serialize values and records together' do + assert_equal [ 3, Person.find(5).gid.to_s ], ActiveJob::Arguments.serialize([ 3, Person.find(5) ]) + end +end + +class ParameterDeserializationTest < ActiveSupport::TestCase + test 'should make no change to regular values' do + assert_equal [ 1, "something" ], ActiveJob::Arguments.deserialize([ 1, "something" ]) + end + + test 'should deserialize records with global id' do + assert_equal [ Person.find(5) ], ActiveJob::Arguments.deserialize([ Person.find(5).gid ]) + end + + test 'should serialize values and records together' do + assert_equal [ 3, Person.find(5) ], ActiveJob::Arguments.deserialize([ 3, Person.find(5).gid ]) + end + + test 'should dive deep when deserialising arrays' do + assert_equal [ [ 3, Person.find(5) ] ], ActiveJob::Arguments.deserialize([ [ 3, Person.find(5).gid ] ]) + end + + test 'should dive deep when deserialising hashes' do + assert_equal [ { "5" => Person.find(5) } ], ActiveJob::Arguments.deserialize([ { "5" => Person.find(5).gid } ]) + end + +end diff --git a/activejob/test/cases/queue_naming_test.rb b/activejob/test/cases/queue_naming_test.rb new file mode 100644 index 0000000000..426af608f0 --- /dev/null +++ b/activejob/test/cases/queue_naming_test.rb @@ -0,0 +1,23 @@ +require 'helper' +require 'jobs/hello_job' +require 'jobs/logging_job' +require 'jobs/nested_job' + +class QueueNamingTest < ActiveSupport::TestCase + test 'name derived from base' do + assert_equal "default", HelloJob.queue_name + end + + test 'name appended in job' do + begin + HelloJob.queue_as :greetings + LoggingJob.queue_as :bookkeeping + + assert_equal "default", NestedJob.queue_name + assert_equal "greetings", HelloJob.queue_name + assert_equal "bookkeeping", LoggingJob.queue_name + ensure + HelloJob.queue_name = LoggingJob.queue_name = ActiveJob::Base.default_queue_name + end + end +end diff --git a/activejob/test/cases/queuing_test.rb b/activejob/test/cases/queuing_test.rb new file mode 100644 index 0000000000..f020316d7e --- /dev/null +++ b/activejob/test/cases/queuing_test.rb @@ -0,0 +1,44 @@ +require 'helper' +require 'jobs/hello_job' +require 'active_support/core_ext/numeric/time' + + +class QueuingTest < ActiveSupport::TestCase + setup do + JobBuffer.clear + end + + test 'run queued job' do + HelloJob.enqueue + assert_equal "David says hello", JobBuffer.last_value + end + + test 'run queued job with arguments' do + HelloJob.enqueue "Jamie" + assert_equal "Jamie says hello", JobBuffer.last_value + end + + test 'run queued job later' do + begin + result = HelloJob.enqueue_at 1.second.ago, "Jamie" + assert result + rescue NotImplementedError + skip + end + end + + test 'job returned by enqueue has the arguments available' do + job = HelloJob.enqueue "Jamie" + assert_equal [ "Jamie" ], job.arguments + end + + + test 'job returned by enqueue_at has the timestamp available' do + begin + job = HelloJob.enqueue_at Time.utc(2014, 1, 1) + assert_equal Time.utc(2014, 1, 1), job.enqueued_at + rescue NotImplementedError + skip + end + end +end diff --git a/activejob/test/cases/rescue_test.rb b/activejob/test/cases/rescue_test.rb new file mode 100644 index 0000000000..c9473314f2 --- /dev/null +++ b/activejob/test/cases/rescue_test.rb @@ -0,0 +1,30 @@ +require 'helper' +require 'jobs/rescue_job' + +require 'active_support/core_ext/object/inclusion' + +class RescueTest < ActiveSupport::TestCase + setup do + JobBuffer.clear + end + + test 'rescue perform exception with retry' do + job = RescueJob.new + job.execute(SecureRandom.uuid, "david") + assert_equal [ "rescued from ArgumentError", "performed beautifully" ], JobBuffer.values + end + + test 'let through unhandled perform exception' do + job = RescueJob.new + assert_raises(RescueJob::OtherError) do + job.execute(SecureRandom.uuid, "other") + end + end + + test 'rescue from deserialization errors' do + RescueJob.enqueue Person.new(404) + assert_includes JobBuffer.values, 'rescued from DeserializationError' + assert_includes JobBuffer.values, 'DeserializationError original exception was Person::RecordNotFound' + assert_not_includes JobBuffer.values, 'performed beautifully' + end +end diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb new file mode 100644 index 0000000000..ca67700273 --- /dev/null +++ b/activejob/test/helper.rb @@ -0,0 +1,50 @@ +require File.expand_path('../../../load_paths', __FILE__) + +require 'active_job' + +GlobalID.app = 'aj' + +@adapter = ENV['AJADAPTER'] || 'inline' + +def sidekiq? + @adapter == 'sidekiq' +end + +def rubinius? + RUBY_ENGINE == 'rbx' +end + +def ruby_193? + RUBY_VERSION == '1.9.3' && RUBY_ENGINE != 'java' +end + +#Sidekiq don't work with MRI 1.9.3 +#Travis uses rbx 2.6 which don't support unicode characters in methods. +#Remove the check when Travis change to rbx 2.7+ +exit if sidekiq? && (ruby_193? || rubinius?) + +require "adapters/#{@adapter}" + +require 'active_support/testing/autorun' + +ActiveJob::Base.logger.level = Logger::DEBUG + +module JobBuffer + class << self + def clear + @buffer = [] + end + + def add(value) + @buffer << value + end + + def values + @buffer + end + + def last_value + @buffer.last + end + end +end diff --git a/activejob/test/jobs/callback_job.rb b/activejob/test/jobs/callback_job.rb new file mode 100644 index 0000000000..056dd073e8 --- /dev/null +++ b/activejob/test/jobs/callback_job.rb @@ -0,0 +1,32 @@ +class CallbackJob < ActiveJob::Base + before_perform ->(job) { job.history << "CallbackJob ran before_perform" } + after_perform ->(job) { job.history << "CallbackJob ran after_perform" } + + before_enqueue ->(job) { job.history << "CallbackJob ran before_enqueue" } + after_enqueue ->(job) { job.history << "CallbackJob ran after_enqueue" } + + around_perform :around_perform + around_enqueue :around_enqueue + + + def perform(person = "david") + # NOTHING! + end + + def history + @history ||= [] + end + + # FIXME: Not sure why these can't be declared inline like before/after + def around_perform + history << "CallbackJob ran around_perform_start" + yield + history << "CallbackJob ran around_perform_stop" + end + + def around_enqueue + history << "CallbackJob ran around_enqueue_start" + yield + history << "CallbackJob ran around_enqueue_stop" + end +end diff --git a/activejob/test/jobs/gid_job.rb b/activejob/test/jobs/gid_job.rb new file mode 100644 index 0000000000..35c2366ec4 --- /dev/null +++ b/activejob/test/jobs/gid_job.rb @@ -0,0 +1,6 @@ +class GidJob < ActiveJob::Base + def perform(person) + JobBuffer.add("Person with ID: #{person.id}") + end +end + diff --git a/activejob/test/jobs/hello_job.rb b/activejob/test/jobs/hello_job.rb new file mode 100644 index 0000000000..4c6256af0d --- /dev/null +++ b/activejob/test/jobs/hello_job.rb @@ -0,0 +1,5 @@ +class HelloJob < ActiveJob::Base + def perform(greeter = "David") + JobBuffer.add("#{greeter} says hello") + end +end diff --git a/activejob/test/jobs/logging_job.rb b/activejob/test/jobs/logging_job.rb new file mode 100644 index 0000000000..d84ed8589b --- /dev/null +++ b/activejob/test/jobs/logging_job.rb @@ -0,0 +1,10 @@ +class LoggingJob < ActiveJob::Base + def perform(dummy) + logger.info "Dummy, here is it: #{dummy}" + end + + def job_id + "LOGGING-JOB-ID" + end +end + diff --git a/activejob/test/jobs/nested_job.rb b/activejob/test/jobs/nested_job.rb new file mode 100644 index 0000000000..fd66f68991 --- /dev/null +++ b/activejob/test/jobs/nested_job.rb @@ -0,0 +1,10 @@ +class NestedJob < ActiveJob::Base + def perform + LoggingJob.enqueue "NestedJob" + end + + def job_id + "NESTED-JOB-ID" + end +end + diff --git a/activejob/test/jobs/rescue_job.rb b/activejob/test/jobs/rescue_job.rb new file mode 100644 index 0000000000..e9cb37d1c4 --- /dev/null +++ b/activejob/test/jobs/rescue_job.rb @@ -0,0 +1,25 @@ +class RescueJob < ActiveJob::Base + class OtherError < StandardError; end + + rescue_from(ArgumentError) do + JobBuffer.add('rescued from ArgumentError') + arguments[0] = "DIFFERENT!" + retry_now + end + + rescue_from(ActiveJob::DeserializationError) do |e| + JobBuffer.add('rescued from DeserializationError') + JobBuffer.add("DeserializationError original exception was #{e.original_exception.class.name}") + end + + def perform(person = "david") + case person + when "david" + raise ArgumentError, "Hair too good" + when "other" + raise OtherError + else + JobBuffer.add('performed beautifully') + end + end +end diff --git a/activejob/test/models/person.rb b/activejob/test/models/person.rb new file mode 100644 index 0000000000..76a8f40616 --- /dev/null +++ b/activejob/test/models/person.rb @@ -0,0 +1,20 @@ +class Person + class RecordNotFound < StandardError; end + + include GlobalID::Identification + + attr_reader :id + + def self.find(id) + raise RecordNotFound.new("Cannot find person with ID=404") if id.to_i==404 + new(id) + end + + def initialize(id) + @id = id + end + + def ==(other_person) + other_person.is_a?(Person) && id.to_s == other_person.id.to_s + end +end diff --git a/activejob/test/support/backburner/inline.rb b/activejob/test/support/backburner/inline.rb new file mode 100644 index 0000000000..f761b53e27 --- /dev/null +++ b/activejob/test/support/backburner/inline.rb @@ -0,0 +1,8 @@ +require 'backburner' + +Backburner::Worker.class_eval do + class << self; alias_method :original_enqueue, :enqueue; end + def self.enqueue(job_class, args=[], opts={}) + job_class.perform(*args) + end +end
\ No newline at end of file diff --git a/activejob/test/support/delayed_job/delayed/backend/test.rb b/activejob/test/support/delayed_job/delayed/backend/test.rb new file mode 100644 index 0000000000..b50ed36fc2 --- /dev/null +++ b/activejob/test/support/delayed_job/delayed/backend/test.rb @@ -0,0 +1,113 @@ +#copied from https://github.com/collectiveidea/delayed_job/blob/master/spec/delayed/backend/test.rb +require 'ostruct' + +# An in-memory backend suitable only for testing. Tries to behave as if it were an ORM. +module Delayed + module Backend + module Test + class Job + attr_accessor :id + attr_accessor :priority + attr_accessor :attempts + attr_accessor :handler + attr_accessor :last_error + attr_accessor :run_at + attr_accessor :locked_at + attr_accessor :locked_by + attr_accessor :failed_at + attr_accessor :queue + + include Delayed::Backend::Base + + cattr_accessor :id + self.id = 0 + + def initialize(hash = {}) + self.attempts = 0 + self.priority = 0 + self.id = (self.class.id += 1) + hash.each{|k,v| send(:"#{k}=", v)} + end + + @jobs = [] + def self.all + @jobs + end + + def self.count + all.size + end + + def self.delete_all + all.clear + end + + def self.create(attrs = {}) + new(attrs).tap do |o| + o.save + end + end + + def self.create!(*args); create(*args); end + + def self.clear_locks!(worker_name) + all.select{|j| j.locked_by == worker_name}.each {|j| j.locked_by = nil; j.locked_at = nil} + end + + # Find a few candidate jobs to run (in case some immediately get locked by others). + def self.find_available(worker_name, limit = 5, max_run_time = Worker.max_run_time) + jobs = all.select do |j| + j.run_at <= db_time_now && + (j.locked_at.nil? || j.locked_at < db_time_now - max_run_time || j.locked_by == worker_name) && + !j.failed? + end + + jobs = jobs.select{|j| Worker.queues.include?(j.queue)} if Worker.queues.any? + jobs = jobs.select{|j| j.priority >= Worker.min_priority} if Worker.min_priority + jobs = jobs.select{|j| j.priority <= Worker.max_priority} if Worker.max_priority + jobs.sort_by{|j| [j.priority, j.run_at]}[0..limit-1] + end + + # Lock this job for this worker. + # Returns true if we have the lock, false otherwise. + def lock_exclusively!(max_run_time, worker) + now = self.class.db_time_now + if locked_by != worker + # We don't own this job so we will update the locked_by name and the locked_at + self.locked_at = now + self.locked_by = worker + end + + return true + end + + def self.db_time_now + Time.current + end + + def update_attributes(attrs = {}) + attrs.each{|k,v| send(:"#{k}=", v)} + save + end + + def destroy + self.class.all.delete(self) + end + + def save + self.run_at ||= Time.current + + self.class.all << self unless self.class.all.include?(self) + true + end + + def save!; save; end + + def reload + reset + self + end + end + end + end +end diff --git a/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb b/activejob/test/support/delayed_job/delayed/serialization/test.rb index e69de29bb2..e69de29bb2 100644 --- a/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb +++ b/activejob/test/support/delayed_job/delayed/serialization/test.rb diff --git a/activejob/test/support/queue_classic/inline.rb b/activejob/test/support/queue_classic/inline.rb new file mode 100644 index 0000000000..5e9c295e01 --- /dev/null +++ b/activejob/test/support/queue_classic/inline.rb @@ -0,0 +1,11 @@ +require 'queue_classic' + +module QC + class Queue + def enqueue(method, *args) + receiver_str, _, message = method.rpartition('.') + receiver = eval(receiver_str) + receiver.send(message, *args) + end + end +end diff --git a/activejob/test/support/sneakers/inline.rb b/activejob/test/support/sneakers/inline.rb new file mode 100644 index 0000000000..16d9b830fa --- /dev/null +++ b/activejob/test/support/sneakers/inline.rb @@ -0,0 +1,12 @@ +require 'sneakers' + +module Sneakers + module Worker + module ClassMethods + def enqueue(msg) + worker = self.new(nil, nil, {}) + worker.work(*msg) + end + end + end +end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 4d9186017f..c14b0688c7 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,36 @@ +* Passwords with spaces only allowed in `ActiveModel::SecurePassword`. + + Presence validation can be used to restore old behavior. + + *Yevhene Shemet* + +* Validate options passed to `ActiveModel::Validations.validate`. + + Preventing, in many cases, the simple mistake of using `validate` instead of `validates`. + + *Sonny Michaud* + +* Deprecate `reset_#{attribute}` in favor of `restore_#{attribute}`. + + These methods may cause confusion with the `reset_changes` that behaves differently + of them. + +* Deprecate `ActiveModel::Dirty#reset_changes` in favor of `#clear_changes_information`. + + This method name is causing confusion with the `reset_#{attribute}` + methods. While `reset_name` set the value of the name attribute for the + previous value `reset_changes` only discard the changes and previous + changes. + +* Added `restore_attributes` method to `ActiveModel::Dirty` API to restore all the + changed values to the previous data. + + *Igor G.* + +* Allow proc and symbol as values for `only_integer` of `NumericalityValidator` + + *Robin Mehner* + * `has_secure_password` now verifies that the given password is less than 72 characters if validations are enabled. diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 98ffffeb10..ca04f48c1c 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -15,8 +15,9 @@ module ActiveModel # * Call <tt>attr_name_will_change!</tt> before each change to the tracked # attribute. # * Call <tt>changes_applied</tt> after the changes are persisted. - # * Call <tt>reset_changes</tt> when you want to reset the changes + # * Call <tt>clear_changes_information</tt> when you want to reset the changes # information. + # * Call <tt>restore_attributes</tt> when you want to restore previous data. # # A minimal implementation could be: # @@ -36,11 +37,18 @@ module ActiveModel # # def save # # do persistence work + # # changes_applied # end # # def reload! - # reset_changes + # # get the values from the persistence layer + # + # clear_changes_information + # end + # + # def rollback! + # restore_attributes # end # end # @@ -72,6 +80,13 @@ module ActiveModel # person.reload! # person.previous_changes # => {} # + # Rollback the changes: + # + # person.name = "Uncle Bob" + # person.rollback! + # person.name # => "Bill" + # person.name_changed? # => false + # # Assigning the same value leaves the attribute unchanged: # # person.name = 'Bill' @@ -84,9 +99,11 @@ module ActiveModel # person.changed # => ["name"] # person.changes # => {"name" => ["Bill", "Bob"]} # - # If an attribute is modified in-place then make use of <tt>[attribute_name]_will_change!</tt> - # to mark that the attribute is changing. Otherwise ActiveModel can't track - # changes to in-place attributes. + # If an attribute is modified in-place then make use of + # +[attribute_name]_will_change!+ to mark that the attribute is changing. + # Otherwise Active Model can't track changes to in-place attributes. Note + # that Active Record can detect in-place modifications automatically. You do + # not need to call +[attribute_name]_will_change!+ on Active Record models. # # person.name_will_change! # person.name_change # => ["Bill", "Bill"] @@ -99,6 +116,7 @@ module ActiveModel included do attribute_method_suffix '_changed?', '_change', '_will_change!', '_was' attribute_method_affix prefix: 'reset_', suffix: '!' + attribute_method_affix prefix: 'restore_', suffix: '!' end # Returns +true+ if any attribute have unsaved changes, +false+ otherwise. @@ -162,20 +180,30 @@ module ActiveModel attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) end + # Restore all previous data of the provided attributes. + def restore_attributes(attributes = changed) + attributes.each { |attr| restore_attribute! attr } + end + private # Removes current changes and makes them accessible through +previous_changes+. - def changes_applied + def changes_applied # :doc: @previously_changed = changes @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end - # Removes all dirty data: current changes and previous changes - def reset_changes + # Clear all dirty data: current changes and previous changes. + def clear_changes_information # :doc: @previously_changed = ActiveSupport::HashWithIndifferentAccess.new @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end + def reset_changes + ActiveSupport::Deprecation.warn "#reset_changes is deprecated and will be removed on Rails 5. Please use #clear_changes_information instead." + clear_changes_information + end + # Handle <tt>*_change</tt> for +method_missing+. def attribute_change(attr) [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr) @@ -191,15 +219,36 @@ module ActiveModel rescue TypeError, NoMethodError end - changed_attributes[attr] = value + set_attribute_was(attr, value) end # Handle <tt>reset_*!</tt> for +method_missing+. def reset_attribute!(attr) + ActiveSupport::Deprecation.warn "#reset_#{attr}! is deprecated and will be removed on Rails 5. Please use #restore_#{attr}! instead." + + restore_attribute!(attr) + end + + # Handle <tt>restore_*!</tt> for +method_missing+. + def restore_attribute!(attr) if attribute_changed?(attr) __send__("#{attr}=", changed_attributes[attr]) - changed_attributes.delete(attr) + clear_attribute_changes([attr]) end end + + # This is necessary because `changed_attributes` might be overridden in + # other implemntations (e.g. in `ActiveRecord`) + alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc: + + # Force an attribute to have a particular "before" value + def set_attribute_was(attr, old_value) + attributes_changed_by_setter[attr] = old_value + end + + # Remove changes information for the provided attributes. + def clear_attribute_changes(attributes) + attributes_changed_by_setter.except!(*attributes) + end end end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 95029fd538..fc8034f9c7 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -23,7 +23,7 @@ module ActiveModel # attr_reader :errors # # def validate! - # errors.add(:name, "cannot be nil") if name == nil + # errors.add(:name, "cannot be nil") if name.nil? # end # # # The following methods are needed to be minimally implemented diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index ef3449b7cb..d9ec502f88 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -215,9 +215,8 @@ module ActiveModel # provided method below, or rolling your own is required. module Naming def self.extended(base) #:nodoc: - base.class_eval do - delegate :model_name, to: :class - end + base.remove_possible_method :model_name + base.delegate :model_name, to: :class end # Returns an ActiveModel::Name object for module. It can be diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index fdfd8cb147..f6ad35769f 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -64,6 +64,8 @@ module ActiveModel include InstanceMethodsOnActivation if options.fetch(:validations, true) + include ActiveModel::Validations + # This ensures the model has a password by checking whether the password_digest # is present, so that this works with both new and existing records. However, # when there is an error, the message is added to the password attribute instead @@ -103,7 +105,7 @@ module ActiveModel attr_reader :password # Encrypts the password into the +password_digest+ attribute, only if the - # new password is not blank. + # new password is not empty. # # class User < ActiveRecord::Base # has_secure_password validations: false @@ -117,7 +119,7 @@ module ActiveModel def password=(unencrypted_password) if unencrypted_password.nil? self.password_digest = nil - elsif unencrypted_password.present? + elsif !unencrypted_password.empty? @password = unencrypted_password cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost) diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index e546863305..7ee033ba5f 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -142,6 +142,11 @@ module ActiveModel # value. def validate(*args, &block) options = args.extract_options! + + if args.all? { |arg| arg.is_a?(Symbol) } + options.assert_valid_keys([:on, :if, :unless]) + end + if options.key?(:on) options = options.dup options[:if] = Array(options[:if]) @@ -149,6 +154,7 @@ module ActiveModel Array(options[:on]).include?(o.validation_context) } end + args << options set_callback(:validate, *args, &block) end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index a9fb9804d4..5bd162433d 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -30,7 +30,7 @@ module ActiveModel return end - if options[:only_integer] + if allow_only_integer?(record) unless value = parse_raw_value_as_an_integer(raw_value) record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value)) return @@ -75,6 +75,17 @@ module ActiveModel filtered[:value] = value filtered end + + def allow_only_integer?(record) + case options[:only_integer] + when Symbol + record.send(options[:only_integer]) + when Proc + options[:only_integer].call(record) + else + options[:only_integer] + end + end end module HelperMethods @@ -121,6 +132,7 @@ module ActiveModel # * <tt>:equal_to</tt> # * <tt>:less_than</tt> # * <tt>:less_than_or_equal_to</tt> + # * <tt>:only_integer</tt> # # For example: # diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 65cb1e5a88..0116de68ab 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -79,7 +79,7 @@ module ActiveModel # include ActiveModel::Validations # attr_accessor :title # - # validates :title, presence: true + # validates :title, presence: true, title: true # end # # It can be useful to access the class that is using that validator when there are prerequisites such diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index 2853476c91..db2cd885e2 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -43,6 +43,10 @@ class DirtyTest < ActiveModel::TestCase end def reload + clear_changes_information + end + + def deprecated_reload reset_changes end end @@ -107,7 +111,7 @@ class DirtyTest < ActiveModel::TestCase test "resetting attribute" do @model.name = "Bob" - @model.reset_name! + @model.restore_name! assert_nil @model.name assert !@model.name_changed? end @@ -176,4 +180,49 @@ class DirtyTest < ActiveModel::TestCase assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes end + + test "reset_changes is deprecated" do + @model.name = 'Dmitry' + @model.name_changed? + @model.save + @model.name = 'Bob' + + assert_equal [nil, 'Dmitry'], @model.previous_changes['name'] + assert_equal 'Dmitry', @model.changed_attributes['name'] + + assert_deprecated do + @model.deprecated_reload + end + + assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes + assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes + end + + test "restore_attributes should restore all previous data" do + @model.name = 'Dmitry' + @model.color = 'Red' + @model.save + @model.name = 'Bob' + @model.color = 'White' + + @model.restore_attributes + + assert_not @model.changed? + assert_equal 'Dmitry', @model.name + assert_equal 'Red', @model.color + end + + test "restore_attributes can restore only some attributes" do + @model.name = 'Dmitry' + @model.color = 'Red' + @model.save + @model.name = 'Bob' + @model.color = 'White' + + @model.restore_attributes(['name']) + + assert @model.changed? + assert_equal 'Dmitry', @model.name + assert_equal 'White', @model.color + end end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index 522a7cebb4..804e0c24f6 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -11,3 +11,5 @@ ActiveSupport::Deprecation.debug = true I18n.enforce_available_locales = false require 'active_support/testing/autorun' + +require 'mocha/setup' # FIXME: stop using mocha diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index e59f00c8c5..6d56c8344a 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -20,15 +20,12 @@ class SecurePasswordTest < ActiveModel::TestCase ActiveModel::SecurePassword.min_cost = @original_min_cost end - test "create/update without validations" do - assert @visitor.valid?(:create), 'visitor should be valid' - assert @visitor.valid?(:update), 'visitor should be valid' - - @visitor.password = '123' - @visitor.password_confirmation = '456' + test "automatically include ActiveModel::Validations when validations are enabled" do + assert_respond_to @user, :valid? + end - assert @visitor.valid?(:create), 'visitor should be valid' - assert @visitor.valid?(:update), 'visitor should be valid' + test "don't include ActiveModel::Validations when validations are disabled" do + assert_not_respond_to @visitor, :valid? end test "create a new user with validations and valid password/confirmation" do @@ -43,6 +40,11 @@ class SecurePasswordTest < ActiveModel::TestCase assert @user.valid?(:create), 'user should be valid' end + test "create a new user with validation and a spaces only password" do + @user.password = ' ' * 72 + assert @user.valid?(:create), 'user should be valid' + end + test "create a new user with validation and a blank password" do @user.password = '' assert !@user.valid?(:create), 'user should be invalid' @@ -108,6 +110,11 @@ class SecurePasswordTest < ActiveModel::TestCase assert @existing_user.valid?(:update), 'user should be valid' end + test "updating an existing user with validation and a spaces only password" do + @user.password = ' ' * 72 + assert @user.valid?(:update), 'user should be valid' + end + test "updating an existing user with validation and a blank password and password_confirmation" do @existing_user.password = '' @existing_user.password_confirmation = '' diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb index e1657407cf..3834d327ea 100644 --- a/activemodel/test/cases/validations/numericality_validation_test.rb +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -50,6 +50,21 @@ class NumericalityValidationTest < ActiveModel::TestCase valid!(NIL + INTEGERS) end + def test_validates_numericality_of_with_integer_only_and_symbol_as_value + Topic.validates_numericality_of :approved, only_integer: :condition_is_true_but_its_not + + invalid!(NIL + BLANK + JUNK) + valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY) + end + + def test_validates_numericality_of_with_integer_only_and_proc_as_value + Topic.send(:define_method, :allow_only_integers?, lambda { false }) + Topic.validates_numericality_of :approved, only_integer: Proc.new {|topic| topic.allow_only_integers? } + + invalid!(NIL + BLANK + JUNK) + valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY) + end + def test_validates_numericality_with_greater_than Topic.validates_numericality_of :approved, greater_than: 10 diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 4fee704ef5..ba0aacc2a5 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -4,7 +4,6 @@ require 'cases/helper' require 'models/topic' require 'models/reply' require 'models/custom_reader' -require 'models/automobile' require 'active_support/json' require 'active_support/xml_mini' @@ -167,6 +166,13 @@ class ValidationsTest < ActiveModel::TestCase end end + def test_invalid_options_to_validate + assert_raises(ArgumentError) do + # A common mistake -- we meant to call 'validates' + Topic.validate :title, presence: true + end + end + def test_errors_conversions Topic.validates_presence_of %w(title content) t = Topic.new @@ -283,25 +289,30 @@ class ValidationsTest < ActiveModel::TestCase end def test_validations_on_the_instance_level - auto = Automobile.new + Topic.validates :title, :author_name, presence: true + Topic.validates :content, length: { minimum: 10 } - assert auto.invalid? - assert_equal 3, auto.errors.size - - auto.make = 'Toyota' - auto.model = 'Corolla' - auto.approved = '1' + topic = Topic.new + assert topic.invalid? + assert_equal 3, topic.errors.size - assert auto.valid? + topic.title = 'Some Title' + topic.author_name = 'Some Author' + topic.content = 'Some Content Whose Length is more than 10.' + assert topic.valid? end def test_validate - auto = Automobile.new + Topic.validate do + validates_presence_of :title, :author_name + validates_length_of :content, minimum: 10 + end - assert_empty auto.errors + topic = Topic.new + assert_empty topic.errors - auto.validate - assert_not_empty auto.errors + topic.validate + assert_not_empty topic.errors end def test_strict_validation_in_validates diff --git a/activemodel/test/models/automobile.rb b/activemodel/test/models/automobile.rb deleted file mode 100644 index 4df2fe8b3a..0000000000 --- a/activemodel/test/models/automobile.rb +++ /dev/null @@ -1,13 +0,0 @@ -class Automobile - include ActiveModel::Validations - - validate :validations - - attr_accessor :make, :model, :approved - - def validations - validates_presence_of :make - validates_length_of :model, within: 2..10 - validates_acceptance_of :approved, allow_nil: false - end -end diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb index cbe259b1ad..1ec6001c48 100644 --- a/activemodel/test/models/user.rb +++ b/activemodel/test/models/user.rb @@ -1,6 +1,5 @@ class User extend ActiveModel::Callbacks - include ActiveModel::Validations include ActiveModel::SecurePassword define_model_callbacks :create diff --git a/activemodel/test/models/visitor.rb b/activemodel/test/models/visitor.rb index 4d7f4be097..22ad1a3c3d 100644 --- a/activemodel/test/models/visitor.rb +++ b/activemodel/test/models/visitor.rb @@ -1,6 +1,5 @@ class Visitor extend ActiveModel::Callbacks - include ActiveModel::Validations include ActiveModel::SecurePassword define_model_callbacks :create diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 6fc770d293..b09d9e336b 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,310 @@ +* Fixed an issue where custom accessor methods (such as those generated by + `enum`) with the same name as a global method are incorrectly overridden + when subclassing. + + Fixes #16288. + + *Godfrey Chan* + +* `*_was` and `changes` now work correctly for in-place attribute changes as + well. + + *Sean Griffin* + +* Fix regression on after_commit that didnt fire when having nested transactions. + + Fixes #16425 + + *arthurnn* + +* Do not try to write timestamps when a table has no timestamps columns. + + Fixes #8813. + + *Sergey Potapov* + +* `index_exists?` with `:name` option does verify specified columns. + + Example: + + add_index :articles, :title, name: "idx_title" + + # Before: + index_exists? :articles, :title, name: "idx_title" # => `true` + index_exists? :articles, :body, name: "idx_title" # => `true` + + # After: + index_exists? :articles, :title, name: "idx_title" # => `true` + index_exists? :articles, :body, name: "idx_title" # => `false` + + *Yves Senn*, *Matthew Draper* + +* When calling `update_columns` on a record that is not persisted, the error + message now reflects whether that object is a new record or has been + destroyed. + + *Lachlan Sylvester* + +* Define `id_was` to get the previous value of the primary key. + + Currently when we call id_was and we have a custom primary key name + Active Record will return the current value of the primary key. This + make impossible to correctly do an update operation if you change the + id. + + Fixes #16413. + + *Rafael Mendonça França* + +* Deprecate `DatabaseTasks.load_schema` to act on the current connection. + Use `.load_schema_current` instead. In the future `load_schema` will + require the `configuration` to act on as an argument. + + *Yves Senn* + +* Fixed automatic maintaining test schema to properly handle sql structure + schema format. + + Fixes #15394. + + *Wojciech Wnętrzak* + +* Fix type casting to Decimal from Float with large precision. + + *Tomohiro Hashidate* + +* Deprecate `Reflection#source_macro` + + `Reflection#source_macro` is no longer needed in Active Record + source so it has been deprecated. Code that used `source_macro` + was removed in #16353. + + *Eileen M. Uchtitelle*, *Aaron Patterson* + +* No verbose backtrace by db:drop when database does not exist. + + Fixes #16295. + + *Kenn Ejima* + +* Add support for Postgresql JSONB. + + Example: + + create_table :posts do |t| + t.jsonb :meta_data + end + + *Philippe Creux*, *Chris Teague* + +* `db:purge` with MySQL respects `Rails.env`. + + *Yves Senn* + +* `change_column_default :table, :column, nil` with PostgreSQL will issue a + `DROP DEFAULT` instead of a `DEFAULT NULL` query. + + Fixes #16261. + + *Matthew Draper*, *Yves Senn* + +* Allow to specify a type for the foreign key column in `references` + and `add_reference`. + + Example: + + change_table :vehicle do |t| + t.references :station, type: :uuid + end + + *Andrey Novikov*, *Łukasz Sarnacki* + +* `create_join_table` removes a common prefix when generating the join table. + This matches the existing behavior of HABTM associations. + + Fixes #13683. + + *Stefan Kanev* + +* Dont swallow errors on compute_type when having a bad alias_method on + a class. + + *arthurnn* + +* PostgreSQL invalid `uuid` are convert to nil. + + *Abdelkader Boudih* + +* Restore 4.0 behavior for using serialize attributes with `JSON` as coder. + + With 4.1.x, `serialize` started returning a string when `JSON` was passed as + the second attribute. It will now return a hash as per previous versions. + + Example: + + class Post < ActiveRecord::Base + serialize :comment, JSON + end + + class Comment + include ActiveModel::Model + attr_accessor :category, :text + end + + post = Post.create! + post.comment = Comment.new(category: "Animals", text: "This is a comment about squirrels.") + post.save! + + # 4.0 + post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."} + + # 4.1 before + post.comment # => "#<Comment:0x007f80ab48ff98>" + + # 4.1 after + post.comment # => {"category"=>"Animals", "text"=>"This is a comment about squirrels."} + + When using `JSON` as the coder in `serialize`, Active Record will use the + new `ActiveRecord::Coders::JSON` coder which delegates its `dump/load` to + `ActiveSupport::JSON.encode/decode`. This ensures special objects are dumped + correctly using the `#as_json` hook. + + To keep the previous behaviour, supply a custom coder instead + ([example](https://gist.github.com/jenncoop/8c4142bbe59da77daa63)). + + Fixes #15594. + + *Jenn Cooper* + +* Do not use `RENAME INDEX` syntax for MariaDB 10.0. + + Fixes #15931. + + *Jeff Browning* + +* Calling `#empty?` on a `has_many` association would use the value from the + counter cache if one exists. + + *David Verhasselt* + +* Fix the schema dump generated for tables without constraints and with + primary key with default value of custom PostgreSQL function result. + + Fixes #16111. + + *Andrey Novikov* + +* Fix the SQL generated when a `delete_all` is run on an association to not + produce an `IN` statements. + + Before: + + UPDATE "categorizations" SET "category_id" = NULL WHERE + "categorizations"."category_id" = 1 AND "categorizations"."id" IN (1, 2) + + After: + + UPDATE "categorizations" SET "category_id" = NULL WHERE + "categorizations"."category_id" = 1 + + *Eileen M. Uchitelle, Aaron Patterson* + +* Avoid type casting boolean and ActiveSupport::Duration values to numeric + values for string columns. Otherwise, in some database, the string column + values will be coerced to a numeric allowing false or 0.seconds match any + string starting with a non-digit. + + Example: + + App.where(apikey: false) # => SELECT * FROM users WHERE apikey = '0' + + *Dylan Thacker-Smith* + +* Add a `:required` option to singular associations, providing a nicer + API for presence validations on associations. + + *Sean Griffin* + +* Fixed error in `reset_counters` when associations have `select` scope. + (Call to `count` generates invalid SQL.) + + *Cade Truitt* + +* After a successful `reload`, `new_record?` is always false. + + Fixes #12101. + + *Matthew Draper* + +* PostgreSQL renaming table doesn't attempt to rename non existent sequences. + + *Abdelkader Boudih* + +* Move 'dependent: :destroy' handling for 'belongs_to' + from 'before_destroy' to 'after_destroy' callback chain + + Fixes #12380. + + *Ivan Antropov* + +* Detect in-place modifications on String attributes. + + Before this change user have to mark the attribute as changed to it be persisted + in the database. Now it is not required anymore. + + Before: + + user = User.first + user.name << ' Griffin' + user.name_will_change! + user.save + user.reload.name # => "Sean Griffin" + + After: + + user = User.first + user.name << ' Griffin' + user.save + user.reload.name # => "Sean Griffin" + + *Sean Griffin* + +* Add `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the record + is invalid. + + *Bogdan Gusiev*, *Marc Schütz* + +* Support for adding and removing foreign keys. Foreign keys are now + a part of `schema.rb`. This is supported by Mysql2Adapter, MysqlAdapter + and PostgreSQLAdapter. + + Many thanks to *Matthew Higgins* for laying the foundation with his work on + [foreigner](https://github.com/matthuhiggins/foreigner). + + Example: + + # within your migrations: + add_foreign_key :articles, :authors + remove_foreign_key :articles, :authors + + *Yves Senn* + +* Fix subtle bugs regarding attribute assignment on models with no primary + key. `'id'` will no longer be part of the attributes hash. + + *Sean Griffin* + +* Deprecate automatic counter caches on `has_many :through`. The behavior was + broken and inconsistent. + + *Sean Griffin* + +* `preload` preserves readonly flag for associations. + + See #15853. + + *Yves Senn* + * Assume numeric types have changed if they were assigned to a value that would fail numericality validation, regardless of the old value. Previously this would only occur if the old value was 0. @@ -39,8 +346,7 @@ *Yves Senn* -* Deprecate `serialized_attributes` without replacement. You can access its - behavior by going through the column's type object. +* Deprecate `serialized_attributes` without replacement. *Sean Griffin* @@ -58,21 +364,16 @@ * `ActiveRecord::Dirty` now detects in-place changes to mutable values. Serialized attributes on ActiveRecord models will no longer save when - unchanged. Fixes #8328. - - Sean Griffin - -* Fixed automatic maintaining test schema to properly handle sql structure - schema format. + unchanged. - Fixes #15394. + Fixes #8328. - *Wojciech Wnętrzak* + *Sean Griffin* * Pluck now works when selecting columns from different tables with the same name. - Fixes #15649 + Fixes #15649. *Sean Griffin* @@ -226,7 +527,7 @@ * Fixed the inferred table name of a has_and_belongs_to_many auxiliar table inside a schema. - Fixes #14824 + Fixes #14824. *Eric Chahin* @@ -768,7 +1069,7 @@ *Vilius Luneckas* *Ahmed AbouElhamayed* * `before_add` callbacks are fired before the record is saved on - `has_and_belongs_to_many` assocations *and* on `has_many :through` + `has_and_belongs_to_many` associations *and* on `has_many :through` associations. Before this change, `before_add` callbacks would be fired before the record was saved on `has_and_belongs_to_many` associations, but *not* on `has_many :through` associations. diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 7769966a22..b1069e5dcc 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -146,27 +146,9 @@ task :drop_postgresql_databases => 'db:postgresql:drop' task :rebuild_postgresql_databases => 'db:postgresql:rebuild' task :lines do - lines, codelines, total_lines, total_codelines = 0, 0, 0, 0 - - FileList["lib/active_record/**/*.rb"].each do |file_name| - next if file_name =~ /vendor/ - File.open(file_name, 'r') do |f| - while line = f.gets - lines += 1 - next if line =~ /^\s*$/ - next if line =~ /^\s*#/ - codelines += 1 - end - end - puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}" - - total_lines += lines - total_codelines += codelines - - lines, codelines = 0, 0 - end - - puts "Total: Lines #{total_lines}, LOC #{total_codelines}" + load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics' + files = FileList["lib/active_record/**/*.rb"] + CodeTools::LineStatistics.new(files).print_loc end spec = eval(File.read('activerecord.gemspec')) diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 8075008574..6231851be5 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '~> 6.0.0' + s.add_dependency 'arel', '>= 6.0.0.beta1', '< 6.1' end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index ab85414277..9028970a3d 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -27,12 +27,12 @@ require 'active_model' require 'arel' require 'active_record/version' +require 'active_record/attribute_set' module ActiveRecord extend ActiveSupport::Autoload autoload :Attribute - autoload :AttributeSet autoload :Base autoload :Callbacks autoload :Core @@ -97,6 +97,7 @@ module ActiveRecord module Coders autoload :YAMLColumn, 'active_record/coders/yaml_column' + autoload :JSON, 'active_record/coders/json' end module AttributeMethods diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 6222bfe903..d3b9b8251a 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -717,9 +717,9 @@ module ActiveRecord # == Eager loading of associations # # Eager loading is a way to find objects of a certain class and a number of named associations. - # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 + # This is one of the easiest ways of to prevent the dreaded N+1 problem in which fetching 100 # posts that each need to display their author triggers 101 database queries. Through the - # use of eager loading, the 101 queries can be reduced to 2. + # use of eager loading, the number of queries will be reduced from 101 to 2. # # class Post < ActiveRecord::Base # belongs_to :author @@ -1052,7 +1052,7 @@ module ActiveRecord # Specifies a one-to-many association. The following methods for retrieval and query of # collections of associated objects will be added: # - # +collection+ is a placeholder for the symbol passed as the first argument, so + # +collection+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>. # # [collection(force_reload = false)] @@ -1131,7 +1131,7 @@ module ActiveRecord # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>) # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>) - # The declaration can also include an options hash to specialize the behavior of the association. + # The declaration can also include an +options+ hash to specialize the behavior of the association. # # === Options # [:class_name] @@ -1209,7 +1209,7 @@ module ActiveRecord # Option examples: # has_many :comments, -> { order "posted_on" } # has_many :comments, -> { includes :author } - # has_many :people, -> { where("deleted = 0").order("name") }, class_name: "Person" + # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person" # has_many :tracks, -> { order "position" }, dependent: :destroy # has_many :comments, dependent: :nullify # has_many :tags, as: :taggable @@ -1227,7 +1227,7 @@ module ActiveRecord # # The following methods for retrieval and query of a single associated object will be added: # - # +association+ is a placeholder for the symbol passed as the first argument, so + # +association+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>. # # [association(force_reload = false)] @@ -1259,7 +1259,7 @@ module ActiveRecord # # === Options # - # The declaration can also include an options hash to specialize the behavior of the association. + # The declaration can also include an +options+ hash to specialize the behavior of the association. # # Options are: # [:class_name] @@ -1309,6 +1309,10 @@ module ActiveRecord # that is the inverse of this <tt>has_one</tt> association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. # # Option examples: # has_one :credit_card, dependent: :destroy # destroys the associated credit card @@ -1320,6 +1324,7 @@ module ActiveRecord # has_one :boss, readonly: :true # has_one :club, through: :membership # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable + # has_one :credit_card, required: true def has_one(name, scope = nil, options = {}) reflection = Builder::HasOne.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1333,7 +1338,7 @@ module ActiveRecord # Methods will be added for retrieval and query for a single associated object, for which # this object holds an id: # - # +association+ is a placeholder for the symbol passed as the first argument, so + # +association+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>. # # [association(force_reload = false)] @@ -1359,7 +1364,7 @@ module ActiveRecord # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>) # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>) # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>) - # The declaration can also include an options hash to specialize the behavior of the association. + # The declaration can also include an +options+ hash to specialize the behavior of the association. # # === Options # @@ -1413,7 +1418,7 @@ module ActiveRecord # # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [:touch] - # If true, the associated object will be touched (the updated_at/on attributes set to now) + # If true, the associated object will be touched (the updated_at/on attributes set to current time) # when this record is either saved or destroyed. If you specify a symbol, that attribute # will be updated with the current time in addition to the updated_at/on attribute. # [:inverse_of] @@ -1421,18 +1426,23 @@ module ActiveRecord # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in # combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. # # Option examples: # belongs_to :firm, foreign_key: "client_of" # belongs_to :person, primary_key: "name", foreign_key: "person_name" # belongs_to :author, class_name: "Person", foreign_key: "author_id" - # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" }, + # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count }, # class_name: "Coupon", foreign_key: "coupon_id" # belongs_to :attachable, polymorphic: true # belongs_to :project, readonly: true # belongs_to :post, counter_cache: true # belongs_to :company, touch: true # belongs_to :company, touch: :employees_last_updated_at + # belongs_to :company, required: true def belongs_to(name, scope = nil, options = {}) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1470,7 +1480,7 @@ module ActiveRecord # # Adds the following methods for retrieval and query: # - # +collection+ is a placeholder for the symbol passed as the first argument, so + # +collection+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>. # # [collection(force_reload = false)] @@ -1531,7 +1541,7 @@ module ActiveRecord # * <tt>Developer#projects.exists?(...)</tt> # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>) # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>) - # The declaration may include an options hash to specialize the behavior of the association. + # The declaration may include an +options+ hash to specialize the behavior of the association. # # === Options # @@ -1577,7 +1587,7 @@ module ActiveRecord scope = nil end - habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(:has_and_belongs_to_many, name, scope, options, self) + habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self) builder = Builder::HasAndBelongsToMany.new name, self, options diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index f085fd1cfd..947d61ee7b 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -36,6 +36,7 @@ module ActiveRecord::Associations::Builder reflection = builder.build(model) define_accessors model, reflection define_callbacks model, reflection + define_validations model, reflection builder.define_extensions model reflection end @@ -85,7 +86,11 @@ module ActiveRecord::Associations::Builder end def self.define_callbacks(model, reflection) - add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent] + if dependent = reflection.options[:dependent] + check_dependent_options(dependent) + add_destroy_callbacks(model, reflection) + end + Association.extensions.each do |extension| extension.build model, reflection end @@ -120,17 +125,23 @@ module ActiveRecord::Associations::Builder CODE end + def self.define_validations(model, reflection) + # noop + end + def self.valid_dependent_options raise NotImplementedError end private - def self.add_before_destroy_callbacks(model, reflection) - unless valid_dependent_options.include? reflection.options[:dependent] - raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}" + def self.check_dependent_options(dependent) + unless valid_dependent_options.include? dependent + raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}" end + end + def self.add_destroy_callbacks(model, reflection) name = reflection.name model.before_destroy lambda { |o| o.association(name).handle_dependency } end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 3998aca23e..954ea3878a 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -107,5 +107,10 @@ module ActiveRecord::Associations::Builder model.after_touch callback model.after_destroy callback end + + def self.add_destroy_callbacks(model, reflection) + name = reflection.name + model.after_destroy lambda { |o| o.association(name).handle_dependency } + end end end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index a1f4f51664..c194c8ae9a 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -16,7 +16,7 @@ module ActiveRecord::Associations::Builder private - def self.add_before_destroy_callbacks(model, reflection) + def self.add_destroy_callbacks(model, reflection) super unless reflection.options[:through] end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index e655c389a6..6e6dd7204c 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -3,7 +3,7 @@ module ActiveRecord::Associations::Builder class SingularAssociation < Association #:nodoc: def valid_options - super + [:remote, :dependent, :primary_key, :inverse_of] + super + [:dependent, :primary_key, :inverse_of, :required] end def self.define_accessors(model, reflection) @@ -27,5 +27,12 @@ module ActiveRecord::Associations::Builder end CODE end + + def self.define_validations(model, reflection) + super + if reflection.options[:required] + model.validates_presence_of reflection.name + 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 477888228d..1413efaf7f 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -41,6 +41,14 @@ module ActiveRecord end end + def empty? + if has_cached_counter? + size.zero? + else + super + end + end + private # Returns the number of records in this collection. @@ -80,10 +88,14 @@ module ActiveRecord end def update_counter(difference, reflection = reflection()) + update_counter_in_database(difference, reflection) + update_counter_in_memory(difference, reflection) + end + + def update_counter_in_database(difference, reflection = reflection()) if has_cached_counter?(reflection) counter = cached_counter_attribute_name(reflection) owner.class.update_counters(owner.id, counter => difference) - update_counter_in_memory(difference, reflection) end end @@ -91,7 +103,7 @@ module ActiveRecord if has_cached_counter?(reflection) counter = cached_counter_attribute_name(reflection) owner[counter] += difference - owner.changed_attributes.delete(counter) # eww + owner.send(:clear_attribute_changes, counter) # eww end end @@ -107,8 +119,12 @@ module ActiveRecord # Hence this method. def inverse_updates_counter_cache?(reflection = reflection()) counter_name = cached_counter_attribute_name(reflection) + inverse_updates_counter_named?(counter_name, reflection) + end + + def inverse_updates_counter_named?(counter_name, reflection = reflection()) reflection.klass._reflections.values.any? { |inverse_reflection| - :belongs_to == inverse_reflection.macro && + inverse_reflection.belongs_to? && inverse_reflection.counter_cache_column == counter_name } end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index af38f2f6dd..44c4436e95 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Has Many Through Association module Associations @@ -63,6 +62,15 @@ module ActiveRecord end save_through_record(record) + if has_cached_counter? && !through_reflection_updates_counter_cache? + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + Automatic updating of counter caches on through associations has been + deprecated, and will be removed in Rails 5.0. Instead, please set the + appropriate counter_cache options on the has_many and belongs_to for + your associations to #{through_reflection.name}. + MESSAGE + update_counter_in_database(1) + end record end @@ -217,6 +225,11 @@ module ActiveRecord def invertible_for?(record) false end + + def through_reflection_updates_counter_cache? + counter_name = cached_counter_attribute_name + inverse_updates_counter_named?(counter_name, through_reflection) + end end end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 944caacab6..e6095d84dc 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Belongs To Has One Association module Associations diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index fbb4551b22..ec5c189cd3 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -131,7 +131,6 @@ module ActiveRecord def instantiate(result_set, aliases) primary_key = aliases.column_alias(join_root, join_root.primary_key) - type_caster = result_set.column_type primary_key seen = Hash.new { |h,parent_klass| h[parent_klass] = Hash.new { |i,parent_id| @@ -144,8 +143,7 @@ module ActiveRecord column_aliases = aliases.column_aliases join_root result_set.each { |row_hash| - primary_id = type_caster.type_cast_from_database row_hash[primary_key] - parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases) + parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases) construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) } 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 a0e83c0a02..c3bbdccad8 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -37,14 +37,9 @@ module ActiveRecord table = tables.shift klass = reflection.klass - case reflection.source_macro - when :belongs_to - key = reflection.association_primary_key - foreign_key = reflection.foreign_key - else - key = reflection.foreign_key - foreign_key = reflection.active_record_primary_key - end + join_keys = reflection.join_keys(klass) + key = join_keys.key + foreign_key = join_keys.foreign_key constraint = build_constraint(klass, table, key, foreign_table, foreign_key) @@ -95,7 +90,7 @@ module ActiveRecord # end # # If I execute `Physician.joins(:appointments).to_a` then - # reflection # => #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...> + # klass # => Physician # table # => #<Arel::Table @name="appointments" ...> # key # => physician_id # foreign_table # => #<Arel::Table @name="physicians" ...> diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 441768f302..c0639742be 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -151,6 +151,10 @@ module ActiveRecord end end + if preload_values[:readonly] || values[:readonly] + scope.readonly! + end + if options[:as] scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 1fed7f74e7..d57da366bd 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -63,7 +63,7 @@ module ActiveRecord should_reset = (through_scope != through_reflection.klass.unscoped) || (reflection.options[:source_type] && through_reflection.collection?) - # Dont cache the association - we would only be caching a subset + # Don't cache the association - we would only be caching a subset if should_reset owners.each { |owner| owner.association(association_name).reset diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index f00fef8b9e..611d471e62 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -3,7 +3,7 @@ module ActiveRecord module Associations module ThroughAssociation #:nodoc: - delegate :source_reflection, :through_reflection, :chain, :to => :reflection + delegate :source_reflection, :through_reflection, :to => :reflection protected @@ -13,7 +13,7 @@ module ActiveRecord # 2. To get the type conditions for any STI models in the chain def target_scope scope = super - chain.drop(1).each do |reflection| + reflection.chain.drop(1).each do |reflection| relation = reflection.klass.all relation.merge!(reflection.scope) if reflection.scope @@ -77,7 +77,7 @@ module ActiveRecord end def ensure_mutable - if source_reflection.macro != :belongs_to + unless source_reflection.belongs_to? raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) end end diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index da8eb10dc6..8cc1904575 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -1,30 +1,43 @@ module ActiveRecord class Attribute # :nodoc: class << self - def from_database(value, type) - FromDatabase.new(value, type) + def from_database(name, value, type) + FromDatabase.new(name, value, type) end - def from_user(value, type) - FromUser.new(value, type) + def from_user(name, value, type) + FromUser.new(name, value, type) + end + + def null(name) + Null.new(name) + end + + def uninitialized(name, type) + Uninitialized.new(name, type) end end - attr_reader :value_before_type_cast, :type + attr_reader :name, :value_before_type_cast, :type # This method should not be called directly. # Use #from_database or #from_user - def initialize(value_before_type_cast, type) + def initialize(name, value_before_type_cast, type) + @name = name @value_before_type_cast = value_before_type_cast @type = type end def value # `defined?` is cheaper than `||=` when we get back falsy values - @value = type_cast(value_before_type_cast) unless defined?(@value) + @value = original_value unless defined?(@value) @value end + def original_value + type_cast(value_before_type_cast) + end + def value_for_database type.type_cast_for_database(value) end @@ -37,10 +50,29 @@ module ActiveRecord type.changed_in_place?(old_value, value) end - def type_cast + def with_value_from_user(value) + self.class.from_user(name, value, type) + end + + def with_value_from_database(value) + self.class.from_database(name, value, type) + end + + def type_cast(*) raise NotImplementedError end + def initialized? + true + end + + def ==(other) + self.class == other.class && + name == other.name && + value_before_type_cast == other.value_before_type_cast && + type == other.type + end + protected def initialize_dup(other) @@ -61,15 +93,39 @@ module ActiveRecord end end - class Null # :nodoc: - class << self - attr_reader :value, :value_before_type_cast, :value_for_database + class Null < Attribute # :nodoc: + def initialize(name) + super(name, nil, Type::Value.new) + end + + def value + nil + end + + def with_value_from_database(value) + raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" + end + alias_method :with_value_from_user, :with_value_from_database + end - def changed_from?(*) - false + class Uninitialized < Attribute # :nodoc: + def initialize(name, type) + super(name, nil, type) + end + + def value + if block_given? + yield name end - alias changed_in_place_from? changed_from? + end + + def value_for_database + end + + def initialized? + false end end + private_constant :FromDatabase, :FromUser, :Null, :Uninitialized end end diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 5745bbe0e3..5b96623b6e 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -26,7 +26,7 @@ module ActiveRecord def add_user_provided_columns(*) super.map do |column| - decorated_type = attribute_type_decorations.apply(self, column.name, column.cast_type) + decorated_type = attribute_type_decorations.apply(column.name, column.cast_type) column.with_type(decorated_type) end end @@ -43,8 +43,8 @@ module ActiveRecord TypeDecorator.new(@decorations.merge(*args)) end - def apply(context, name, type) - decorations = decorators_for(context, name, type) + def apply(name, type) + decorations = decorators_for(name, type) decorations.inject(type) do |new_type, block| block.call(new_type) end @@ -52,13 +52,13 @@ module ActiveRecord private - def decorators_for(context, name, type) - matching(context, name, type).map(&:last) + def decorators_for(name, type) + matching(name, type).map(&:last) end - def matching(context, name, type) + def matching(name, type) @decorations.values.select do |(matcher, _)| - context.instance_exec(name, type, &matcher) + matcher.call(name, type) end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 51f6a009db..1ff28ceccc 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -57,6 +57,8 @@ module ActiveRecord end end + class GeneratedAttributeMethods < Module; end # :nodoc: + module ClassMethods def inherited(child_class) #:nodoc: child_class.initialize_generated_modules @@ -64,7 +66,7 @@ module ActiveRecord end def initialize_generated_modules # :nodoc: - @generated_attribute_methods = Module.new { extend Mutex_m } + @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m } @attribute_methods_generated = false include @generated_attribute_methods end @@ -73,7 +75,7 @@ module ActiveRecord # accessors, mutators and query methods. def define_attribute_methods # :nodoc: return false if @attribute_methods_generated - # Use a mutex; we don't want two thread simultaneously trying to define + # Use a mutex; we don't want two threads simultaneously trying to define # attribute methods. generated_attribute_methods.synchronize do return false if @attribute_methods_generated @@ -113,10 +115,11 @@ module ActiveRecord if superclass == Base super else - # If B < A and A defines its own attribute method, then we don't want to overwrite that. - defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods) - base_defined = Base.method_defined?(method_name) || Base.private_method_defined?(method_name) - defined && !base_defined || super + # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass + # defines its own attribute method, then we don't want to overwrite that. + defined = method_defined_within?(method_name, superclass, Base) && + ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods) + defined || super end end @@ -193,7 +196,7 @@ module ActiveRecord # # person = Person.new # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter - # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> + # # => #<ActiveRecord::ConnectionAdapters::Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> # # person.column_for_attribute(:nothing) # # => nil @@ -202,8 +205,8 @@ module ActiveRecord if column.nil? ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) `column_for_attribute` will return a null object for non-existent columns - in Rails 5.0. If you would like to continue to receive `nil`, you should - instead call `model.class.columns_hash[name]` + in Rails 5.0. Use `has_attribute?` if you need to check for an + attribute's existence. MESSAGE end column @@ -251,7 +254,7 @@ module ActiveRecord # person.has_attribute?('age') # => true # person.has_attribute?(:nothing) # => false def has_attribute?(attr_name) - @attributes.include?(attr_name.to_s) + @attributes.key?(attr_name.to_s) end # Returns an array of names for the attributes available on this object. @@ -279,9 +282,9 @@ module ActiveRecord end # Returns an <tt>#inspect</tt>-like string for the value of the - # attribute +attr_name+. String attributes are truncated upto 50 + # attribute +attr_name+. String attributes are truncated up to 50 # characters, Date and Time attributes are returned in the - # <tt>:db</tt> format, Array attributes are truncated upto 10 values. + # <tt>:db</tt> format, Array attributes are truncated up to 10 values. # Other attributes return the value of <tt>#inspect</tt> without # modification. # @@ -386,7 +389,7 @@ module ActiveRecord def attribute_method?(attr_name) # :nodoc: # We check defined? because Syck calls respond_to? before actually calling initialize. - defined?(@attributes) && @attributes.include?(attr_name) + defined?(@attributes) && @attributes.key?(attr_name) end private diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index 9ee9a7815f..fd61febd57 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -57,7 +57,7 @@ module ActiveRecord # task.attributes_before_type_cast # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast - @attributes.each_with_object({}) { |(k, v), h| h[k] = v.value_before_type_cast } + @attributes.values_before_type_cast end private diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index e1a86fd3aa..d3f4e51c33 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -34,7 +34,7 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - reset_changes + clear_changes_information end end @@ -51,30 +51,26 @@ module ActiveRecord super | changed_in_place end - def attribute_changed?(attr_name, options = {}) - result = super - # We can't change "from" something in place. Only setters can define - # "from" and "to" - result ||= changed_in_place?(attr_name) unless options.key?(:from) - result - end - def changes_applied super store_original_raw_attributes end - def reset_changes + def clear_changes_information super original_raw_attributes.clear end + def changed_attributes + super.reverse_merge(attributes_changed_in_place).freeze + end + private def calculate_changes_from_defaults @changed_attributes = nil self.class.column_defaults.each do |attr, orig_value| - changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value) + set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value) end end @@ -100,9 +96,9 @@ module ActiveRecord def save_changed_attribute(attr, old_value) if attribute_changed?(attr) - changed_attributes.delete(attr) unless _field_changed?(attr, old_value) + clear_attribute_changes(attr) unless _field_changed?(attr, old_value) else - changed_attributes[attr] = old_value if _field_changed?(attr, old_value) + set_attribute_was(attr, old_value) if _field_changed?(attr, old_value) end end @@ -132,6 +128,13 @@ module ActiveRecord @attributes[attr].changed_from?(old_value) end + def attributes_changed_in_place + changed_in_place.each_with_object({}) do |attr_name, h| + orig = @attributes[attr_name].original_value + h[attr_name] = orig + end + end + def changed_in_place self.class.attribute_names.select do |attr_name| changed_in_place?(attr_name) diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 1c81a5b71b..9bd333bbac 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -39,6 +39,12 @@ module ActiveRecord read_attribute_before_type_cast(self.class.primary_key) end + # Returns the primary key previous value. + def id_was + sync_with_transaction_state + attribute_was(self.class.primary_key) + end + protected def attribute_method?(attr_name) @@ -54,7 +60,7 @@ module ActiveRecord end end - ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast).to_set + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set def dangerous_attribute_method?(method_name) super && !ID_ATTRIBUTE_METHODS.include?(method_name) @@ -83,12 +89,9 @@ module ActiveRecord end def get_primary_key(base_name) #:nodoc: - return 'id' if base_name.blank? - - case primary_key_prefix_type - when :table_name + if base_name && primary_key_prefix_type == :table_name base_name.foreign_key(false) - when :table_name_with_underscore + elsif base_name && primary_key_prefix_type == :table_name_with_underscore base_name.foreign_key else if ActiveRecord::Base != self && table_exists? diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 8c1cc128f7..10869dfc1e 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -81,17 +81,10 @@ module ActiveRecord # Returns the value of the attribute identified by <tt>attr_name</tt> after # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). - def read_attribute(attr_name) + def read_attribute(attr_name, &block) name = attr_name.to_s - @attributes.fetch(name) { - if name == 'id' - return read_attribute(self.class.primary_key) - elsif block_given? && self.class.columns_hash.key?(name) - return yield(name) - else - return nil - end - }.value + name = self.class.primary_key if name == 'id' + @attributes.fetch_value(name, &block) end private diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 734d94865a..264ce2bdfa 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -37,7 +37,12 @@ module ActiveRecord # serialize :preferences, Hash # end def serialize(attr_name, class_name_or_coder = Object) - coder = if [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } + # When ::JSON is used, force it to go through the Active Support JSON encoder + # to ensure special objects (e.g. Active Record models) are dumped correctly + # using the #as_json hook. + coder = if class_name_or_coder == ::JSON + Coders::JSON + elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } class_name_or_coder else Coders::YAMLColumn.new(class_name_or_coder) 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 5f1dc8bc9f..f439bd1ffe 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -33,15 +33,25 @@ module ActiveRecord class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false self.skip_time_zone_conversion_for_attributes = [] - - matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) } - decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type| - TimeZoneConverter.new(type) - end end module ClassMethods private + + def inherited(subclass) + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or + # `skip_time_zone_conversion_for_attributes` would not be picked up. + subclass.class_eval do + matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) } + decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type| + TimeZoneConverter.new(type) + end + end + super + end + def create_time_zone_conversion_attribute?(name, cast_type) time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 246a2cd8ba..b3c8209a74 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -69,16 +69,11 @@ module ActiveRecord def write_attribute_with_type_cast(attr_name, value, should_type_cast) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - type = type_for_attribute(attr_name) - - unless has_attribute?(attr_name) || self.class.columns_hash.key?(attr_name) - raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" - end if should_type_cast - @attributes[attr_name] = Attribute.from_user(value, type) + @attributes.write_from_user(attr_name, value) else - @attributes[attr_name] = Attribute.from_database(value, type) + @attributes.write_from_database(attr_name, value) end value diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index ed2500a675..98ac63c7e1 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -1,27 +1,49 @@ +require 'active_record/attribute_set/builder' + module ActiveRecord class AttributeSet # :nodoc: - delegate :[], :[]=, :fetch, :include?, :keys, :each_with_object, to: :attributes + delegate :keys, to: :initialized_attributes def initialize(attributes) @attributes = attributes end + def [](name) + attributes[name] || Attribute.null(name) + end + + def values_before_type_cast + attributes.transform_values(&:value_before_type_cast) + end + def to_hash - attributes.each_with_object({}) { |(k, v), h| h[k] = v.value } + initialized_attributes.transform_values(&:value) end alias_method :to_h, :to_hash + def key?(name) + attributes.key?(name) && self[name].initialized? + end + + def fetch_value(name, &block) + self[name].value(&block) + end + + def write_from_database(name, value) + attributes[name] = self[name].with_value_from_database(value) + end + + def write_from_user(name, value) + attributes[name] = self[name].with_value_from_user(value) + end + def freeze @attributes.freeze super end def initialize_dup(_) - @attributes = attributes.dup - attributes.each do |key, attr| - attributes[key] = attr.dup - end - + @attributes = attributes.transform_values(&:dup) super end @@ -30,23 +52,26 @@ module ActiveRecord super end - class Builder # :nodoc: - def initialize(types) - @types = types + def reset(key) + if key?(key) + write_from_database(key, nil) end + end - def build_from_database(values, additional_types = {}) - attributes = Hash.new(Attribute::Null) - values.each_with_object(attributes) do |(name, value), hash| - type = additional_types.fetch(name, @types[name]) - hash[name] = Attribute.from_database(value, type) - end - AttributeSet.new(attributes) + def ensure_initialized(key) + unless self[key].initialized? + write_from_database(key, nil) end end protected attr_reader :attributes + + private + + def initialized_attributes + attributes.select { |_, attr| attr.initialized? } + end end end diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb new file mode 100644 index 0000000000..1e146a07da --- /dev/null +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -0,0 +1,32 @@ +module ActiveRecord + class AttributeSet # :nodoc: + class Builder # :nodoc: + attr_reader :types + + def initialize(types) + @types = types + end + + def build_from_database(values = {}, additional_types = {}) + attributes = build_attributes_from_values(values, additional_types) + add_uninitialized_attributes(attributes) + AttributeSet.new(attributes) + end + + private + + def build_attributes_from_values(values, additional_types) + values.each_with_object({}) do |(name, value), hash| + type = additional_types.fetch(name, types[name]) + hash[name] = Attribute.from_database(name, value, type) + end + end + + def add_uninitialized_attributes(attributes) + types.except(*attributes.keys).each do |name, type| + attributes[name] = Attribute.uninitialized(name, type) + end + end + end + end +end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 492d8f3560..890a1314d9 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -110,13 +110,12 @@ module ActiveRecord def clear_caches_calculated_from_columns @attributes_builder = nil - @column_defaults = nil @column_names = nil @column_types = nil @columns = nil @columns_hash = nil @content_columns = nil - @raw_column_defaults = nil + @default_attributes = nil end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 662c99269e..f978fbd0a4 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -9,6 +9,7 @@ require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/transform_values' require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/introspection' @@ -140,6 +141,7 @@ module ActiveRecord #:nodoc: # # In addition to the basic accessors, query methods are also automatically available on the Active Record object. # Query methods allow you to test whether an attribute value is present. + # For numeric values, present is defined as non-zero. # # For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call # to determine whether the user has a name: @@ -219,25 +221,9 @@ module ActiveRecord #:nodoc: # # == Single table inheritance # - # Active Record allows inheritance by storing the name of the class in a column that by - # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). - # This means that an inheritance looking like this: - # - # class Company < ActiveRecord::Base; end - # class Firm < Company; end - # class Client < Company; end - # class PriorityClient < Client; end - # - # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in - # the companies table with type = "Firm". You can then fetch this row again using - # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object. - # - # If you don't have a type column defined in your table, single-table inheritance won't - # be triggered. In that case, it'll work just like normal subclasses with no special magic - # for differentiating between them or reloading the right type with find. - # - # Note, all the attributes for all the cases are kept in the same table. Read more: - # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # Active Record allows inheritance by storing the name of the class in a + # column that is named "type" by default. See ActiveRecord::Inheritance for + # more details. # # == Connection to multiple databases in different models # diff --git a/activerecord/lib/active_record/coders/json.rb b/activerecord/lib/active_record/coders/json.rb new file mode 100644 index 0000000000..75d3bfe625 --- /dev/null +++ b/activerecord/lib/active_record/coders/json.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module Coders # :nodoc: + class JSON # :nodoc: + def self.dump(obj) + ActiveSupport::JSON.encode(obj) + end + + def self.load(json) + ActiveSupport::JSON.decode(json) unless json.nil? + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index cb75070e3a..a5fa9d6adc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -462,23 +462,44 @@ module ActiveRecord # # For example, suppose that you have 5 models, with the following hierarchy: # - # | - # +-- Book - # | | - # | +-- ScaryBook - # | +-- GoodBook - # +-- Author - # +-- BankAccount + # class Author < ActiveRecord::Base + # end # - # Suppose that Book is to connect to a separate database (i.e. one other - # than the default database). Then Book, ScaryBook and GoodBook will all use - # the same connection pool. Likewise, Author and BankAccount will use the - # same connection pool. However, the connection pool used by Author/BankAccount - # is not the same as the one used by Book/ScaryBook/GoodBook. + # class BankAccount < ActiveRecord::Base + # end # - # Normally there is only a single ConnectionHandler instance, accessible via - # ActiveRecord::Base.connection_handler. Active Record models use this to - # determine the connection pool that they should use. + # class Book < ActiveRecord::Base + # establish_connection "library_db" + # end + # + # class ScaryBook < Book + # end + # + # class GoodBook < Book + # end + # + # And a database.yml that looked like this: + # + # development: + # database: my_application + # host: localhost + # + # library_db: + # database: library + # host: some.library.org + # + # Your primary database in the development environment is "my_application" + # but the Book model connects to a separate database called "library_db" + # (this can even be a database on a different machine). + # + # Book, ScaryBook and GoodBook will all use the same connection pool to + # "library_db" while Author, BankAccount, and any other models you create + # will use the default connection pool to "my_application". + # + # The various connection pools are managed by a single instance of + # ConnectionHandler accessible via ActiveRecord::Base.connection_handler. + # All Active Record models use this handler to determine the connection pool that they + # should use. class ConnectionHandler def initialize # These caches are keyed by klass.name, NOT klass. Keying them by klass 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 e8ce00d92b..98e96099cb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -203,62 +203,30 @@ module ActiveRecord if options[:isolation] raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" end - yield else - within_new_transaction(options) { yield } + transaction_manager.within_new_transaction(options) { yield } end rescue ActiveRecord::Rollback # rollbacks are silently swallowed end - def within_new_transaction(options = {}) #:nodoc: - transaction = begin_transaction(options) - yield - rescue Exception => error - rollback_transaction if transaction - raise - ensure - begin - commit_transaction unless error - rescue Exception - rollback_transaction - raise - end - end - - def open_transactions - @transaction.number - end + attr_reader :transaction_manager #:nodoc: - def current_transaction #:nodoc: - @transaction - end + delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager def transaction_open? - @transaction.open? - end - - def begin_transaction(options = {}) #:nodoc: - @transaction = @transaction.begin(options) - end - - def commit_transaction #:nodoc: - @transaction = @transaction.commit - end - - def rollback_transaction #:nodoc: - @transaction = @transaction.rollback + current_transaction.open? end def reset_transaction #:nodoc: - @transaction = ClosedTransaction.new(self) + @transaction_manager = TransactionManager.new(self) end # Register a record with the current transaction so that its after_commit and after_rollback callbacks # can be called. def add_transaction_record(record) - @transaction.add_record(record) + current_transaction.add_record(record) end # Begins the transaction (and turns off auto-committing). diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index ff92375820..eb88845913 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -9,12 +9,7 @@ module ActiveRecord # records are quoted as their primary key return value.quoted_id if value.respond_to?(:quoted_id) - # FIXME: The only case we get an object other than nil or a real column - # is `SchemaStatements#add_column` with a PG array that has a non-empty default - # value. Is this really the only case? Are we missing tests for other types? - # We should have a real column object passed (or nil) here, and check for that - # instead - if column.respond_to?(:cast_type) + if column value = column.cast_type.type_cast_for_database(value) end @@ -29,12 +24,7 @@ module ActiveRecord return value.id end - # FIXME: The only case we get an object other than nil or a real column - # is `SchemaStatements#add_column` with a PG array that has a non-empty default - # value. Is this really the only case? Are we missing tests for other types? - # We should have a real column object passed (or nil) here, and check for that - # instead - if column.respond_to?(:cast_type) + if column value = column.cast_type.type_cast_for_database(value) end @@ -66,7 +56,7 @@ module ActiveRecord # This works for mysql and mysql2 where table.column can be used to # resolve ambiguity. # - # We override this in the sqlite and postgresql adapters to use only + # We override this in the sqlite3 and postgresql adapters to use only # the column name (as per syntax requirements). def quote_table_name_for_assignment(table, attr) quote_table_name("#{table}.#{attr}") 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 47fe501752..adad6cd542 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -23,6 +23,8 @@ module ActiveRecord def visit_AlterTable(o) sql = "ALTER TABLE #{quote_table_name(o.name)} " sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ') + sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ') end def visit_ColumnDefinition(o) @@ -41,6 +43,21 @@ module ActiveRecord create_sql end + def visit_AddForeignKey(o) + sql = <<-SQL.strip_heredoc + ADD CONSTRAINT #{quote_column_name(o.name)} + FOREIGN KEY (#{quote_column_name(o.column)}) + REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) + SQL + sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete + sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update + sql + end + + def visit_DropForeignKey(name) + "DROP CONSTRAINT #{quote_column_name(name)}" + end + def column_options(o) column_options = {} column_options[:null] = o.null unless o.null.nil? @@ -77,6 +94,7 @@ module ActiveRecord def quote_value(value, column) column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale) + column.cast_type ||= type_for_column(column) @conn.quote(value, column) end @@ -84,6 +102,23 @@ module ActiveRecord def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end + + def action_sql(action, dependency) + case dependency + when :nullify then "ON #{action} SET NULL" + when :cascade then "ON #{action} CASCADE" + when :restrict then "ON #{action} RESTRICT" + else + raise ArgumentError, <<-MSG.strip_heredoc + '#{dependency}' is not supported for :on_update or :on_delete. + Supported values are: :nullify, :cascade, :restrict + MSG + end + end + + def type_for_column(column) + @conn.lookup_cast_type(column.sql_type) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index a9b3e9cfb9..9e07e9a5c4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,7 +15,7 @@ module ActiveRecord # 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, :primary_key, :sql_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type, :cast_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key @@ -25,6 +25,50 @@ module ActiveRecord class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc: end + class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: + def name + options[:name] + end + + def column + options[:column] + end + + def primary_key + options[:primary_key] || default_primary_key + end + + def on_delete + options[:on_delete] + end + + def on_update + options[:on_update] + end + + def custom_primary_key? + options[:primary_key] != default_primary_key + end + + private + def default_primary_key + "id" + end + end + + module TimestampDefaultDeprecation # :nodoc: + def emit_warning_if_null_unspecified(options) + return if options.key?(:null) + + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `timestamp` was called without specifying an option for `null`. In Rails + 5.0, this behavior will change to `null: false`. You should manually + specify `null: true` to prevent the behavior of your existing migrations + from changing. + MESSAGE + end + end + # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # @@ -46,6 +90,8 @@ module ActiveRecord # The table definitions # The Columns are stored as a ColumnDefinition in the +columns+ attribute. class TableDefinition + include TimestampDefaultDeprecation + # An array of ColumnDefinition objects, representing the column changes # that have been defined. attr_accessor :indexes @@ -245,16 +291,27 @@ module ActiveRecord # <tt>:updated_at</tt> to the table. def timestamps(*args) options = args.extract_options! + emit_warning_if_null_unspecified(options) column(:created_at, :datetime, options) column(:updated_at, :datetime, options) end + # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. + # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+ + # by default, the <tt>:type</tt> option can be used to specify a different type. + # + # t.references(:user) + # t.references(:user, type: "string") + # t.belongs_to(:supplier, polymorphic: true) + # + # See SchemaStatements#add_reference def references(*args) options = args.extract_options! polymorphic = options.delete(:polymorphic) index_options = options.delete(:index) + type = options.delete(:type) || :integer args.each do |col| - column("#{col}_id", :integer, options) + column("#{col}_id", type, options) column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options end @@ -303,14 +360,26 @@ module ActiveRecord class AlterTable # :nodoc: attr_reader :adds + attr_reader :foreign_key_adds + attr_reader :foreign_key_drops def initialize(td) @td = td @adds = [] + @foreign_key_adds = [] + @foreign_key_drops = [] end def name; @td.name; end + def add_foreign_key(to_table, options) + @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options) + end + + def drop_foreign_key(name) + @foreign_key_drops << name + end + def add_column(name, type, options) name = name.to_s type = type.to_sym @@ -352,6 +421,8 @@ module ActiveRecord # end # class Table + include TimestampDefaultDeprecation + def initialize(table_name, base) @table_name = table_name @base = base @@ -399,8 +470,9 @@ module ActiveRecord # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps # # t.timestamps - def timestamps - @base.add_timestamps(@table_name) + def timestamps(options = {}) + emit_warning_if_null_unspecified(options) + @base.add_timestamps(@table_name, options) end # Changes the column's definition according to the new options. @@ -457,11 +529,14 @@ module ActiveRecord end # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. - # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. + # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+ + # by default, the <tt>:type</tt> option can be used to specify a different type. # # t.references(:user) + # t.references(:user, type: "string") # t.belongs_to(:supplier, polymorphic: true) # + # See SchemaStatements#add_reference def references(*args) options = args.extract_options! args.each do |ref_name| @@ -476,6 +551,7 @@ module ActiveRecord # t.remove_references(:user) # t.remove_belongs_to(:supplier, polymorphic: true) # + # See SchemaStatements#remove_reference def remove_references(*args) options = args.extract_options! args.each do |ref_name| @@ -502,6 +578,5 @@ module ActiveRecord @base.native_database_types end end - end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 22823a8c58..7105df1ee4 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -43,13 +43,14 @@ module ActiveRecord # index_exists?(:suppliers, :company_id, name: "idx_company_id") # def index_exists?(table_name, column_name, options = {}) - column_names = Array(column_name) - index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names) - if options[:unique] - indexes(table_name).any?{ |i| i.unique && i.name == index_name } - else - indexes(table_name).any?{ |i| i.name == index_name } - end + column_names = Array(column_name).map(&:to_s) + index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names) + checks = [] + checks << lambda { |i| i.name == index_name } + checks << lambda { |i| i.columns == column_names } + checks << lambda { |i| i.unique } if options[:unique] + + indexes(table_name).any? { |i| checks.all? { |check| check[i] } } end # Returns an array of Column objects for the table specified by +table_name+. @@ -602,12 +603,18 @@ module ActiveRecord end # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. + # The reference column is an +integer+ by default, the <tt>:type</tt> option can be used to specify + # a different type. # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. # - # ====== Create a user_id column + # ====== Create a user_id integer column # # add_reference(:products, :user) # + # ====== Create a user_id string column + # + # add_reference(:products, :user, type: :string) + # # ====== Create a supplier_id and supplier_type columns # # add_belongs_to(:products, :supplier, polymorphic: true) @@ -619,7 +626,8 @@ module ActiveRecord def add_reference(table_name, ref_name, options = {}) polymorphic = options.delete(:polymorphic) index_options = options.delete(:index) - add_column(table_name, "#{ref_name}_id", :integer, options) + type = options.delete(:type) || :integer + add_column(table_name, "#{ref_name}_id", type, options) add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options end @@ -642,6 +650,115 @@ module ActiveRecord end alias :remove_belongs_to :remove_reference + # Returns an array of foreign keys for the given table. + # The foreign keys are represented as +ForeignKeyDefinition+ objects. + def foreign_keys(table_name) + raise NotImplementedError, "foreign_keys is not implemented" + end + + # Adds a new foreign key. +from_table+ is the table with the key column, + # +to_table+ contains the referenced primary key. + # + # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>. + # +identifier+ is a 10 character long random string. A custom name can be specified with + # the <tt>:name</tt> option. + # + # ====== Creating a simple foreign key + # + # add_foreign_key :articles, :authors + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") + # + # ====== Creating a foreign key on a specific column + # + # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id" + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id") + # + # ====== Creating a cascading foreign key + # + # add_foreign_key :articles, :authors, on_delete: :cascade + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE + # + # The +options+ hash can include the following keys: + # [<tt>:column</tt>] + # The foreign key column name on +from_table+. Defaults to <tt>to_table.singularize + "_id"</tt> + # [<tt>:primary_key</tt>] + # The primary key column name on +to_table+. Defaults to +id+. + # [<tt>:name</tt>] + # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>. + # [<tt>:on_delete</tt>] + # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # [<tt>:on_update</tt>] + # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + def add_foreign_key(from_table, to_table, options = {}) + return unless supports_foreign_keys? + + options[:column] ||= foreign_key_column_for(to_table) + + options = { + column: options[:column], + primary_key: options[:primary_key], + name: foreign_key_name(from_table, options), + on_delete: options[:on_delete], + on_update: options[:on_update] + } + at = create_alter_table from_table + at.add_foreign_key to_table, options + + execute schema_creation.accept(at) + end + + # Removes the given foreign key from the table. + # + # Removes the foreign key on +accounts.branch_id+. + # + # remove_foreign_key :accounts, :branches + # + # Removes the foreign key on +accounts.owner_id+. + # + # remove_foreign_key :accounts, column: :owner_id + # + # Removes the foreign key named +special_fk_name+ on the +accounts+ table. + # + # remove_foreign_key :accounts, name: :special_fk_name + # + def remove_foreign_key(from_table, options_or_to_table = {}) + return unless supports_foreign_keys? + + if options_or_to_table.is_a?(Hash) + options = options_or_to_table + else + options = { column: foreign_key_column_for(options_or_to_table) } + end + + fk_name_to_delete = options.fetch(:name) do + fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column] } + + if fk_to_delete + fk_to_delete.name + else + raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'" + end + end + + at = create_alter_table from_table + at.drop_foreign_key fk_name_to_delete + + execute schema_creation.accept(at) + end + + def foreign_key_column_for(table_name) # :nodoc: + "#{table_name.to_s.singularize}_id" + end + def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name @@ -722,9 +839,9 @@ module ActiveRecord # # add_timestamps(:suppliers) # - def add_timestamps(table_name) - add_column table_name, :created_at, :datetime - add_column table_name, :updated_at, :datetime + def add_timestamps(table_name, options = {}) + add_column table_name, :created_at, :datetime, options + add_column table_name, :updated_at, :datetime, options end # Removes the timestamp columns (+created_at+ and +updated_at+) from the table definition. @@ -852,6 +969,12 @@ module ActiveRecord def create_alter_table(name) AlterTable.new create_table_definition(name, false, {}) end + + def foreign_key_name(table_name, options) # :nodoc: + options.fetch(:name) do + "fk_rails_#{SecureRandom.hex(5)}" + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index bc4884b538..8f06cf3a1f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -1,20 +1,7 @@ module ActiveRecord module ConnectionAdapters - class Transaction #:nodoc: - attr_reader :connection - - def initialize(connection) - @connection = connection - @state = TransactionState.new - end - - def state - @state - end - end - class TransactionState - attr_accessor :parent + attr_reader :parent VALID_STATES = Set.new([:committed, :rolledback, nil]) @@ -43,82 +30,24 @@ module ActiveRecord end end - class ClosedTransaction < Transaction #:nodoc: - def number - 0 - end - - def begin(options = {}) - RealTransaction.new(connection, self, options) - end - - def closed? - true - end - - def open? - false - end - - def joinable? - false - end - - # This is a noop when there are no open transactions - def add_record(record) - end + class NullTransaction #:nodoc: + def initialize; end + def closed?; true; end + def open?; false; end + def joinable?; false; end + def add_record(record); end end - class OpenTransaction < Transaction #:nodoc: - attr_reader :parent, :records - attr_writer :joinable - - def initialize(connection, parent, options = {}) - super connection - - @parent = parent - @records = [] - @finishing = false - @joinable = options.fetch(:joinable, true) - end - - # This state is necessary so that we correctly handle stuff that might - # happen in a commit/rollback. But it's kinda distasteful. Maybe we can - # find a better way to structure it in the future. - def finishing? - @finishing - end - - def joinable? - @joinable && !finishing? - end - - def number - if finishing? - parent.number - else - parent.number + 1 - end - end - - def begin(options = {}) - if finishing? - parent.begin - else - SavepointTransaction.new(connection, self, options) - end - end + class Transaction #:nodoc: - def rollback - @finishing = true - perform_rollback - parent - end + attr_reader :connection, :state, :records, :savepoint_name + attr_writer :joinable - def commit - @finishing = true - perform_commit - parent + def initialize(connection, options) + @connection = connection + @state = TransactionState.new + @records = [] + @joinable = options.fetch(:joinable, true) end def add_record(record) @@ -129,19 +58,25 @@ module ActiveRecord end end - def rollback_records + def rollback @state.set_state(:rolledback) + end + + def rollback_records records.uniq.each do |record| begin - record.rolledback!(parent.closed?) + record.rolledback! full_rollback? rescue => e record.logger.error(e) if record.respond_to?(:logger) && record.logger end end end - def commit_records + def commit @state.set_state(:committed) + end + + def commit_records records.uniq.each do |record| begin record.committed! @@ -151,19 +86,42 @@ module ActiveRecord end end - def closed? - false + def full_rollback?; true; end + def joinable?; @joinable; end + def closed?; false; end + def open?; !closed?; end + end + + class SavepointTransaction < Transaction + + def initialize(connection, savepoint_name, options) + super(connection, options) + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" + end + connection.create_savepoint(@savepoint_name = savepoint_name) + end + + def rollback + super + connection.rollback_to_savepoint(savepoint_name) + rollback_records end - def open? - true + def commit + super + connection.release_savepoint(savepoint_name) + parent = connection.transaction_manager.current_transaction + records.each { |r| parent.add_record(r) } end + + def full_rollback?; false; end end - class RealTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent, options = {}) - super + class RealTransaction < Transaction + def initialize(connection, options) + super if options[:isolation] connection.begin_isolated_db_transaction(options[:isolation]) else @@ -171,37 +129,69 @@ module ActiveRecord end end - def perform_rollback + def rollback + super connection.rollback_db_transaction rollback_records end - def perform_commit + def commit + super connection.commit_db_transaction commit_records end end - class SavepointTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent, options = {}) - if options[:isolation] - raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" - end + class TransactionManager #:nodoc: + def initialize(connection) + @stack = [] + @connection = connection + end - super - connection.create_savepoint + def begin_transaction(options = {}) + transaction = + if @stack.empty? + RealTransaction.new(@connection, options) + else + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options) + end + @stack.push(transaction) + transaction end - def perform_rollback - connection.rollback_to_savepoint - rollback_records + def commit_transaction + @stack.pop.commit end - def perform_commit - @state.set_state(:committed) - @state.parent = parent.state - connection.release_savepoint + def rollback_transaction + @stack.pop.rollback + end + + def within_new_transaction(options = {}) + transaction = begin_transaction options + yield + rescue Exception => error + rollback_transaction if transaction + raise + ensure + begin + commit_transaction unless error + rescue Exception + transaction.rollback + raise + end end + + def open_transactions + @stack.size + end + + def current_transaction + @stack.last || NULL_TRANSACTION + end + + private + NULL_TRANSACTION = NullTransaction.new end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index cc494a7f40..a1b6671664 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -45,7 +45,8 @@ module ActiveRecord end autoload_at 'active_record/connection_adapters/abstract/transaction' do - autoload :ClosedTransaction + autoload :TransactionManager + autoload :NullTransaction autoload :RealTransaction autoload :SavepointTransaction autoload :TransactionState @@ -233,6 +234,11 @@ module ActiveRecord false end + # Does this adapter support creating foreign key constraints? + def supports_foreign_keys? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -352,7 +358,7 @@ module ActiveRecord end def current_savepoint_name - "active_record_#{open_transactions}" + current_transaction.savepoint_name end # Check the connection back in to the connection pool @@ -370,12 +376,12 @@ module ActiveRecord Column.new(name, default, cast_type, sql_type, null) end - protected - def lookup_cast_type(sql_type) # :nodoc: type_map.lookup(sql_type) end + protected + def initialize_type_map(m) # :nodoc: register_class_with_limit m, %r(boolean)i, Type::Boolean register_class_with_limit m, %r(char)i, Type::String 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 3ef8878ad1..a1c370b05d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -12,6 +12,10 @@ module ActiveRecord private + def visit_DropForeignKey(name) + "DROP FOREIGN KEY #{name}" + end + def visit_TableDefinition(o) name = o.name create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} " @@ -192,6 +196,10 @@ module ActiveRecord true end + def supports_foreign_keys? + true + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -381,7 +389,7 @@ module ActiveRecord end def table_exists?(name) - return false unless name + return false unless name.present? return true if tables(nil, nil, name).any? name = name.to_s @@ -465,7 +473,7 @@ module ActiveRecord end def rename_index(table_name, old_name, new_name) - if (version[0] == 5 && version[1] >= 7) || version[0] >= 6 + if supports_rename_index? execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}" else super @@ -501,6 +509,34 @@ module ActiveRecord execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}" end + def foreign_keys(table_name) + fk_info = select_all <<-SQL.strip_heredoc + SELECT fk.referenced_table_name as 'to_table' + ,fk.referenced_column_name as 'primary_key' + ,fk.column_name as 'column' + ,fk.constraint_name as 'name' + FROM information_schema.key_column_usage fk + WHERE fk.referenced_column_name is not null + AND fk.table_schema = '#{@config[:database]}' + AND fk.table_name = '#{table_name}' + SQL + + create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE") + options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE") + + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s @@ -728,8 +764,8 @@ module ActiveRecord "DROP INDEX #{index_name}" end - def add_timestamps_sql(table_name) - [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)] + def add_timestamps_sql(table_name, options = {}) + [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)] end def remove_timestamps_sql(table_name) @@ -738,10 +774,22 @@ module ActiveRecord private + def version + @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + end + + def mariadb? + full_version =~ /mariadb/i + end + def supports_views? version[0] >= 5 end + def supports_rename_index? + mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6 + end + def configure_connection variables = @config.fetch(:variables, {}).stringify_keys @@ -757,8 +805,8 @@ module ActiveRecord # Make MySQL reject illegal values rather than truncating or blanking them, see # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. - if strict_mode? && !variables.has_key?('sql_mode') - variables['sql_mode'] = 'STRICT_ALL_TABLES' + unless variables.has_key?('sql_mode') + variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' end # NAMES does not have an equals sign, see @@ -779,6 +827,15 @@ module ActiveRecord # ...and send them all in one query @connection.query "SET #{encoding} #{variable_assignments}" end + + def extract_foreign_key_action(structure, name, action) # :nodoc: + if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/ + case $1 + when 'CASCADE'; :cascade + when 'SET NULL'; :nullify + end + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 1f1e2c46f4..5f9cc6edd0 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -16,7 +16,7 @@ module ActiveRecord attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function delegate :type, :precision, :scale, :limit, :klass, :accessor, - :text?, :number?, :binary?, :changed?, + :number?, :binary?, :changed?, :type_cast_from_user, :type_cast_from_database, :type_cast_for_database, :type_cast_for_schema, to: :cast_type diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 2fcb085ab2..d28a54b8f9 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -32,7 +32,7 @@ module ActiveRecord # } def initialize(url) raise "Database URL cannot be empty" if url.blank? - @uri = URI.parse(url) + @uri = uri_parser.parse(url) @adapter = @uri.scheme.gsub('-', '_') @adapter = "postgresql" if @adapter == "postgres" @@ -160,7 +160,7 @@ module ActiveRecord # config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } } # spec = Resolver.new(config).spec(:production) # spec.adapter_method - # # => "sqlite3" + # # => "sqlite3_connection" # spec.config # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } # @@ -250,7 +250,7 @@ module ActiveRecord # Connection details inside of the "url" key win any merge conflicts def resolve_hash_connection(spec) if spec["url"] && spec["url"] !~ /^jdbc:/ - connection_hash = resolve_string_connection(spec.delete("url")) + connection_hash = resolve_url_connection(spec.delete("url")) spec.merge!(connection_hash) end spec diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 0a14cdfe89..39d52e6349 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -269,8 +269,8 @@ module ActiveRecord super end - def version - @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + def full_version + @full_version ||= @connection.info[:version] end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index ad07a46e51..a03bc28744 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -470,9 +470,9 @@ module ActiveRecord rows end - # Returns the version of the connected MySQL server. - def version - @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + # Returns the full version of the connected MySQL server. + def full_version + @full_version ||= @connection.server_info end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb deleted file mode 100644 index a865c5c310..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ /dev/null @@ -1,81 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module Cast # :nodoc: - def point_to_string(point) # :nodoc: - "(#{number_for_point(point[0])},#{number_for_point(point[1])})" - end - - def number_for_point(number) - number.to_s.gsub(/\.0$/, '') - end - - def hstore_to_string(object, array_member = false) # :nodoc: - if Hash === object - string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ') - string = escape_hstore(string) if array_member - string - else - object - end - end - - def string_to_hstore(string) # :nodoc: - if string.nil? - nil - elsif String === string - Hash[string.scan(HstorePair).map { |k, v| - v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - [k, v] - }] - else - string - end - end - - def json_to_string(object) # :nodoc: - if Hash === object || Array === object - ActiveSupport::JSON.encode(object) - else - object - end - end - - def range_to_string(object) # :nodoc: - from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin - to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end - "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}" - end - - def string_to_json(string) # :nodoc: - if String === string - ActiveSupport::JSON.decode(string) - else - string - end - end - - private - - HstorePair = begin - quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ - unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ - /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ - end - - def escape_hstore(value) - if value.nil? - 'NULL' - else - if value == "" - '""' - else - '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') - end - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index 847fd4dded..37e5c3859c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -1,11 +1,7 @@ -require 'active_record/connection_adapters/postgresql/cast' - module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: - extend PostgreSQL::Cast - attr_accessor :array def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 33a98b4fcb..d28a2b4fa0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -14,6 +14,7 @@ require 'active_record/connection_adapters/postgresql/oid/hstore' require 'active_record/connection_adapters/postgresql/oid/inet' require 'active_record/connection_adapters/postgresql/oid/integer' require 'active_record/connection_adapters/postgresql/oid/json' +require 'active_record/connection_adapters/postgresql/oid/jsonb' require 'active_record/connection_adapters/postgresql/oid/money' require 'active_record/connection_adapters/postgresql/oid/point' require 'active_record/connection_adapters/postgresql/oid/range' @@ -21,6 +22,7 @@ require 'active_record/connection_adapters/postgresql/oid/specialized_string' require 'active_record/connection_adapters/postgresql/oid/time' require 'active_record/connection_adapters/postgresql/oid/uuid' require 'active_record/connection_adapters/postgresql/oid/vector' +require 'active_record/connection_adapters/postgresql/oid/xml' require 'active_record/connection_adapters/postgresql/oid/type_map_initializer' diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index 243ecd13cf..1dbb40ca1d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -19,6 +19,32 @@ module ActiveRecord value end end + + def type_cast_for_database(value) + Data.new(super) if value + end + + class Data + def initialize(value) + @value = value + end + + def to_s + value + end + + def binary? + /\A[01]*\Z/ === value + end + + def hex? + /\A[0-9A-F]*\Z/i === value + end + + protected + + attr_reader :value + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb index 26c5d3d78f..78ef94b912 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb @@ -7,6 +7,7 @@ module ActiveRecord def cast_value(value) case value + when ::Float then value when 'Infinity' then ::Float::INFINITY when '-Infinity' then -::Float::INFINITY when 'NaN' then ::Float::NAN diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index 57b20477df..be4525c94f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -10,16 +10,48 @@ module ActiveRecord end def type_cast_from_database(value) - ConnectionAdapters::PostgreSQLColumn.string_to_hstore(value) + if value.is_a?(::String) + ::Hash[value.scan(HstorePair).map { |k, v| + v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + [k, v] + }] + else + value + end end def type_cast_for_database(value) - ConnectionAdapters::PostgreSQLColumn.hstore_to_string(value) + if value.is_a?(::Hash) + value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ') + else + value + end end def accessor ActiveRecord::Store::StringKeyedHashAccessor end + + private + + HstorePair = begin + quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ + unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ + /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ + end + + def escape_hstore(value) + if value.nil? + 'NULL' + else + if value == "" + '""' + else + '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') + end + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index ab1165f301..e12ddd9901 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -10,11 +10,19 @@ module ActiveRecord end def type_cast_from_database(value) - ConnectionAdapters::PostgreSQLColumn.string_to_json(value) + if value.is_a?(::String) + ::ActiveSupport::JSON.decode(value) + else + super + end end def type_cast_for_database(value) - ConnectionAdapters::PostgreSQLColumn.json_to_string(value) + if value.is_a?(::Array) || value.is_a?(::Hash) + ::ActiveSupport::JSON.encode(value) + else + super + end end def accessor diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb new file mode 100644 index 0000000000..380c50fc14 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Jsonb < Json # :nodoc: + def type + :jsonb + end + + def changed_in_place?(raw_old_value, new_value) + # Postgres does not preserve insignificant whitespaces when + # roundtripping jsonb columns. This causes some false positives for + # the comparison here. Therefore, we need to parse and re-dump the + # raw value here to ensure the insignificant whitespaces are + # consistent with our encoder's output. + raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value)) + super(raw_old_value, new_value) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index 9b6494867f..bac8b01d6b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -25,11 +25,17 @@ module ActiveRecord def type_cast_for_database(value) if value.is_a?(::Array) - PostgreSQLColumn.point_to_string(value) + "(#{number_for_point(value[0])},#{number_for_point(value[1])})" else super end end + + private + + def number_for_point(number) + number.to_s.gsub(/\.0$/, '') + end end end end 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 991cdd0913..ae967d5167 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -10,28 +10,10 @@ module ActiveRecord @type = type end - def extract_bounds(value) - from, to = value[1..-2].split(',') - { - from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, - to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, - exclude_start: (value[0] == '('), - exclude_end: (value[-1] == ')') - } - end - - def infinity?(value) - value.respond_to?(:infinite?) && value.infinite? - end - def type_cast_for_schema(value) value.inspect.gsub('Infinity', '::Float::INFINITY') end - def type_cast_single(value) - infinity?(value) ? value : @subtype.type_cast_from_database(value) - end - def cast_value(value) return if value == 'empty' return value if value.is_a?(::Range) @@ -53,6 +35,40 @@ This is not reliable and will be removed in the future. end ::Range.new(from, to, extracted[:exclude_end]) end + + def type_cast_for_database(value) + if value.is_a?(::Range) + from = type_cast_single_for_database(value.begin) + to = type_cast_single_for_database(value.end) + "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}" + else + super + end + end + + private + + def type_cast_single(value) + infinity?(value) ? value : @subtype.type_cast_from_database(value) + end + + def type_cast_single_for_database(value) + infinity?(value) ? '' : @subtype.type_cast_for_database(value) + end + + def extract_bounds(value) + from, to = value[1..-2].split(',') + { + from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, + to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, + exclude_start: (value[0] == '('), + exclude_end: (value[-1] == ')') + } + end + + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb index b2a42e9ebb..2d2fede4e8 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 @@ -8,10 +8,6 @@ module ActiveRecord def initialize(type) @type = type end - - def text? - false - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb index 89728b0fe2..dd97393eac 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -3,12 +3,21 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Uuid < Type::Value # :nodoc: + RFC_4122 = %r{\A\{?[a-fA-F0-9]{4}-? + [a-fA-F0-9]{4}-? + [a-fA-F0-9]{4}-? + [1-5][a-fA-F0-9]{3}-? + [8-Bab][a-fA-F0-9]{3}-? + [a-fA-F0-9]{4}-? + [a-fA-F0-9]{4}-? + [a-fA-F0-9]{4}-?\}?\z}x + def type :uuid end def type_cast(value) - value.presence + value.to_s[RFC_4122, 0] end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb new file mode 100644 index 0000000000..334af7c598 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb @@ -0,0 +1,28 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Xml < Type::String # :nodoc: + def type + :xml + end + + def type_cast_for_database(value) + return unless value + Data.new(super) + end + + class Data # :nodoc: + def initialize(value) + @value = value + end + + def to_s + @value + end + end + end + end + 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 4caed77952..cf5c8d288e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -18,95 +18,18 @@ module ActiveRecord def quote(value, column = nil) #:nodoc: return super unless column - sql_type = type_to_sql(column.type, column.limit, column.precision, column.scale) - case value - when Range - if /range$/ =~ sql_type - "'#{PostgreSQLColumn.range_to_string(value)}'::#{sql_type}" - else - super - end - when Array - case sql_type - when 'point' then super(PostgreSQLColumn.point_to_string(value)) - when 'json' then super(PostgreSQLColumn.json_to_string(value)) - else - super(value, array_column(column)) - end - when Hash - case sql_type - when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) - when 'json' then super(PostgreSQLColumn.json_to_string(value), column) - else super - end when Float - if value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" - elsif value.infinite? || value.nan? + if value.infinite? || value.nan? "'#{value.to_s}'" else super end - when Numeric - if sql_type == 'money' || [:string, :text].include?(column.type) - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" - else - super - end - when String - case sql_type - when 'xml' then "xml '#{quote_string(value)}'" - when /^bit/ - case value - when /^[01]*$/ then "B'#{value}'" # Bit-string notation - when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation - end - else - super - end else super end end - def type_cast(value, column, array_member = false) - return super(value, column) unless column - - case value - when Range - if /range$/ =~ column.sql_type - PostgreSQLColumn.range_to_string(value) - else - super(value, column) - end - when NilClass - if column.array && array_member - 'NULL' - elsif column.array - value - else - super(value, column) - end - when Array - case column.sql_type - when 'point' then PostgreSQLColumn.point_to_string(value) - when 'json' then PostgreSQLColumn.json_to_string(value) - else - super(value, array_column(column)) - end - when Hash - case column.sql_type - when 'hstore' then PostgreSQLColumn.hstore_to_string(value, array_member) - when 'json' then PostgreSQLColumn.json_to_string(value) - else super(value, column) - end - else - super(value, column) - end - end - # Quotes strings for use in SQL input. def quote_string(s) #:nodoc: @connection.escape(s) @@ -160,43 +83,35 @@ module ActiveRecord private def _quote(value) - if value.is_a?(Type::Binary::Data) + case value + when Type::Binary::Data "'#{escape_bytea(value.to_s)}'" + when OID::Xml::Data + "xml '#{quote_string(value.to_s)}'" + when OID::Bit::Data + if value.binary? + "B'#{value}'" + elsif value.hex? + "X'#{value}'" + end else super end end def _type_cast(value) - if value.is_a?(Type::Binary::Data) + case value + when Type::Binary::Data # Return a bind param hash with format as binary. # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc # for more information { value: value.to_s, format: 1 } + when OID::Xml::Data, OID::Bit::Data + value.to_s else super end end - - def array_column(column) - if column.array && !column.respond_to?(:cast_type) - Column.new('', nil, OID::Array.new(AdapterProxyType.new(column, self))) - else - column - end - end - - class AdapterProxyType < SimpleDelegator # :nodoc: - def initialize(column, adapter) - @column = column - @adapter = adapter - super(column) - end - - def type_cast_for_database(value) - @adapter.type_cast(value, @column) - 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 0867e5ef54..83554bbf74 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -64,6 +64,10 @@ module ActiveRecord column(name, :json, options) end + def jsonb(name, options = {}) + column(name, :jsonb, options) + end + def citext(name, options = {}) column(name, :citext, options) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index b2aeb3a058..7042817672 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -31,6 +31,14 @@ module ActiveRecord super end end + + def type_for_column(column) + if column.array + @conn.lookup_cast_type("#{column.sql_type}[]") + else + super + end + end end module SchemaStatements @@ -375,8 +383,8 @@ module ActiveRecord end # Renames a table. - # Also renames a table's primary key sequence if the sequence name matches the - # Active Record default. + # Also renames a table's primary key sequence if the sequence name exists and + # matches the Active Record default. # # Example: # rename_table('octopuses', 'octopi') @@ -384,7 +392,7 @@ module ActiveRecord clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq.identifier == "#{table_name}_#{pk}_seq" + if seq && seq.identifier == "#{table_name}_#{pk}_seq" new_seq = "#{new_name}_#{pk}_seq" execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" end @@ -415,8 +423,16 @@ module ActiveRecord def change_column_default(table_name, column_name, default) clear_cache! column = column_for(table_name, column_name) + return unless column - execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote_default_value(default, column)}" if column + alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s" + if default.nil? + # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will + # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". + execute alter_column_query % "DROP DEFAULT" + else + execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}" + end end def change_column_null(table_name, column_name, null, default = nil) @@ -448,6 +464,42 @@ module ActiveRecord execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" end + def foreign_keys(table_name) + fk_info = select_all <<-SQL.strip_heredoc + SELECT t2.relname AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete + FROM pg_constraint c + JOIN pg_class t1 ON c.conrelid = t1.oid + JOIN pg_class t2 ON c.confrelid = t2.oid + JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid + JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid + JOIN pg_namespace t3 ON c.connamespace = t3.oid + WHERE c.contype = 'f' + AND t1.relname = #{quote(table_name)} + AND t3.nspname = ANY (current_schemas(false)) + ORDER BY c.conname + SQL + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + options[:on_delete] = extract_foreign_key_action(row['on_delete']) + options[:on_update] = extract_foreign_key_action(row['on_update']) + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + + def extract_foreign_key_action(specifier) # :nodoc: + case specifier + when 'c'; :cascade + when 'n'; :nullify + when 'r'; :restrict + end + end + def index_name_length 63 end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 6d5151dfe4..eede374678 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -159,6 +159,10 @@ module ActiveRecord true end + def supports_foreign_keys? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end @@ -344,14 +348,13 @@ module ActiveRecord if supports_extensions? res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", 'SCHEMA' - res.column_types['enabled'].type_cast_from_database res.rows.first.first + res.cast_values.first end end def extensions if supports_extensions? - res = exec_query "SELECT extname from pg_extension", "SCHEMA" - res.rows.map { |r| res.column_types['extname'].type_cast_from_database r.first } + exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values else super end @@ -380,6 +383,11 @@ module ActiveRecord PostgreSQL::Table.new(table_name, base) end + def lookup_cast_type(sql_type) # :nodoc: + oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i + super(oid) + end + protected # Returns the version of the connected PostgreSQL server. @@ -443,10 +451,11 @@ module ActiveRecord m.register_type 'point', OID::Point.new m.register_type 'hstore', OID::Hstore.new m.register_type 'json', OID::Json.new + m.register_type 'jsonb', OID::Jsonb.new m.register_type 'cidr', OID::Cidr.new m.register_type 'inet', OID::Inet.new m.register_type 'uuid', OID::Uuid.new - m.register_type 'xml', OID::SpecializedString.new(:xml) + m.register_type 'xml', OID::Xml.new m.register_type 'tsvector', OID::SpecializedString.new(:tsvector) m.register_type 'macaddr', OID::SpecializedString.new(:macaddr) m.register_type 'citext', OID::SpecializedString.new(:citext) @@ -561,7 +570,7 @@ module ActiveRecord end def exec_no_cache(sql, name, binds) - log(sql, name, binds) { @connection.async_exec(sql) } + log(sql, name, binds) { @connection.async_exec(sql, []) } end def exec_cache(sql, name, binds) diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 4d8afcf16a..a10ce330c7 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,4 +1,3 @@ - module ActiveRecord module ConnectionAdapters class SchemaCache @@ -20,6 +19,7 @@ module ActiveRecord # A cached lookup for table existence. def table_exists?(name) + prepare_tables if @tables.empty? return @tables[name] if @tables.key? name @tables[name] = connection.table_exists?(name) @@ -83,6 +83,12 @@ module ActiveRecord def marshal_load(array) @version, @columns, @columns_hash, @primary_keys, @tables = array end + + private + + def prepare_tables + connection.tables.each { |table| @tables[table] = true } + 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 e6163771e8..faf1cdc686 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -14,9 +14,9 @@ module ActiveRecord raise ArgumentError, "No database file specified. Missing argument: database" end - # Allow database path relative to Rails.root, but only if - # the database path is not the special path that tells - # Sqlite to build a database only in memory. + # Allow database path relative to Rails.root, but only if the database + # path is not the special path that tells sqlite to build a database only + # in memory. if ':memory:' != config[:database] config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root) dirname = File.dirname(config[:database]) @@ -50,6 +50,16 @@ module ActiveRecord end end + class SQLite3String < Type::String # :nodoc: + def type_cast_for_database(value) + if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT + value.encode(Encoding::UTF_8) + else + super + end + end + end + # The SQLite3 adapter works SQLite 3.6.16 or newer # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3). # @@ -220,13 +230,23 @@ module ActiveRecord # QUOTING ================================================== def _quote(value) # :nodoc: - if value.is_a?(Type::Binary::Data) + case value + when Type::Binary::Data "x'#{value.hex}'" else super end end + def _type_cast(value) # :nodoc: + case value + when BigDecimal + value.to_f + else + super + end + end + def quote_string(s) #:nodoc: @connection.class.quote(s) end @@ -249,19 +269,6 @@ module ActiveRecord end end - def type_cast(value, column) # :nodoc: - return value.to_f if BigDecimal === value - return super unless String === value - return super unless column && value - - value = super - if column.type == :string && value.encoding == Encoding::ASCII_8BIT - logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger - value = value.encode Encoding::UTF_8 - end - value - end - # DATABASE STATEMENTS ====================================== def explain(arel, binds = []) @@ -378,7 +385,7 @@ module ActiveRecord table_name && tables(nil, table_name).any? end - # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+. + # Returns an array of +Column+ objects for the table specified by +table_name+. def columns(table_name) #:nodoc: table_structure(table_name).map do |field| case field["dflt_value"] @@ -503,6 +510,7 @@ module ActiveRecord def initialize_type_map(m) super m.register_type(/binary/i, SQLite3Binary.new) + register_class_with_limit m, %r(char)i, SQLite3String end def select(sql, name = nil, binds = []) #:nodoc: diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 10b9e27b7d..d22806fbdf 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -107,6 +107,11 @@ module ActiveRecord end module ClassMethods + def allocate + define_attribute_methods + super + end + def initialize_find_by_cache self.find_by_statement_cache = {}.extend(Mutex_m) end @@ -248,12 +253,7 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil, options = {}) - defaults = {} - self.class.raw_column_defaults.each do |k, v| - defaults[k] = v.duplicable? ? v.dup : v - end - - @attributes = self.class.attributes_builder.build_from_database(defaults) + @attributes = self.class.default_attributes.dup init_internals initialize_internals_callback @@ -320,9 +320,8 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: - pk = self.class.primary_key @attributes = @attributes.dup - @attributes[pk] = Attribute.from_database(nil, type_for_attribute(pk)) + @attributes.reset(self.class.primary_key) run_callbacks(:initialize) unless _initialize_callbacks.empty? @@ -520,10 +519,7 @@ module ActiveRecord end def init_internals - pk = self.class.primary_key - unless @attributes.include?(pk) - @attributes[pk] = Attribute.from_database(nil, type_for_attribute(pk)) - end + @attributes.ensure_initialized(self.class.primary_key) @aggregation_cache = {} @association_cache = {} diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 05c4b13016..f0b6afc4b4 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -34,11 +34,11 @@ module ActiveRecord foreign_key = has_many_association.foreign_key.to_s child_class = has_many_association.klass - reflection = child_class._reflections.values.find { |e| :belongs_to == e.macro && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } + reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(counter_association).count + arel_table[counter_name] => object.send(counter_association).count(:all) }, primary_key) connection.update stmt end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 38c6dcf88d..5958373e88 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -69,12 +69,12 @@ module ActiveRecord # # Where conditions on an enum attribute must use the ordinal value of an enum. module Enum - def self.extended(base) + def self.extended(base) # :nodoc: base.class_attribute(:defined_enums) base.defined_enums = {} end - def inherited(base) + def inherited(base) # :nodoc: base.defined_enums = defined_enums.deep_dup super end @@ -145,11 +145,11 @@ module ActiveRecord value = read_attribute(attr_name) if attribute_changed?(attr_name) if mapping[old] == value - changed_attributes.delete(attr_name) + clear_attribute_changes([attr_name]) end else if old != value - changed_attributes[attr_name] = mapping.key old + set_attribute_was(attr_name, mapping.key(old)) end end else diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index e65dab07ba..727a9befc1 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -27,7 +27,7 @@ module ActiveRecord end.join("\n") end.join("\n") - # Overriding inspect to be more human readable, specially in the console. + # Overriding inspect to be more human readable, especially in the console. def str.inspect self end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 4cd5f92207..4306b36ae1 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -2,7 +2,7 @@ require 'erb' require 'yaml' require 'zlib' require 'active_support/dependencies' -require 'active_support/core_ext/securerandom' +require 'active_support/core_ext/digest/uuid' require 'active_record/fixture_set/file' require 'active_record/errors' @@ -551,7 +551,7 @@ module ActiveRecord # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes. def self.identify(label, column_type = :integer) if column_type == :uuid - SecureRandom.uuid_v5(SecureRandom::UUID_OID_NAMESPACE, label.to_s) + Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s) else Zlib.crc32(label.to_s) % MAX_ID end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 08fc91c9df..251d682a02 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,6 +1,37 @@ require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord + # == Single table inheritance + # + # Active Record allows inheritance by storing the name of the class in a column that by + # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). + # This means that an inheritance looking like this: + # + # class Company < ActiveRecord::Base; end + # class Firm < Company; end + # class Client < Company; end + # class PriorityClient < Client; end + # + # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in + # the companies table with type = "Firm". You can then fetch this row again using + # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object. + # + # Be aware that because the type column is an attribute on the record every new + # subclass will instantly be marked as dirty and the type column will be included + # in the list of changed attributes on the record. This is different from non + # STI classes: + # + # Company.new.changed? # => false + # Firm.new.changed? # => true + # Firm.new.changes # => {"type"=>["","Firm"]} + # + # If you don't have a type column defined in your table, single-table inheritance won't + # be triggered. In that case, it'll work just like normal subclasses with no special magic + # for differentiating between them or reloading the right type with find. + # + # Note, all the attributes for all the cases are kept in the same table. Read more: + # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # module Inheritance extend ActiveSupport::Concern @@ -120,14 +151,8 @@ module ActiveRecord candidates << type_name candidates.each do |candidate| - begin - constant = ActiveSupport::Dependencies.constantize(candidate) - return constant if candidate == constant.to_s - # We don't want to swallow NoMethodError < NameError errors - rescue NoMethodError - raise - rescue NameError - end + constant = ActiveSupport::Dependencies.safe_constantize(candidate) + return constant if candidate == constant.to_s end raise NameError.new("uninitialized constant #{candidates.first}", candidates.first) diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index f4ec13a177..52eeb8ae1f 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -53,11 +53,6 @@ module ActiveRecord included do class_attribute :lock_optimistically, instance_writer: false self.lock_optimistically = true - - is_lock_column = ->(name, _) { lock_optimistically && name == locking_column } - decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type| - LockingType.new(type) - end end def locking_enabled? #:nodoc: @@ -167,6 +162,22 @@ module ActiveRecord counters = counters.merge(locking_column => 1) if locking_enabled? super end + + private + + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `lock_optimistically`, or + # `locking_column` would not be picked up. + def inherited(subclass) + subclass.class_eval do + is_lock_column = ->(name, _) { lock_optimistically && name == locking_column } + decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type| + LockingType.new(type) + end + end + super + end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 01c001e692..a6847e28c2 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -366,22 +366,27 @@ module ActiveRecord # This class is used to verify that all migrations have been run before # loading a web page if config.active_record.migration_error is set to :page_load class CheckPending - def initialize(app, connection = Base.connection) + def initialize(app) @app = app - @connection = connection @last_check = 0 end def call(env) - if @connection.supports_migrations? + if connection.supports_migrations? mtime = ActiveRecord::Migrator.last_migration.mtime.to_i if @last_check < mtime - ActiveRecord::Migration.check_pending!(@connection) + ActiveRecord::Migration.check_pending!(connection) @last_check = mtime end end @app.call(env) end + + private + + def connection + ActiveRecord::Base.connection + end end class << self @@ -394,7 +399,7 @@ module ActiveRecord def load_schema_if_pending! if ActiveRecord::Migrator.needs_migration? - ActiveRecord::Tasks::DatabaseTasks.load_schema + ActiveRecord::Tasks::DatabaseTasks.load_schema_current check_pending! end end @@ -645,7 +650,9 @@ module ActiveRecord unless @connection.respond_to? :revert unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) - arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table + if [:rename_table, :add_foreign_key].include?(method) + arguments[1] = proper_table_name(arguments.second, table_name_options) + end end end return super unless connection.respond_to?(method) @@ -807,22 +814,22 @@ module ActiveRecord migrations = migrations(migrations_paths) migrations.select! { |m| yield m } if block_given? - self.new(:up, migrations, target_version).migrate + new(:up, migrations, target_version).migrate end def down(migrations_paths, target_version = nil, &block) migrations = migrations(migrations_paths) migrations.select! { |m| yield m } if block_given? - self.new(:down, migrations, target_version).migrate + new(:down, migrations, target_version).migrate end def run(direction, migrations_paths, target_version) - self.new(direction, migrations(migrations_paths), target_version).run + new(direction, migrations(migrations_paths), target_version).run end def open(migrations_paths) - self.new(:up, migrations(migrations_paths), nil) + new(:up, migrations(migrations_paths), nil) end def schema_migrations_table_name @@ -885,7 +892,7 @@ module ActiveRecord private def move(direction, migrations_paths, steps) - migrator = self.new(direction, migrations(migrations_paths)) + migrator = new(direction, migrations(migrations_paths)) start_index = migrator.migrations.index(migrator.current_migration) if start_index diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index c44d8c1665..36256415df 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -74,7 +74,9 @@ module ActiveRecord :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column_default, :add_reference, :remove_reference, :transaction, :drop_join_table, :drop_table, :execute_block, :enable_extension, - :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too + :change_column, :execute, :remove_columns, :change_column_null, + :add_foreign_key, :remove_foreign_key + # irreversible methods need to be here too ].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) @@ -85,7 +87,7 @@ module ActiveRecord alias :add_belongs_to :add_reference alias :remove_belongs_to :remove_reference - def change_table(table_name, options = {}) + def change_table(table_name, options = {}) # :nodoc: yield delegate.update_table_definition(table_name, self) end @@ -167,6 +169,21 @@ module ActiveRecord [:change_column_null, args] end + def invert_add_foreign_key(args) + from_table, to_table, add_options = args + add_options ||= {} + + if add_options[:name] + options = { name: add_options[:name] } + elsif add_options[:column] + options = { column: add_options[:column] } + else + options = to_table + end + + [:remove_foreign_key, [from_table, options]] + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) if @delegate.respond_to?(method) diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb index ebf64cbcdc..05569fadbd 100644 --- a/activerecord/lib/active_record/migration/join_table.rb +++ b/activerecord/lib/active_record/migration/join_table.rb @@ -8,7 +8,7 @@ module ActiveRecord end def join_table_name(table_1, table_2) - [table_1.to_s, table_2.to_s].sort.join("_").to_sym + ModelSchema.derive_join_table_name(table_1, table_2).to_sym end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 099042cab2..850220babd 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -55,6 +55,17 @@ module ActiveRecord delegate :type_for_attribute, to: :class end + # Derives the join table name for +first_table+ and +second_table+. The + # table names appear in alphabetical order. A common prefix is removed + # (useful for namespaced models like Music::Artist and Music::Record): + # + # artists, records => artists_records + # records, artists => artists_records + # music_artists, music_records => music_artists_records + def self.derive_join_table_name(first_table, second_table) # :nodoc: + [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + end + module ClassMethods # Guesses the table name (in forced lower-case) based on the name of the class in the # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy @@ -224,8 +235,8 @@ module ActiveRecord end def column_types # :nodoc: - @column_types ||= Hash.new(Type::Value.new).tap do |column_types| - columns.each { |column| column_types[column.name] = column.cast_type } + @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h| + h.default = Type::Value.new end end @@ -236,17 +247,12 @@ module ActiveRecord # Returns a hash where the keys are column names and the values are # default values when instantiating the AR object for this table. def column_defaults - @column_defaults ||= Hash[raw_column_defaults.map { |name, default| - [name, type_for_attribute(name).type_cast_from_database(default)] - }] + default_attributes.to_hash end - # Returns a hash where the keys are the column names and the values - # are the default values suitable for use in `@raw_attriubtes` - def raw_column_defaults # :nodoc: - @raw_column_defaults ||= Hash[columns_hash.map { |name, column| - [name, column.default] - }] + def default_attributes # :nodoc: + @default_attributes ||= attributes_builder.build_from_database( + columns_hash.transform_values(&:default)) end # Returns an array of column names as strings. @@ -292,11 +298,10 @@ module ActiveRecord connection.schema_cache.clear_table_cache!(table_name) if table_exists? @arel_engine = nil - @column_defaults = nil - @raw_column_defaults = nil @column_names = nil @column_types = nil @content_columns = nil + @default_attributes = nil @dynamic_methods_hash = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @relation = nil diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index ee634d7bb3..755ff2b2f1 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -36,6 +36,23 @@ module ActiveRecord end end + # Creates an object (or multiple objects) and saves it to the database, + # if validations pass. Raises a RecordInvalid error if validations fail, + # unlike Base#create. + # + # The +attributes+ parameter can be either a Hash or an Array of Hashes. + # These describe which attributes to be created on the object, or + # multiple objects when given an Array of Hashes. + def create!(attributes = nil, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create!(attr, &block) } + else + object = new(attributes, &block) + object.save! + object + end + end + # Given an attributes hash, +instantiate+ returns a new instance of # the appropriate class. Accepts only keys as strings. # @@ -270,7 +287,8 @@ module ActiveRecord # This method raises an +ActiveRecord::ActiveRecordError+ when called on new # objects, or when at least one of the attributes is marked as readonly. def update_columns(attributes) - raise ActiveRecordError, "cannot update on a new record object" unless persisted? + raise ActiveRecordError, "cannot update a new record" if new_record? + raise ActiveRecordError, "cannot update a destroyed record" if destroyed? attributes.each_key do |key| verify_readonly_attribute(key.to_s) @@ -396,6 +414,7 @@ module ActiveRecord end @attributes = fresh_object.instance_variable_get('@attributes') + @new_record = false self end @@ -447,7 +466,7 @@ module ActiveRecord changes[self.class.locking_column] = increment_lock if locking_enabled? - changed_attributes.except!(*changes.keys) + clear_attribute_changes(changes.keys) primary_key = self.class.primary_key self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 else diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index df8654e5c1..dcb2bd3d84 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Query Cache class QueryCache @@ -29,9 +28,10 @@ module ActiveRecord end def call(env) - enabled = ActiveRecord::Base.connection.query_cache_enabled + connection = ActiveRecord::Base.connection + enabled = connection.query_cache_enabled connection_id = ActiveRecord::Base.connection_id - ActiveRecord::Base.connection.enable_query_cache! + connection.enable_query_cache! response = @app.call(env) response[2] = Rack::BodyProxy.new(response[2]) do diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index ecf5afc4f4..458862a538 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -41,10 +41,7 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task :migrate => [:environment, :load_config] do - ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true - ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration| - ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope) - end + ActiveRecord::Tasks::DatabaseTasks.migrate db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration end @@ -93,22 +90,21 @@ db_namespace = namespace :db do desc 'Display status of migrations' task :status => [:environment, :load_config] do - unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) + unless ActiveRecord::SchemaMigration.table_exists? abort 'Schema migrations table does not exist yet.' end - db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}") - db_list.map! { |version| ActiveRecord::SchemaMigration.normalize_migration_number(version) } - file_list = [] - ActiveRecord::Migrator.migrations_paths.each do |path| - Dir.foreach(path) do |file| - # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern - if match_data = /^(\d{3,})_(.+)\.rb$/.match(file) - version = ActiveRecord::SchemaMigration.normalize_migration_number(match_data[1]) - status = db_list.delete(version) ? 'up' : 'down' - file_list << [status, version, match_data[2].humanize] + db_list = ActiveRecord::SchemaMigration.normalized_versions + + file_list = + ActiveRecord::Migrator.migrations_paths.flat_map do |path| + # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern + Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do + version = ActiveRecord::SchemaMigration.normalize_migration_number($1) + status = db_list.delete(version) ? 'up' : 'down' + [status, version, $2.humanize] + end end - end - end + db_list.map! do |version| ['up', version, '********** NO FILE **********'] end @@ -116,8 +112,8 @@ db_namespace = namespace :db do puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 - (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration| - puts "#{migration[0].center(8)} #{migration[1].ljust(14)} #{migration[2]}" + (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name| + puts "#{status.center(8)} #{version.ljust(14)} #{name}" end puts end @@ -189,17 +185,21 @@ db_namespace = namespace :db do task :load => [:environment, :load_config] do require 'active_record/fixtures' - base_dir = if ENV['FIXTURES_PATH'] - File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten - else - ActiveRecord::Tasks::DatabaseTasks.fixtures_path - end + base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path - fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact + fixtures_dir = if ENV['FIXTURES_DIR'] + File.join base_dir, ENV['FIXTURES_DIR'] + else + base_dir + end - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(',') : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| - ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file) - end + fixture_files = if ENV['FIXTURES'] + ENV['FIXTURES'].split(',') + else + Pathname.glob("#{fixtures_dir}/**/*.yml").map {|f| f.basename.sub_ext('').to_s } + end + + ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files) end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." @@ -211,12 +211,7 @@ db_namespace = namespace :db do puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label - base_dir = if ENV['FIXTURES_PATH'] - File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten - else - ActiveRecord::Tasks::DatabaseTasks.fixtures_path - end - + base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path Dir["#{base_dir}/**/*.yml"].each do |file| if data = YAML::load(ERB.new(IO.read(file)).result) @@ -245,7 +240,7 @@ db_namespace = namespace :db do desc 'Load a schema.rb file into the database' task :load => [:environment, :load_config] do - ActiveRecord::Tasks::DatabaseTasks.load_schema(:ruby, ENV['SCHEMA']) + ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA']) end task :load_if_ruby => ['db:create', :environment] do @@ -291,7 +286,7 @@ db_namespace = namespace :db do desc "Recreate the databases from the structure.sql file" task :load => [:environment, :load_config] do - ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE']) + ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['DB_STRUCTURE']) end task :load_if_sql => ['db:create', :environment] do @@ -322,9 +317,8 @@ db_namespace = namespace :db do task :load_schema => %w(db:test:deprecated db:test:purge) do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) ActiveRecord::Schema.verbose = false - db_namespace["schema:load"].invoke + ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA'] ensure if should_reconnect ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) @@ -334,12 +328,7 @@ db_namespace = namespace :db do # desc "Recreate the test database from an existent structure.sql file" task :load_structure => %w(db:test:deprecated db:test:purge) do - begin - ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test']) - db_namespace["structure:load"].invoke - ensure - ActiveRecord::Tasks::DatabaseTasks.current_config(:config => nil) - end + ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA'] end # desc "Recreate the test database from a fresh schema" diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index b3ddfd63d4..85bbac43e4 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -1,4 +1,3 @@ - module ActiveRecord module ReadonlyAttributes extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 28c39bdd5c..1547c8e3f4 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -13,14 +13,21 @@ module ActiveRecord end def self.create(macro, name, scope, options, ar) - case macro - when :has_many, :belongs_to, :has_one - klass = options[:through] ? ThroughReflection : AssociationReflection - when :composed_of - klass = AggregateReflection - end - - klass.new(macro, name, scope, options, ar) + klass = case macro + when :composed_of + AggregateReflection + when :has_many + HasManyReflection + when :has_one + HasOneReflection + when :belongs_to + BelongsToReflection + else + raise "Unsupported Macro: #{macro}" + end + + reflection = klass.new(name, scope, options, ar) + options[:through] ? ThroughReflection.new(reflection) : reflection end def self.add_reflection(ar, name, reflection) @@ -31,7 +38,7 @@ module ActiveRecord ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection) end - # \Reflection enables to interrogate Active Record classes and objects + # \Reflection enables interrogating Active Record classes and objects # about their associations and aggregations. This information can, # for example, be used in a form builder that takes an Active Record object # and creates input fields for all of the attributes depending on their type @@ -110,26 +117,64 @@ module ActiveRecord end end + # Holds all the methods that are shared between MacroReflection, AssociationReflection + # and ThroughReflection + class AbstractReflection # :nodoc: + def table_name + klass.table_name + end + + # Returns a new, unsaved instance of the associated class. +attributes+ will + # be passed to the class's constructor. + def build_association(attributes, &block) + klass.new(attributes, &block) + end + + def quoted_table_name + klass.quoted_table_name + end + + def primary_key_type + klass.type_for_attribute(klass.primary_key) + end + + # Returns the class name for the macro. + # + # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> + # <tt>has_many :clients</tt> returns <tt>'Client'</tt> + def class_name + @class_name ||= (options[:class_name] || derive_class_name).to_s + end + + JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: + + def join_keys(assoc_klass) + JoinKeys.new(foreign_key, active_record_primary_key) + end + + def source_macro + ActiveSupport::Deprecation.warn("ActiveRecord::Base.source_macro is deprecated and " \ + "will be removed without replacement.") + macro + end + end # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. # # MacroReflection - # AggregateReflection # AssociationReflection - # ThroughReflection - class MacroReflection + # AggregateReflection + # HasManyReflection + # HasOneReflection + # BelongsToReflection + # ThroughReflection + class MacroReflection < AbstractReflection # Returns the name of the macro. # # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt> # <tt>has_many :clients</tt> returns <tt>:clients</tt> attr_reader :name - # Returns the macro type. - # - # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt> - # <tt>has_many :clients</tt> returns <tt>:has_many</tt> - attr_reader :macro - attr_reader :scope # Returns the hash of options used for the macro. @@ -142,8 +187,7 @@ module ActiveRecord attr_reader :plural_name # :nodoc: - def initialize(macro, name, scope, options, active_record) - @macro = macro + def initialize(name, scope, options, active_record) @name = name @scope = scope @options = options @@ -167,15 +211,11 @@ module ActiveRecord # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class # <tt>has_many :clients</tt> returns the Client class def klass - @klass ||= class_name.constantize + @klass ||= compute_class(class_name) end - # Returns the class name for the macro. - # - # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> - # <tt>has_many :clients</tt> returns <tt>'Client'</tt> - def class_name - @class_name ||= (options[:class_name] || derive_class_name).to_s + def compute_class(name) + name.constantize end # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute, @@ -188,23 +228,6 @@ module ActiveRecord active_record == other_aggregation.active_record end - JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: - - def join_keys(assoc_klass) - if source_macro == :belongs_to - if polymorphic? - reflection_key = association_primary_key(assoc_klass) - else - reflection_key = association_primary_key - end - reflection_foreign_key = foreign_key - else - reflection_key = foreign_key - reflection_foreign_key = active_record_primary_key - end - JoinKeys.new(reflection_key, reflection_foreign_key) - end - private def derive_class_name name.to_s.camelize @@ -237,15 +260,18 @@ module ActiveRecord # a new association object. Use +build_association+ or +create_association+ # instead. This allows plugins to hook into association object creation. def klass - @klass ||= active_record.send(:compute_type, class_name) + @klass ||= compute_class(class_name) + end + + def compute_class(name) + active_record.send(:compute_type, name) end attr_reader :type, :foreign_type attr_accessor :parent_reflection # [:name, Reflection] - def initialize(macro, name, scope, options, active_record) + def initialize(name, scope, options, active_record) super - @collection = macro == :has_many @automatic_inverse_of = nil @type = options[:as] && "#{options[:as]}_type" @foreign_type = options[:foreign_type] || "#{name}_type" @@ -264,24 +290,10 @@ module ActiveRecord } end - # Returns a new, unsaved instance of the associated class. +attributes+ will - # be passed to the class's constructor. - def build_association(attributes, &block) - klass.new(attributes, &block) - end - def constructable? # :nodoc: @constructable end - def table_name - klass.table_name - end - - def quoted_table_name - klass.quoted_table_name - end - def join_table @join_table ||= options[:join_table] || derive_join_table end @@ -290,10 +302,6 @@ module ActiveRecord @foreign_key ||= options[:foreign_key] || derive_foreign_key end - def primary_key_type - klass.type_for_attribute(klass.primary_key) - end - def association_foreign_key @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key end @@ -341,9 +349,8 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end alias :check_eager_loadable! :check_preloadable! - def join_id_for(owner) #:nodoc: - key = (source_macro == :belongs_to) ? foreign_key : active_record_primary_key - owner[key] + def join_id_for(owner) # :nodoc: + owner[active_record_primary_key] end def through_reflection @@ -370,8 +377,6 @@ Joining, Preloading and eager loading of these associations is deprecated and wi scope ? [[scope]] : [[]] end - alias :source_macro :macro - def has_inverse? inverse_name end @@ -392,11 +397,16 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end end + # Returns the macro type. + # + # <tt>has_many :clients</tt> returns <tt>:has_many</tt> + def macro; raise NotImplementedError; end + # Returns whether or not this association reflection is for a collection # association. Returns +true+ if the +macro+ is either +has_many+ or # +has_and_belongs_to_many+, +false+ otherwise. def collection? - @collection + false end # Returns whether or not the association should be validated as part of @@ -413,14 +423,10 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end # Returns +true+ if +self+ is a +belongs_to+ reflection. - def belongs_to? - macro == :belongs_to - end + def belongs_to?; false; end # Returns +true+ if +self+ is a +has_one+ reflection. - def has_one? - macro == :has_one - end + def has_one?; false; end def association_class case macro @@ -552,7 +558,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end def derive_join_table - [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + ModelSchema.derive_join_table_name active_record.table_name, klass.table_name end def primary_key(klass) @@ -560,22 +566,72 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end end - class HasAndBelongsToManyReflection < AssociationReflection #:nodoc: - def initialize(macro, name, scope, options, active_record) + class HasManyReflection < AssociationReflection # :nodoc: + def initialize(name, scope, options, active_record) + super(name, scope, options, active_record) + end + + def macro; :has_many; end + + def collection?; true; end + end + + class HasOneReflection < AssociationReflection # :nodoc: + def initialize(name, scope, options, active_record) + super(name, scope, options, active_record) + end + + def macro; :has_one; end + + def has_one?; true; end + end + + class BelongsToReflection < AssociationReflection # :nodoc: + def initialize(name, scope, options, active_record) + super(name, scope, options, active_record) + end + + def macro; :belongs_to; end + + def belongs_to?; true; end + + def join_keys(assoc_klass) + key = polymorphic? ? association_primary_key(assoc_klass) : association_primary_key + JoinKeys.new(key, foreign_key) + end + + def join_id_for(owner) # :nodoc: + owner[foreign_key] + end + end + + class HasAndBelongsToManyReflection < AssociationReflection # :nodoc: + def initialize(name, scope, options, active_record) super - @collection = true + end + + def macro; :has_and_belongs_to_many; end + + def collection? + true end end # Holds all the meta-data about a :through association as it was specified # in the Active Record class. - class ThroughReflection < AssociationReflection #:nodoc: + class ThroughReflection < AbstractReflection #:nodoc: + attr_reader :delegate_reflection delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :type, :to => :source_reflection - def initialize(macro, name, scope, options, active_record) - super - @source_reflection_name = options[:source] + def initialize(delegate_reflection) + @delegate_reflection = delegate_reflection + @klass = delegate_reflection.options[:class] + @source_reflection_name = delegate_reflection.options[:source] + end + + def klass + @klass ||= delegate_reflection.compute_class(class_name) end # Returns the source of the through reflection. It checks both a singularized @@ -593,7 +649,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.source_reflection - # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags"> + # # => <ActiveRecord::Reflection::BelongsToReflection: @name=:tag, @active_record=Tagging, @plural_name="tags"> # def source_reflection through_reflection.klass._reflect_on_association(source_reflection_name) @@ -609,7 +665,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.through_reflection - # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings"> + # # => <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @active_record=Post, @plural_name="taggings"> # def through_reflection active_record._reflect_on_association(options[:through]) @@ -629,8 +685,8 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.chain - # # => [<ActiveRecord::Reflection::ThroughReflection: @macro=:has_many, @name=:tags, @options={:through=>:taggings}, @active_record=Post>, - # <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @options={}, @active_record=Post>] + # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>, + # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>] # def chain @chain ||= begin @@ -680,8 +736,14 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end end + def join_keys(assoc_klass) + source_reflection.join_keys(assoc_klass) + end + # The macro used by the source association def source_macro + ActiveSupport::Deprecation.warn("ActiveRecord::Base.source_macro is deprecated and " \ + "will be removed without replacement.") source_reflection.source_macro end @@ -747,6 +809,10 @@ directive on your declaration like: through_reflection.options end + def join_id_for(owner) # :nodoc: + source_reflection.join_id_for(owner) + end + def check_validity! if through_reflection.nil? raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) @@ -777,15 +843,25 @@ directive on your declaration like: protected - def actual_source_reflection # FIXME: this is a horrible name - source_reflection.actual_source_reflection - end + def actual_source_reflection # FIXME: this is a horrible name + source_reflection.send(:actual_source_reflection) + end + + def primary_key(klass) + klass.primary_key || raise(UnknownPrimaryKey.new(klass)) + 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 end + + delegate_methods = AssociationReflection.public_instance_methods - + public_instance_methods + + delegate(*delegate_methods, to: :delegate_reflection) + end end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index cef40be7ce..ad54d84665 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -242,6 +242,11 @@ module ActiveRecord @records end + # Serializes the relation objects Array. + def encode_with(coder) + coder.represent_seq(nil, to_a) + end + def as_json(options = nil) #:nodoc: to_a.as_json(options) end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 29fc150b3d..b069cdce7c 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,4 +1,3 @@ - module ActiveRecord module Batches # Looping through a collection of records from the database diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index c80ffbae92..90e99957f6 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -34,7 +34,7 @@ module ActiveRecord # # => counts the number of different age values # # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ - # between databases. In invalid cases, an error from the databsae is thrown. + # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil, options = {}) # TODO: Remove options argument as soon we remove support to # activerecord-deprecated_finders. @@ -178,16 +178,7 @@ module ActiveRecord columns_hash.key?(cn) ? arel_table[cn] : cn } result = klass.connection.select_all(relation.arel, nil, bind_values) - columns = result.columns.map do |key| - klass.column_types.fetch(key) { - result.column_types.fetch(key) { result.identity_type } - } - end - - result = result.rows.map do |values| - columns.zip(values).map { |column, value| column.type_cast_from_database value } - end - columns.one? ? result.map!(&:first) : result + result.cast_values(klass.column_types) end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 1262b2c291..c8f382ad78 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -952,9 +952,7 @@ WARNING self.bind_values += bind_values attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts) - attributes.values.grep(ActiveRecord::Relation) do |rel| - self.bind_values += rel.bind_values - end + add_relations_to_bind_values(attributes) PredicateBuilder.build_from_hash(klass, attributes, table) else @@ -1137,5 +1135,19 @@ WARNING raise ArgumentError, "The method .#{method_name}() must contain arguments." end end + + # This function is recursive just for better readablity. + # #where argument doesn't support more than one level nested hash in real world. + def add_relations_to_bind_values(attributes) + if attributes.is_a?(Hash) + attributes.each_value do |value| + if value.is_a?(ActiveRecord::Relation) + self.bind_values += value.bind_values + else + add_relations_to_bind_values(value) + end + end + end + end end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 8c29c69a9b..8405fdaeb9 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -42,14 +42,6 @@ module ActiveRecord @column_types = column_types end - def identity_type # :nodoc: - IDENTITY_TYPE - end - - def column_type(name) - @column_types[name] || identity_type - end - def each if block_given? hash_rows.each { |row| yield row } @@ -82,6 +74,15 @@ module ActiveRecord hash_rows.last end + def cast_values(type_overrides = {}) # :nodoc: + types = columns.map { |name| column_type(name, type_overrides) } + result = rows.map do |values| + types.zip(values).map { |type, value| type.type_cast_from_database(value) } + end + + columns.one? ? result.map!(&:first) : result + end + def initialize_copy(other) @columns = columns.dup @rows = rows.dup @@ -91,6 +92,12 @@ module ActiveRecord private + def column_type(name, type_overrides = {}) + type_overrides.fetch(name) do + column_types.fetch(name, IDENTITY_TYPE) + end + end + def hash_rows @hash_rows ||= begin diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 4bfd0167a4..0a5546a760 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Schema # diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index e055d571ab..fae6427ea1 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -91,16 +91,17 @@ HEADER end def tables(stream) - @connection.tables.sort.each do |tbl| - next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| - case ignored - when String; remove_prefix_and_suffix(tbl) == ignored - when Regexp; remove_prefix_and_suffix(tbl) =~ ignored - else - raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' - end + sorted_tables = @connection.tables.sort + + sorted_tables.each do |table_name| + table(table_name, stream) unless ignored?(table_name) + end + + # dump foreign keys at the end to make sure all dependent tables exist. + if @connection.supports_foreign_keys? + sorted_tables.each do |tbl| + foreign_keys(tbl, stream) end - table(tbl, stream) end end @@ -112,7 +113,8 @@ HEADER # first dump primary key column if @connection.respond_to?(:pk_and_sequence_for) pk, _ = @connection.pk_and_sequence_for(table) - elsif @connection.respond_to?(:primary_key) + end + if !pk && @connection.respond_to?(:primary_key) pk = @connection.primary_key(table) end @@ -212,8 +214,49 @@ HEADER end end + def foreign_keys(table, stream) + if (foreign_keys = @connection.foreign_keys(table)).any? + add_foreign_key_statements = foreign_keys.map do |foreign_key| + parts = [ + 'add_foreign_key ' + remove_prefix_and_suffix(foreign_key.from_table).inspect, + remove_prefix_and_suffix(foreign_key.to_table).inspect, + ] + + if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table) + parts << ('column: ' + foreign_key.column.inspect) + end + + if foreign_key.custom_primary_key? + parts << ('primary_key: ' + foreign_key.primary_key.inspect) + end + + if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/ + parts << ('name: ' + foreign_key.name.inspect) + end + + parts << ('on_update: ' + foreign_key.on_update.inspect) if foreign_key.on_update + parts << ('on_delete: ' + foreign_key.on_delete.inspect) if foreign_key.on_delete + + ' ' + parts.join(', ') + end + + stream.puts add_foreign_key_statements.sort.join("\n") + end + end + def remove_prefix_and_suffix(table) table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2") end + + def ignored?(table_name) + ['schema_migrations', ignore_tables].flatten.any? do |ignored| + case ignored + when String; remove_prefix_and_suffix(table_name) == ignored + when Regexp; remove_prefix_and_suffix(table_name) =~ ignored + else + raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' + end + end + end end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 3a004d58c9..b5038104ac 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -43,6 +43,10 @@ module ActiveRecord def normalize_migration_number(number) "%.3d" % number.to_i end + + def normalized_versions + pluck(:version).map { |v| normalize_migration_number v } + end end def version diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 7712a14b79..892c78e479 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -58,7 +58,11 @@ module ActiveRecord end def fixtures_path - @fixtures_path ||= File.join(root, 'test', 'fixtures') + @fixtures_path ||= if ENV['FIXTURES_PATH'] + File.join(root, ENV['FIXTURES_PATH']) + else + File.join(root, 'test', 'fixtures') + end end def root @@ -106,6 +110,8 @@ module ActiveRecord def drop(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).drop + rescue ActiveRecord::NoDatabaseError + $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error $stderr.puts error, *(error.backtrace) $stderr.puts "Couldn't drop #{configuration['database']}" @@ -121,6 +127,16 @@ module ActiveRecord } end + def migrate + verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true + version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil + scope = ENV['SCOPE'] + Migration.verbose = verbose + Migrator.migrate(Migrator.migrations_paths, version) do |migration| + scope.blank? || scope == migration.scope + end + end + def charset_current(environment = env) charset ActiveRecord::Base.configurations[environment] end @@ -168,22 +184,39 @@ module ActiveRecord end def load_schema(format = ActiveRecord::Base.schema_format, file = nil) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + This method will act on a specific connection in the future. + To act on the current connection, use `load_schema_current` instead. + MESSAGE + load_schema_current(format, file) + end + + # This method is the successor of +load_schema+. We should rename it + # after +load_schema+ went through a deprecation cycle. (Rails > 4.2) + def load_schema_for(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc: case format when :ruby file ||= File.join(db_dir, "schema.rb") check_schema_file(file) - purge(current_config) + purge(configuration) + ActiveRecord::Base.establish_connection(configuration) load(file) when :sql file ||= File.join(db_dir, "structure.sql") check_schema_file(file) - purge(current_config) - structure_load(current_config, file) + purge(configuration) + structure_load(configuration, file) else raise ArgumentError, "unknown format #{format.inspect}" end end + def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) + each_current_configuration(environment) { |configuration| + load_schema_for configuration, format, file + } + end + def check_schema_file(filename) unless File.exist?(filename) message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.} diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 644c4852b9..d890196f47 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -42,7 +42,7 @@ module ActiveRecord end def purge - establish_connection :test + establish_connection configuration connection.recreate_database configuration['database'], creation_options end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 3d02ee07d0..ce1de4b76e 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -54,7 +54,7 @@ module ActiveRecord command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" raise 'Error dumping database' unless Kernel.system(command) - File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" } + File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" } end def structure_load(filename) diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index e2e37e7c00..936a18d99a 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Timestamp # @@ -48,8 +47,9 @@ module ActiveRecord current_time = current_time_from_proper_timezone all_timestamp_attributes.each do |column| - if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil? - write_attribute(column.to_s, current_time) + column = column.to_s + if has_attribute?(column) && !attribute_present?(column) + write_attribute(column, current_time) end end end @@ -114,7 +114,7 @@ module ActiveRecord def clear_timestamp_attributes all_timestamp_attributes_in_model.each do |attribute_name| self[attribute_name] = nil - changed_attributes.delete(attribute_name) + clear_attribute_changes([attribute_name]) end end end diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb index 7416c554c7..d29ff4e494 100644 --- a/activerecord/lib/active_record/type/binary.rb +++ b/activerecord/lib/active_record/type/binary.rb @@ -24,7 +24,7 @@ module ActiveRecord class Data # :nodoc: def initialize(value) - @value = value + @value = value.to_s end def to_s diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb index a9db51c6ba..d10778eeb6 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activerecord/lib/active_record/type/decimal.rb @@ -14,10 +14,25 @@ module ActiveRecord private def cast_value(value) - if value.respond_to?(:to_d) - value.to_d + case value + when ::Float + BigDecimal(value, float_precision) + when ::Numeric, ::String + BigDecimal(value, precision.to_i) else - value.to_s.to_d + if value.respond_to?(:to_d) + value.to_d + else + cast_value(value.to_s) + end + end + end + + def float_precision + if precision.to_i > ::Float::DIG + 1 + ::Float::DIG + 1 + else + precision.to_i end end end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 42bbed7103..abeea769c4 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -12,7 +12,7 @@ module ActiveRecord end def type_cast_from_database(value) - if is_default_value?(value) + if default_value?(value) value else coder.load(super) @@ -21,7 +21,7 @@ module ActiveRecord def type_cast_for_database(value) return if value.nil? - unless is_default_value?(value) + unless default_value?(value) super coder.dump(value) end end @@ -43,7 +43,7 @@ module ActiveRecord private - def is_default_value?(value) + def default_value?(value) value == coder.load(nil) end end diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb index 3b1554bd5a..150defb106 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activerecord/lib/active_record/type/string.rb @@ -5,8 +5,20 @@ module ActiveRecord :string end - def text? - true + def changed_in_place?(raw_old_value, new_value) + if new_value.is_a?(::String) + raw_old_value != new_value + end + end + + def type_cast_for_database(value) + case value + when ::Numeric, ActiveSupport::Duration then value.to_s + when ::String then ::String.new(value) + when true then "1" + when false then "0" + else super + end end private @@ -15,7 +27,8 @@ module ActiveRecord case value when true then "1" when false then "0" - else value.to_s + # String.new is slightly faster than dup + else ::String.new(value.to_s) end end end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 081da7547e..9456a4a56c 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -50,10 +50,6 @@ module ActiveRecord # These predicates are not documented, as I need to look further into # their use, and see if they can be removed entirely. - def text? # :nodoc: - false - end - def number? # :nodoc: false end @@ -73,13 +69,20 @@ module ActiveRecord end # Determines whether the mutable value has been modified since it was - # read. Returns +false+ by default. This method should not need to be - # overriden directly. Types which return a mutable value should include + # read. Returns +false+ by default. This method should not be overridden + # directly. Types which return a mutable value should include # +Type::Mutable+, which will define this method. def changed_in_place?(*) false end + def ==(other) + self.class == other.class && + precision == other.precision && + scale == other.scale && + limit == other.limit + end + private def type_cast(value) diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 9999624fcf..7f7d49cdb4 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -29,21 +29,6 @@ module ActiveRecord extend ActiveSupport::Concern include ActiveModel::Validations - module ClassMethods - # Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+ - # so an exception is raised if the record is invalid. - def create!(attributes = nil, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| create!(attr, &block) } - else - object = new(attributes) - yield(object) if block_given? - object.save! - object - end - end - end - # The validation process on save can be skipped by passing <tt>validate: false</tt>. # The regular Base#save method is replaced with this when the validations # module is mixed in, which it is by default. @@ -54,7 +39,7 @@ module ActiveRecord # Attempts to save the record just like Base#save but will raise a +RecordInvalid+ # exception instead of returning +false+ if the record is not valid. def save!(options={}) - perform_validations(options) ? super : raise(RecordInvalid.new(self)) + perform_validations(options) ? super : raise_record_invalid end # Runs all the validations within the specified context. Returns +true+ if @@ -75,8 +60,24 @@ module ActiveRecord alias_method :validate, :valid? + # Runs all the validations within the specified context. Returns +true+ if + # no errors are found, raises +RecordInvalid+ otherwise. + # + # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if + # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not. + # + # Validations with no <tt>:on</tt> option will run no matter the context. Validations with + # some <tt>:on</tt> option will only run in the specified context. + def validate!(context = nil) + valid?(context) || raise_record_invalid + end + protected + def raise_record_invalid + raise(RecordInvalid.new(self)) + end + def perform_validations(options={}) # :nodoc: options[:validate] == false || valid?(options[:context]) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 04e28a0cfe..2dba4c7b94 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -61,9 +61,11 @@ module ActiveRecord column = klass.columns_hash[attribute_name] value = klass.connection.type_cast(value, column) - value = value.to_s[0, column.limit] if value && column.limit && column.text? + if value.is_a?(String) && column.limit + value = value.to_s[0, column.limit] + end - if !options[:case_sensitive] && value && column.text? + if !options[:case_sensitive] && value.is_a?(String) # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else @@ -150,7 +152,7 @@ module ActiveRecord # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, # proc or string should return or evaluate to a +true+ or +false+ value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to - # determine if the validation should ot occur (e.g. <tt>unless: :skip_validation</tt>, + # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>, # 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. 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 d3c853cfea..7a3c6f5e95 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -55,7 +55,7 @@ module ActiveRecord def attributes_with_index attributes.select { |a| !a.reference? && a.has_index? } end - + def validate_file_name! unless file_name =~ /^[_a-z0-9]+$/ raise IllegalMigrationNameError.new(file_name) diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb index fd94a2d038..f7bf6987c4 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb @@ -9,7 +9,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration <% end -%> <% end -%> <% if options[:timestamps] %> - t.timestamps + t.timestamps null: false <% end -%> end <% attributes_with_index.each do |attribute| -%> diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb index 808598699b..539d969fce 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb @@ -1,7 +1,7 @@ <% module_namespacing do -%> class <%= class_name %> < <%= parent_class_name.classify %> <% attributes.select(&:reference?).each do |attribute| -%> - belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %> + belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %> <% end -%> <% if attributes.any?(&:password_digest?) -%> has_secure_password diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 778c4ed7e5..6f84bae432 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -192,7 +192,7 @@ module ActiveRecord def test_select_methods_passing_a_association_relation author = Author.create!(name: 'john') Post.create!(author: author, title: 'foo', body: 'bar') - query = author.posts.select(:title) + query = author.posts.where(title: 'foo').select(:title) assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values)) assert_equal({"title" => "foo"}, @connection.select_one(query)) assert @connection.select_all(query).is_a?(ActiveRecord::Result) diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 7c0f11b033..a84673e452 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -105,7 +105,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase with_real_execute do begin ActiveRecord::Base.connection.create_table :delete_me do |t| - t.timestamps + t.timestamps null: true end ActiveRecord::Base.connection.remove_timestamps :delete_me assert !column_present?('delete_me', 'updated_at', 'datetime') diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 4c90d06732..a7b0addc1b 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -134,12 +134,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal [["STRICT_ALL_TABLES"]], result.rows end - def test_mysql_strict_mode_disabled_dont_override_global_sql_mode + def test_mysql_strict_mode_disabled run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false})) - global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode" - session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal global_sql_mode.rows, session_sql_mode.rows + result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows end end @@ -151,7 +150,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end - def test_mysql_sql_mode_variable_overides_strict_mode + def test_mysql_sql_mode_variable_overrides_strict_mode run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' })) result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode' diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index cefc3e3c7e..49cfafd7a5 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -105,7 +105,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase with_real_execute do begin ActiveRecord::Base.connection.create_table :delete_me do |t| - t.timestamps + t.timestamps null: true end ActiveRecord::Base.connection.remove_timestamps :delete_me assert !column_present?('delete_me', 'updated_at', 'datetime') diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 267aa232d9..f3c711a64b 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -47,7 +47,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal "1", attributes["published"] assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal 1, @connection.type_cast(true, string_column) + assert_equal "1", @connection.type_cast(true, string_column) end test "test type casting without emulated booleans" do @@ -60,7 +60,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal "1", attributes["published"] assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal 1, @connection.type_cast(true, string_column) + assert_equal "1", @connection.type_cast(true, string_column) end test "with booleans stored as 1 and 0" do diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 65f50e77bb..beedb4f3a1 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -60,12 +60,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal [["STRICT_ALL_TABLES"]], result.rows end - def test_mysql_strict_mode_disabled_dont_override_global_sql_mode + def test_mysql_strict_mode_disabled run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false})) - global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode" - session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal global_sql_mode.rows, session_sql_mode.rows + result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows end end @@ -77,7 +76,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end - def test_mysql_sql_mode_variable_overides_strict_mode + def test_mysql_sql_mode_variable_overrides_strict_mode run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' })) result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode' diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index ec73ec35aa..9c49599d34 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -6,12 +6,12 @@ module ActiveRecord class SchemaMigrationsTest < ActiveRecord::TestCase def test_renaming_index_on_foreign_key connection.add_index "engines", "car_id" - connection.execute "ALTER TABLE engines ADD CONSTRAINT fk_engines_cars FOREIGN KEY (car_id) REFERENCES cars(id)" + connection.add_foreign_key :engines, :cars, name: "fk_engines_cars" connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed") assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name) ensure - connection.execute "ALTER TABLE engines DROP FOREIGN KEY fk_engines_cars" + connection.remove_foreign_key :engines, name: "fk_engines_cars" end def test_initializes_schema_migrations_for_encoding_utf8mb4 diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index a51d5e9d31..8df1b7d18c 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -38,7 +38,6 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal :string, @column.type assert_equal "character varying", @column.sql_type assert @column.array - assert_not @column.text? assert_not @column.number? assert_not @column.binary? @@ -192,16 +191,6 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings)) end - def test_update_all - pg_array = PgArray.create! tags: ["one", "two", "three"] - - PgArray.update_all tags: ["four", "five"] - assert_equal ["four", "five"], pg_array.reload.tags - - PgArray.update_all tags: [] - assert_equal [], pg_array.reload.tags - end - def test_escaping unknown = 'foo\\",bar,baz,\\' tags = ["hello_#{unknown}"] diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index 9ee3610afd..72222c01fd 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -26,7 +26,6 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase column = PostgresqlBitString.columns_hash["a_bit"] assert_equal :bit, column.type assert_equal "bit(8)", column.sql_type - assert_not column.text? assert_not column.number? assert_not column.binary? assert_not column.array @@ -36,7 +35,6 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase column = PostgresqlBitString.columns_hash["a_bit_varying"] assert_equal :bit_varying, column.type assert_equal "bit varying(4)", column.sql_type - assert_not column.text? assert_not column.number? assert_not column.binary? assert_not column.array diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index 90e837d426..2acb64f81c 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -35,7 +35,6 @@ if ActiveRecord::Base.connection.supports_extensions? column = Citext.columns_hash['cival'] assert_equal :citext, column.type assert_equal 'citext', column.sql_type - assert_not column.text? assert_not column.number? assert_not column.binary? assert_not column.array diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 42c68cdae7..cfab5ca902 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -51,7 +51,6 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase assert_nil column.type assert_equal "full_address", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -113,7 +112,6 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase assert_equal :full_address, column.type assert_equal "full_address", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index fd7fdecff1..1500adb42d 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -30,7 +30,6 @@ class PostgresqlDomainTest < ActiveRecord::TestCase assert_equal :decimal, column.type assert_equal "custom_money", column.sql_type assert column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index 0e97f37a6c..d99c4a292e 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -34,7 +34,6 @@ class PostgresqlEnumTest < ActiveRecord::TestCase assert_equal :enum, column.type assert_equal "mood", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index ec646de5e9..9dadb177ca 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -9,7 +9,6 @@ class PostgresqlFullTextTest < ActiveRecord::TestCase assert_equal :tsvector, column.type assert_equal "tsvector", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index faf195783d..6c0adbbeaa 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -28,7 +28,6 @@ class PostgresqlPointTest < ActiveRecord::TestCase column = PostgresqlPoint.columns_hash["x"] assert_equal :point, column.type assert_equal "point", column.sql_type - assert_not column.text? assert_not column.number? assert_not column.binary? assert_not column.array diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 06788df4e1..1296eb72c0 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -56,7 +56,6 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_equal :hstore, @column.type assert_equal "hstore", @column.sql_type assert_not @column.number? - assert_not @column.text? assert_not @column.binary? assert_not @column.array end @@ -112,13 +111,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase end def test_type_cast_hstore - assert @column - - data = "\"1\"=>\"2\"" - hash = @column.class.string_to_hstore data - assert_equal({'1' => '2'}, hash) - assert_equal({'1' => '2'}, @column.type_cast_from_database(data)) - + assert_equal({'1' => '2'}, @column.type_cast_from_database("\"1\"=>\"2\"")) assert_equal({}, @column.type_cast_from_database("")) assert_equal({'key'=>nil}, @column.type_cast_from_database('key => NULL')) assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b"))) @@ -173,19 +166,19 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase end def test_gen1 - assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) + assert_equal(%q(" "=>""), @column.cast_type.type_cast_for_database({' '=>''})) end def test_gen2 - assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''})) + assert_equal(%q(","=>""), @column.cast_type.type_cast_for_database({','=>''})) end def test_gen3 - assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''})) + assert_equal(%q("="=>""), @column.cast_type.type_cast_for_database({'='=>''})) end def test_gen4 - assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''})) + assert_equal(%q(">"=>""), @column.cast_type.type_cast_for_database({'>'=>''})) end def test_parse1 @@ -295,16 +288,6 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_cycle("a\nb" => "c\nd") end - def test_update_all - hstore = Hstore.create! tags: { "one" => "two" } - - Hstore.update_all tags: { "three" => "four" } - assert_equal({ "three" => "four" }, hstore.reload.tags) - - Hstore.update_all tags: { } - assert_equal({ }, hstore.reload.tags) - end - class TagCollection def initialize(hash); @hash = hash end def to_hash; @hash end diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb new file mode 100644 index 0000000000..22e8873333 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -0,0 +1,44 @@ +require "cases/helper" + +class PostgresqlInfinityTest < ActiveRecord::TestCase + class PostgresqlInfinity < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:postgresql_infinities) do |t| + t.float :float + t.datetime :datetime + end + end + + teardown do + @connection.execute("DROP TABLE IF EXISTS postgresql_infinities") + end + + test "type casting infinity on a float column" do + record = PostgresqlInfinity.create!(float: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.float + end + + test "update_all with infinity on a float column" do + record = PostgresqlInfinity.create! + PostgresqlInfinity.update_all(float: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.float + end + + test "type casting infinity on a datetime column" do + record = PostgresqlInfinity.create!(datetime: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.datetime + end + + test "update_all with infinity on a datetime column" do + record = PostgresqlInfinity.create! + PostgresqlInfinity.update_all(datetime: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.datetime + end +end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 4cdb4a4893..86ba849445 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -4,7 +4,7 @@ require "cases/helper" require 'active_record/base' require 'active_record/connection_adapters/postgresql_adapter' -class PostgresqlJSONTest < ActiveRecord::TestCase +module PostgresqlJSONSharedTestCases class JsonDataType < ActiveRecord::Base self.table_name = 'json_data_type' @@ -16,8 +16,8 @@ class PostgresqlJSONTest < ActiveRecord::TestCase begin @connection.transaction do @connection.create_table('json_data_type') do |t| - t.json 'payload', :default => {} - t.json 'settings' + t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {} + t.public_send column_type, 'settings' # t.json 'settings' end end rescue ActiveRecord::StatementInvalid @@ -26,22 +26,21 @@ class PostgresqlJSONTest < ActiveRecord::TestCase @column = JsonDataType.columns_hash['payload'] end - teardown do + def teardown @connection.execute 'drop table if exists json_data_type' end def test_column column = JsonDataType.columns_hash["payload"] - assert_equal :json, column.type - assert_equal "json", column.sql_type + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end def test_default - @connection.add_column 'json_data_type', 'permissions', :json, default: '{"users": "read", "posts": ["read", "write"]}' + @connection.add_column 'json_data_type', 'permissions', column_type, default: '{"users": "read", "posts": ["read", "write"]}' JsonDataType.reset_column_information assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions']) @@ -53,11 +52,11 @@ class PostgresqlJSONTest < ActiveRecord::TestCase def test_change_table_supports_json @connection.transaction do @connection.change_table('json_data_type') do |t| - t.json 'users', default: '{}' + t.public_send column_type, 'users', default: '{}' # t.json 'users', default: '{}' end JsonDataType.reset_column_information column = JsonDataType.columns_hash['users'] - assert_equal :json, column.type + assert_equal column_type, column.type raise ActiveRecord::Rollback # reset the schema change end @@ -77,7 +76,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase column = JsonDataType.columns_hash["payload"] data = "{\"a_key\":\"a_value\"}" - hash = column.class.string_to_json data + hash = column.type_cast_from_database(data) assert_equal({'a_key' => 'a_value'}, hash) assert_equal({'a_key' => 'a_value'}, column.type_cast_from_database(data)) @@ -155,16 +154,6 @@ class PostgresqlJSONTest < ActiveRecord::TestCase assert_equal "320×480", y.resolution end - def test_update_all - json = JsonDataType.create! payload: { "one" => "two" } - - JsonDataType.update_all payload: { "three" => "four" } - assert_equal({ "three" => "four" }, json.reload.payload) - - JsonDataType.update_all payload: { } - assert_equal({ }, json.reload.payload) - end - def test_changes_in_place json = JsonDataType.new assert_not json.changed? @@ -186,3 +175,19 @@ class PostgresqlJSONTest < ActiveRecord::TestCase assert_not json.changed? end end + +class PostgresqlJSONTest < ActiveRecord::TestCase + include PostgresqlJSONSharedTestCases + + def column_type + :json + end +end + +class PostgresqlJSONBTest < ActiveRecord::TestCase + include PostgresqlJSONSharedTestCases + + def column_type + :jsonb + end +end diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index ddb7cd658c..889e369bd6 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -31,7 +31,6 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase assert_equal :ltree, column.type assert_equal "ltree", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index cf2a4ab6ea..87183174f2 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -26,7 +26,6 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase assert_equal "money", column.sql_type assert_equal 2, column.scale assert column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -70,4 +69,28 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase money.reload assert_equal new_value, money.wealth end + + def test_update_all_with_money_string + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: "987.65") + money.reload + + assert_equal 987.65, money.wealth + end + + def test_update_all_with_money_big_decimal + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: '123.45'.to_d) + money.reload + + assert_equal 123.45, money.wealth + end + + def test_update_all_with_money_numeric + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: 123.45) + money.reload + + assert_equal 123.45, money.wealth + end end diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb index 32085cbb17..4f4c1103fa 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -10,7 +10,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_equal :cidr, column.type assert_equal "cidr", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -20,7 +19,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_equal :inet, column.type assert_equal "inet", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -30,7 +28,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_equal :macaddr, column.type assert_equal "macaddr", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 218c59247e..11d5173d37 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -57,6 +57,17 @@ module ActiveRecord assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0)) assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime) end + + def test_quote_range + range = "1,2]'; SELECT * FROM users; --".."a" + c = PostgreSQLColumn.new(nil, nil, OID::Range.new(Type::Integer.new, :int8range)) + assert_equal "'[1,0]'", @conn.quote(range, c) + end + + def test_quote_bit_string + c = PostgreSQLColumn.new(nil, 1, OID::Bit.new) + assert_equal nil, @conn.quote("'); SELECT * FROM users; /*\n01\n*/--", c) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 0f6e39322c..d812cd01c4 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -262,6 +262,23 @@ _SQL assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") } end + def test_update_all_with_ranges + PostgresqlRange.create! + + PostgresqlRange.update_all(int8_range: 1..100) + + assert_equal 1...101, PostgresqlRange.first.int8_range + end + + def test_ranges_correctly_escape_input + range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a" + PostgresqlRange.update_all(int8_range: range) + + assert_nothing_raised do + PostgresqlRange.first + end + end + private def assert_equal_round_trip(range, attribute, value) round_trip(range, attribute, value) diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index 3614b29190..eb32c4d2c2 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -87,7 +87,7 @@ class TimestampTest < ActiveRecord::TestCase def test_timestamps_helper_with_custom_precision ActiveRecord::Base.connection.create_table(:foos) do |t| - t.timestamps :precision => 4 + t.timestamps :null => true, :precision => 4 end assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision') assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision') @@ -95,7 +95,7 @@ class TimestampTest < ActiveRecord::TestCase def test_passing_precision_to_timestamp_does_not_set_limit ActiveRecord::Base.connection.create_table(:foos) do |t| - t.timestamps :precision => 4 + t.timestamps :null => true, :precision => 4 end assert_nil activerecord_column_option("foos", "created_at", "limit") assert_nil activerecord_column_option("foos", "updated_at", "limit") @@ -104,14 +104,14 @@ class TimestampTest < ActiveRecord::TestCase def test_invalid_timestamp_precision_raises_error assert_raises ActiveRecord::ActiveRecordError do ActiveRecord::Base.connection.create_table(:foos) do |t| - t.timestamps :precision => 7 + t.timestamps :null => true, :precision => 7 end end end def test_postgres_agrees_with_activerecord_about_precision ActiveRecord::Base.connection.create_table(:foos) do |t| - t.timestamps :precision => 4 + t.timestamps :null => true, :precision => 4 end assert_equal '4', pg_datetime_precision('foos', 'created_at') assert_equal '4', pg_datetime_precision('foos', 'updated_at') diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 40ed0f64a4..66006d718f 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -9,15 +9,6 @@ module PostgresqlUUIDHelper @connection ||= ActiveRecord::Base.connection end - def enable_uuid_ossp - unless connection.extension_enabled?('uuid-ossp') - connection.enable_extension 'uuid-ossp' - connection.commit_db_transaction - end - - connection.reconnect! - end - def drop_table(name) connection.execute "drop table if exists #{name}" end @@ -60,7 +51,6 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase assert_equal :uuid, column.type assert_equal "uuid", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -70,6 +60,43 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase assert_equal(nil, UUIDType.last.guid) end + def test_treat_invalid_uuid_as_nil + uuid = UUIDType.create! guid: 'foobar' + assert_equal(nil, uuid.guid) + end + + def test_invalid_uuid_dont_modify_before_type_cast + uuid = UUIDType.new guid: 'foobar' + assert_equal 'foobar', uuid.guid_before_type_cast + end + + def test_rfc_4122_regex + # Valid uuids + ['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11', + '{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}', + 'a0eebc999c0b4ef8bb6d6bb9bd380a11', + 'a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11', + '{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}'].each do |valid_uuid| + uuid = UUIDType.new guid: valid_uuid + assert_not_nil uuid.guid + end + + # Invalid uuids + [['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11'], + Hash.new, + 0, + 0.0, + true, + 'Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11', + '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}', + 'a0eebc999r0b4ef8ab6d6bb9bd380a11', + 'a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11', + '{a0eebc99-bb6d6bb9-bd380a11}'].each do |invalid_uuid| + uuid = UUIDType.new guid: invalid_uuid + assert_nil uuid.guid + end + end + def test_uuid_formats ["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11", "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}", @@ -91,16 +118,32 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase end setup do - enable_uuid_ossp + enable_uuid_ossp!(connection) connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t| t.string 'name' t.uuid 'other_uuid', default: 'uuid_generate_v4()' end + + # Create custom PostgreSQL function to generate UUIDs + # to test dumping tables which columns have defaults with custom functions + connection.execute <<-SQL + CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid + AS $$ SELECT * FROM uuid_generate_v4() $$ + LANGUAGE SQL VOLATILE; + SQL + + # Create such a table with custom function as default value generator + connection.create_table('pg_uuids_2', id: :uuid, default: 'my_uuid_generator()') do |t| + t.string 'name' + t.uuid 'other_uuid_2', default: 'my_uuid_generator()' + end end teardown do drop_table "pg_uuids" + drop_table 'pg_uuids_2' + connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();' end if ActiveRecord::Base.connection.supports_extensions? @@ -132,6 +175,13 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string) assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string) end + + def test_schema_dumper_for_uuid_primary_key_with_custom_default + schema = StringIO.new + ActiveRecord::SchemaDumper.dump(connection, schema) + assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema.string) + assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema.string) + end end end @@ -139,7 +189,7 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase include PostgresqlUUIDHelper setup do - enable_uuid_ossp + enable_uuid_ossp!(connection) connection.create_table('pg_uuids', id: false) do |t| t.primary_key :id, :uuid, default: nil @@ -176,14 +226,14 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase end setup do - enable_uuid_ossp + enable_uuid_ossp!(connection) connection.transaction do connection.create_table('pg_uuid_posts', id: :uuid) do |t| t.string 'title' end connection.create_table('pg_uuid_comments', id: :uuid) do |t| - t.uuid :uuid_post_id, default: 'uuid_generate_v4()' + t.references :uuid_post, type: :uuid t.string 'content' end end diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index 48c6eeb62c..4165dd5ac9 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -11,7 +11,7 @@ class PostgresqlXMLTest < ActiveRecord::TestCase begin @connection.transaction do @connection.create_table('xml_data_type') do |t| - t.xml 'payload', default: {} + t.xml 'payload' end end rescue ActiveRecord::StatementInvalid @@ -32,4 +32,17 @@ class PostgresqlXMLTest < ActiveRecord::TestCase @connection.execute %q|insert into xml_data_type (payload) VALUES(null)| assert_nil XmlDataType.first.payload end + + def test_round_trip + data = XmlDataType.new(payload: "<foo>bar</foo>") + assert_equal "<foo>bar</foo>", data.payload + data.save! + assert_equal "<foo>bar</foo>", data.reload.payload + end + + def test_update_all + data = XmlDataType.create! + XmlDataType.update_all(payload: "<bar>baz</bar>") + assert_equal "<bar>baz</bar>", data.reload.payload + end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 8c9a051eea..ac8332e2fa 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -15,10 +15,10 @@ module ActiveRecord def test_type_cast_binary_encoding_without_logger @conn.extend(Module.new { def logger; end }) - cast_type = Type::String.new + column = Column.new(nil, nil, Type::String.new) binary = SecureRandom.hex expected = binary.dup.encode!(Encoding::UTF_8) - assert_equal expected, @conn.type_cast(binary, cast_type) + assert_equal expected, @conn.type_cast(binary, column) end def test_type_cast_symbol @@ -103,6 +103,13 @@ module ActiveRecord }.new assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) } end + + def test_quoting_binary_strings + value = "hello".encode('ascii-8bit') + column = Column.new(nil, 1, SQLite3String.new) + + assert_equal "'hello'", @conn.quote(value, column) + end end end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index b89caa3d55..b2bf9480dd 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -57,10 +57,11 @@ module ActiveRecord end end - # sqlite databases should be able to support any type and not - # just the ones mentioned in the native_database_types. - # Therefore test_invalid column should always return true - # even if the type is not valid. + # sqlite3 databases should be able to support any type and not just the + # ones mentioned in the native_database_types. + # + # Therefore test_invalid column should always return true even if the + # type is not valid. def test_invalid_column assert @conn.valid_type?(:foobar) end diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 8700b20dee..3f5858714a 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -14,6 +14,7 @@ if ActiveRecord::Base.connection.supports_migrations? @connection.drop_table :fruits rescue nil @connection.drop_table :nep_fruits rescue nil @connection.drop_table :nep_schema_migrations rescue nil + @connection.drop_table :has_timestamps rescue nil ActiveRecord::SchemaMigration.delete_all rescue nil end @@ -88,5 +89,61 @@ if ActiveRecord::Base.connection.supports_migrations? assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017") assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") end + + def test_timestamps_without_null_is_deprecated_on_create_table + assert_deprecated do + ActiveRecord::Schema.define do + create_table :has_timestamps do |t| + t.timestamps + end + end + end + end + + def test_timestamps_without_null_is_deprecated_on_change_table + assert_deprecated do + ActiveRecord::Schema.define do + create_table :has_timestamps + + change_table :has_timestamps do |t| + t.timestamps + end + end + end + end + + def test_no_deprecation_warning_from_timestamps_on_create_table + assert_not_deprecated do + ActiveRecord::Schema.define do + create_table :has_timestamps do |t| + t.timestamps null: true + end + + drop_table :has_timestamps + + create_table :has_timestamps do |t| + t.timestamps null: false + end + end + end + end + + def test_no_deprecation_warning_from_timestamps_on_change_table + assert_not_deprecated do + ActiveRecord::Schema.define do + create_table :has_timestamps + change_table :has_timestamps do |t| + t.timestamps null: true + end + + drop_table :has_timestamps + + create_table :has_timestamps + change_table :has_timestamps do |t| + t.timestamps null: false, default: Time.now + end + end + end + end end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 9c92dc1141..25555bd75c 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -787,8 +787,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) comment = comments(:greetings) - assert_difference lambda { post.reload.taggings_count }, -1 do - assert_difference 'comment.reload.taggings_count', +1 do + assert_difference lambda { post.reload.tags_count }, -1 do + assert_difference 'comment.reload.tags_count', +1 do tagging.taggable = comment end end @@ -935,3 +935,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Column.count end end + +class BelongsToWithForeignKeyTest < ActiveRecord::TestCase + fixtures :authors, :author_addresses + + def test_destroy_linked_models + address = AuthorAddress.create! + author = Author.create! name: "Author", author_address_id: address.id + + author.destroy! + end +end diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 71c0609df5..51d8e0523e 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -35,9 +35,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations assert_nothing_raised do - Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a + Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a end - authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a + authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a assert_equal 1, assert_no_queries { authors.size } assert_equal 10, assert_no_queries { authors[0].comments.size } end diff --git a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb new file mode 100644 index 0000000000..48f7ddbe83 --- /dev/null +++ b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb @@ -0,0 +1,26 @@ +require "cases/helper" + +class DeprecatedCounterCacheOnHasManyThroughTest < ActiveRecord::TestCase + class Post < ActiveRecord::Base + has_many :taggings, as: :taggable + has_many :tags, through: :taggings + end + + class Tagging < ActiveRecord::Base + belongs_to :taggable, polymorphic: true + belongs_to :tag + end + + class Tag < ActiveRecord::Base + end + + test "counter caches are updated in the database if the belongs_to association doesn't specify a counter cache" do + post = Post.create!(title: 'Hello', body: 'World!') + assert_deprecated { post.tags << Tag.create!(name: 'whatever') } + + assert_equal 1, post.tags.size + assert_equal 1, post.tags_count + assert_equal 1, post.reload.tags.size + assert_equal 1, post.reload.tags_count + end +end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 910067666a..21912fdf0f 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1261,4 +1261,33 @@ class EagerAssociationTest < ActiveRecord::TestCase Author.eager_load(:posts_with_signature).to_a end end + + test "preloading readonly association" do + # has-one + firm = Firm.where(id: "1").preload(:readonly_account).first! + assert firm.readonly_account.readonly? + + # has_and_belongs_to_many + project = Project.where(id: "2").preload(:readonly_developers).first! + assert project.readonly_developers.first.readonly? + + # has-many :through + david = Author.where(id: "1").preload(:readonly_comments).first! + assert david.readonly_comments.first.readonly? + end + + test "eager-loading readonly association" do + skip "eager_load does not yet preserve readonly associations" + # has-one + firm = Firm.where(id: "1").eager_load(:readonly_account).first! + assert firm.readonly_account.readonly? + + # has_and_belongs_to_many + project = Project.where(id: "2").eager_load(:readonly_developers).first! + assert project.readonly_developers.first.readonly? + + # has-many :through + david = Author.where(id: "1").eager_load(:readonly_comments).first! + assert david.readonly_comments.first.readonly? + end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index cc58a4a1a2..859310575e 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 @@ -254,7 +254,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_build devel = Developer.find(1) - proj = assert_no_queries { devel.projects.build("name" => "Projekt") } + proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") } assert !devel.projects.loaded? assert_equal devel.projects.last, proj @@ -269,7 +269,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build devel = Developer.find(1) - proj = assert_no_queries { devel.projects.new("name" => "Projekt") } + proj = assert_no_queries(ignore_none: false) { devel.projects.new("name" => "Projekt") } assert !devel.projects.loaded? assert_equal devel.projects.last, proj @@ -503,7 +503,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = project.developers.first - assert_no_queries do + assert_no_queries(ignore_none: false) do assert project.developers.loaded? assert project.developers.include?(developer) end @@ -824,7 +824,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations projects = Developer.new.projects - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal [], projects assert_equal [], projects.where(title: 'omg') assert_equal [], projects.pluck(:title) diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 3c0b735607..fe961e871c 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -36,7 +36,7 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa author = authors(:david) # this can fail on adapters which require ORDER BY expressions to be included in the SELECT expression # if the reorder clauses are not correctly handled - assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.taggings_count DESC').last + assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.tags_count DESC').last end end @@ -814,14 +814,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_without_dependent_option post = posts(:welcome) - assert_difference "post.reload.taggings_count", -1 do + assert_difference "post.reload.tags_count", -1 do post.taggings.delete(post.taggings.first) end end def test_deleting_updates_counter_cache_with_dependent_delete_all post = posts(:welcome) - post.update_columns(taggings_with_delete_all_count: post.taggings_count) + post.update_columns(taggings_with_delete_all_count: post.tags_count) assert_difference "post.reload.taggings_with_delete_all_count", -1 do post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first) @@ -830,13 +830,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_with_dependent_destroy post = posts(:welcome) - post.update_columns(taggings_with_destroy_count: post.taggings_count) + post.update_columns(taggings_with_destroy_count: post.tags_count) assert_difference "post.reload.taggings_with_destroy_count", -1 do post.taggings_with_destroy.delete(post.taggings_with_destroy.first) end end + def test_calling_empty_with_counter_cache + post = posts(:welcome) + assert_queries(0) do + assert_not post.comments.empty? + end + end + def test_custom_named_counter_cache topic = topics(:first) 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 0fa34e829e..a85e020f0c 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -489,7 +489,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) tag = post.tags.create!(:name => 'doomed') - assert_difference ['post.reload.taggings_count', 'post.reload.tags_count'], -1 do + assert_difference ['post.reload.tags_count'], -1 do posts(:welcome).tags.delete(tag) end end @@ -499,7 +499,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag = post.tags.create!(:name => 'doomed') post.update_columns(tags_with_destroy_count: post.tags.count) - assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do + assert_difference ['post.reload.tags_with_destroy_count'], -1 do posts(:welcome).tags_with_destroy.delete(tag) end end @@ -509,7 +509,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag = post.tags.create!(:name => 'doomed') post.update_columns(tags_with_nullify_count: post.tags.count) - assert_no_difference 'post.reload.taggings_count' do + assert_no_difference 'post.reload.tags_count' do assert_difference 'post.reload.tags_with_nullify_count', -1 do posts(:welcome).tags_with_nullify.delete(tag) end @@ -524,14 +524,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag.tagged_posts = [] post.reload - assert_equal(post.taggings.count, post.taggings_count) + assert_equal(post.taggings.count, post.tags_count) end def test_update_counter_caches_on_destroy post = posts(:welcome) tag = post.tags.create!(name: 'doomed') - assert_difference 'post.reload.taggings_count', -1 do + assert_difference 'post.reload.tags_count', -1 do tag.tagged_posts.destroy(post) end end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index aabeea025f..cace7ba142 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -326,11 +326,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_belongs_to_polymorphic_with_counter_cache - assert_equal 1, posts(:welcome)[:taggings_count] + assert_equal 1, posts(:welcome)[:tags_count] tagging = posts(:welcome).taggings.create(:tag => tags(:general)) - assert_equal 2, posts(:welcome, :reload)[:taggings_count] + assert_equal 2, posts(:welcome, :reload)[:tags_count] tagging.destroy - assert_equal 1, posts(:welcome, :reload)[:taggings_count] + assert_equal 1, posts(:welcome, :reload)[:tags_count] end def test_unavailable_through_reflection @@ -489,7 +489,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 1, post_thinking.tags.size) + assert_equal(count + 1, post_thinking.reload.tags.size) assert_equal(count + 1, post_thinking.tags(true).size) assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo') @@ -497,7 +497,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 2, post_thinking.tags.size) + assert_equal(count + 2, post_thinking.reload.tags.size) assert_equal(count + 2, post_thinking.tags(true).size) assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) } @@ -505,7 +505,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 4, post_thinking.tags.size) + assert_equal(count + 4, post_thinking.reload.tags.size) assert_equal(count + 4, post_thinking.tags(true).size) # Raises if the wrong reflection name is used to set the Edge belongs_to @@ -554,34 +554,35 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_associate_when_deleting_from_has_many_through count = posts(:thinking).tags.count - tags_before = posts(:thinking).tags + tags_before = posts(:thinking).tags.sort tag = Tag.create!(:name => 'doomed') post_thinking = posts(:thinking) post_thinking.tags << tag assert_equal(count + 1, post_thinking.taggings(true).size) - assert_equal(count + 1, post_thinking.tags(true).size) + assert_equal(count + 1, post_thinking.reload.tags(true).size) + assert_not_equal(tags_before, post_thinking.tags.sort) assert_nothing_raised { post_thinking.tags.delete(tag) } assert_equal(count, post_thinking.tags.size) assert_equal(count, post_thinking.tags(true).size) assert_equal(count, post_thinking.taggings(true).size) - assert_equal(tags_before.sort, post_thinking.tags.sort) + assert_equal(tags_before, post_thinking.tags.sort) end def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags count = posts(:thinking).tags.count - tags_before = posts(:thinking).tags + tags_before = posts(:thinking).tags.sort doomed = Tag.create!(:name => 'doomed') doomed2 = Tag.create!(:name => 'doomed2') quaked = Tag.create!(:name => 'quaked') post_thinking = posts(:thinking) post_thinking.tags << doomed << doomed2 - assert_equal(count + 2, post_thinking.tags(true).size) + assert_equal(count + 2, post_thinking.reload.tags(true).size) assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) } assert_equal(count, post_thinking.tags.size) assert_equal(count, post_thinking.tags(true).size) - assert_equal(tags_before.sort, post_thinking.tags.sort) + assert_equal(tags_before, post_thinking.tags.sort) end def test_deleting_junk_from_has_many_through_should_raise_type_mismatch diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 8ef351cda8..31b68c940e 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -130,7 +130,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_has_one_through_with_has_one_source_reflection_preload members = assert_queries(4) { Member.includes(:nested_sponsors).to_a } mustache = sponsors(:moustache_club_sponsor_for_groucho) - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal [mustache], members.first.nested_sponsors end end @@ -153,6 +153,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_one_with_has_many_through_source_reflection_preload + ActiveRecord::Base.connection.table_alias_length # preheat cache members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) } groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy) diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb new file mode 100644 index 0000000000..a6934a056e --- /dev/null +++ b/activerecord/test/cases/associations/required_test.rb @@ -0,0 +1,82 @@ +require "cases/helper" + +class RequiredAssociationsTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Parent < ActiveRecord::Base + end + + class Child < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :parents, force: true + @connection.create_table :children, force: true do |t| + t.belongs_to :parent + end + end + + teardown do + @connection.execute("DROP TABLE IF EXISTS parents") + @connection.execute("DROP TABLE IF EXISTS children") + end + + test "belongs_to associations are not required by default" do + model = subclass_of(Child) do + belongs_to :parent, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end + + assert model.new.save + assert model.new(parent: Parent.new).save + end + + test "required belongs_to associations have presence validated" do + model = subclass_of(Child) do + belongs_to :parent, required: true, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end + + record = model.new + assert_not record.save + assert_equal ["Parent can't be blank"], record.errors.full_messages + + record.parent = Parent.new + assert record.save + end + + test "has_one associations are not required by default" do + model = subclass_of(Parent) do + has_one :child, inverse_of: false, + class_name: "RequiredAssociationsTest::Child" + end + + assert model.new.save + assert model.new(child: Child.new).save + end + + test "required has_one associations have presence validated" do + model = subclass_of(Parent) do + has_one :child, required: true, inverse_of: false, + class_name: "RequiredAssociationsTest::Child" + end + + record = model.new + assert_not record.save + assert_equal ["Child can't be blank"], record.errors.full_messages + + record.child = Child.new + assert record.save + end + + private + + def subclass_of(klass, &block) + subclass = Class.new(klass, &block) + def subclass.name + superclass.name + end + subclass + end +end diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb index b352d1a6c2..cbc2c4e5d7 100644 --- a/activerecord/test/cases/attribute_decorators_test.rb +++ b/activerecord/test/cases/attribute_decorators_test.rb @@ -45,7 +45,6 @@ module ActiveRecord test "decoration does not eagerly load existing columns" do assert_no_queries do - Model.reset_column_information Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 7566af920f..b4917e727a 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -143,7 +143,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase # Syck calls respond_to? before actually calling initialize def test_respond_to_with_allocated_object - topic = Topic.allocate + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'topics' + end + + topic = klass.allocate assert !topic.respond_to?("nothingness") assert !topic.respond_to?(:nothingness) assert_respond_to topic, "title" @@ -253,6 +257,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes end + def test_attributes_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers_projects' + end + + assert_equal klass.column_names, klass.new.attributes.keys + assert_not klass.new.has_attribute?('id') + end + def test_hashes_not_mangled new_topic = { :title => "New Topic" } new_topic_values = { :title => "AnotherTopic" } @@ -797,6 +810,24 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "lol", topic.author_name end + def test_inherited_custom_accessors_with_reserved_names + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'computers' + self.abstract_class = true + def system; "omg"; end + def system=(val); self.developer = val; end + end + + subklass = Class.new(klass) + [klass, subklass].each(&:define_attribute_methods) + + computer = subklass.find(1) + assert_equal "omg", computer.system + + computer.developer = 99 + assert_equal 99, computer.developer + end + def test_on_the_fly_super_invokable_generated_attribute_methods_via_method_missing klass = new_topic_like_ar_class do def title diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb index 402a611efa..dc20c3c676 100644 --- a/activerecord/test/cases/attribute_set_test.rb +++ b/activerecord/test/cases/attribute_set_test.rb @@ -8,6 +8,8 @@ module ActiveRecord assert_equal 1, attributes[:foo].value assert_equal 2.2, attributes[:bar].value + assert_equal :foo, attributes[:foo].name + assert_equal :bar, attributes[:bar].name end test "building with custom types" do @@ -24,6 +26,7 @@ module ActiveRecord assert_equal '3.3', attributes[:foo].value_before_type_cast assert_equal nil, attributes[:bar].value_before_type_cast + assert_equal :bar, attributes[:bar].name end test "duping creates a new hash and dups each attribute" do @@ -35,7 +38,7 @@ module ActiveRecord attributes[:bar].value duped = attributes.dup - duped[:foo] = Attribute.from_database(2, Type::Integer.new) + duped.write_from_database(:foo, 2) duped[:bar].value << 'bar' assert_equal 1, attributes[:foo].value @@ -61,5 +64,102 @@ module ActiveRecord assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash) assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h) end + + test "values_before_type_cast" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal({ foo: '1.1', bar: '2.2' }, attributes.values_before_type_cast) + end + + test "known columns are built with uninitialized attributes" do + attributes = attributes_with_uninitialized_key + assert attributes[:foo].initialized? + assert_not attributes[:bar].initialized? + end + + test "uninitialized attributes are not included in the attributes hash" do + attributes = attributes_with_uninitialized_key + assert_equal({ foo: 1 }, attributes.to_hash) + end + + test "uninitialized attributes are not included in keys" do + attributes = attributes_with_uninitialized_key + assert_equal [:foo], attributes.keys + end + + test "uninitialized attributes return false for key?" do + attributes = attributes_with_uninitialized_key + assert attributes.key?(:foo) + assert_not attributes.key?(:bar) + end + + test "unknown attributes return false for key?" do + attributes = attributes_with_uninitialized_key + assert_not attributes.key?(:wibble) + end + + test "fetch_value returns the value for the given initialized attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal 1, attributes.fetch_value(:foo) + assert_equal 2.2, attributes.fetch_value(:bar) + end + + test "fetch_value returns nil for unknown attributes" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:wibble) + end + + test "fetch_value uses the given block for uninitialized attributes" do + attributes = attributes_with_uninitialized_key + value = attributes.fetch_value(:bar) { |n| n.to_s + '!' } + assert_equal 'bar!', value + end + + test "fetch_value returns nil for uninitialized attributes if no block is given" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:bar) + end + + class MyType + def type_cast_from_user(value) + return if value.nil? + value + " from user" + end + + def type_cast_from_database(value) + return if value.nil? + value + " from database" + end + end + + test "write_from_database sets the attribute with database typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_database(:foo, "value") + + assert_equal "value from database", attributes.fetch_value(:foo) + end + + test "write_from_user sets the attribute with user typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_user(:foo, "value") + + assert_equal "value from user", attributes.fetch_value(:foo) + end + + def attributes_with_uninitialized_key + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + builder.build_from_database(foo: '1.1') + end end end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index 57dd2e9a5e..7b325abf1d 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -4,7 +4,7 @@ require 'minitest/mock' module ActiveRecord class AttributeTest < ActiveRecord::TestCase setup do - @type = MiniTest::Mock.new + @type = Minitest::Mock.new end teardown do @@ -13,7 +13,7 @@ module ActiveRecord test "from_database + read type casts from database" do @type.expect(:type_cast_from_database, 'type cast from database', ['a value']) - attribute = Attribute.from_database('a value', @type) + attribute = Attribute.from_database(nil, 'a value', @type) type_cast_value = attribute.value @@ -22,7 +22,7 @@ module ActiveRecord test "from_user + read type casts from user" do @type.expect(:type_cast_from_user, 'type cast from user', ['a value']) - attribute = Attribute.from_user('a value', @type) + attribute = Attribute.from_user(nil, 'a value', @type) type_cast_value = attribute.value @@ -31,7 +31,7 @@ module ActiveRecord test "reading memoizes the value" do @type.expect(:type_cast_from_database, 'from the database', ['whatever']) - attribute = Attribute.from_database('whatever', @type) + attribute = Attribute.from_database(nil, 'whatever', @type) type_cast_value = attribute.value second_read = attribute.value @@ -42,14 +42,14 @@ module ActiveRecord test "reading memoizes falsy values" do @type.expect(:type_cast_from_database, false, ['whatever']) - attribute = Attribute.from_database('whatever', @type) + attribute = Attribute.from_database(nil, 'whatever', @type) attribute.value attribute.value end test "read_before_typecast returns the given value" do - attribute = Attribute.from_database('raw value', @type) + attribute = Attribute.from_database(nil, 'raw value', @type) raw_value = attribute.value_before_type_cast @@ -59,7 +59,7 @@ module ActiveRecord test "from_database + read_for_database type casts to and from database" do @type.expect(:type_cast_from_database, 'read from database', ['whatever']) @type.expect(:type_cast_for_database, 'ready for database', ['read from database']) - attribute = Attribute.from_database('whatever', @type) + attribute = Attribute.from_database(nil, 'whatever', @type) type_cast_for_database = attribute.value_for_database @@ -69,7 +69,7 @@ module ActiveRecord test "from_user + read_for_database type casts from the user to the database" do @type.expect(:type_cast_from_user, 'read from user', ['whatever']) @type.expect(:type_cast_for_database, 'ready for database', ['read from user']) - attribute = Attribute.from_user('whatever', @type) + attribute = Attribute.from_user(nil, 'whatever', @type) type_cast_for_database = attribute.value_for_database @@ -78,7 +78,7 @@ module ActiveRecord test "duping dups the value" do @type.expect(:type_cast_from_database, 'type cast', ['a value']) - attribute = Attribute.from_database('a value', @type) + attribute = Attribute.from_database(nil, 'a value', @type) value_from_orig = attribute.value value_from_clone = attribute.dup.value @@ -90,14 +90,83 @@ module ActiveRecord test "duping does not dup the value if it is not dupable" do @type.expect(:type_cast_from_database, false, ['a value']) - attribute = Attribute.from_database('a value', @type) + attribute = Attribute.from_database(nil, 'a value', @type) assert_same attribute.value, attribute.dup.value end test "duping does not eagerly type cast if we have not yet type cast" do - attribute = Attribute.from_database('a value', @type) + attribute = Attribute.from_database(nil, 'a value', @type) attribute.dup end + + class MyType + def type_cast_from_user(value) + value + " from user" + end + + def type_cast_from_database(value) + value + " from database" + end + end + + test "with_value_from_user returns a new attribute with the value from the user" do + old = Attribute.from_database(nil, "old", MyType.new) + new = old.with_value_from_user("new") + + assert_equal "old from database", old.value + assert_equal "new from user", new.value + end + + test "with_value_from_database returns a new attribute with the value from the database" do + old = Attribute.from_user(nil, "old", MyType.new) + new = old.with_value_from_database("new") + + assert_equal "old from user", old.value + assert_equal "new from database", new.value + end + + test "uninitialized attributes yield their name if a block is given to value" do + block = proc { |name| name.to_s + "!" } + foo = Attribute.uninitialized(:foo, nil) + bar = Attribute.uninitialized(:bar, nil) + + assert_equal "foo!", foo.value(&block) + assert_equal "bar!", bar.value(&block) + end + + test "uninitialized attributes have no value" do + assert_nil Attribute.uninitialized(:foo, nil).value + end + + test "attributes equal other attributes with the same constructor arguments" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:foo, 1, Type::Integer.new) + assert_equal first, second + end + + test "attributes do not equal attributes with different names" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:bar, 1, Type::Integer.new) + assert_not_equal first, second + end + + test "attributes do not equal attributes with different types" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:foo, 1, Type::Float.new) + assert_not_equal first, second + end + + test "attributes do not equal attributes with different values" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:foo, 2, Type::Integer.new) + assert_not_equal first, second + end + + test "attributes do not equal attributes of other classes" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_user(:foo, 1, Type::Integer.new) + assert_not_equal first, second + end end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 8f83cf7cb4..4c0b0c868a 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1347,14 +1347,32 @@ class BasicsTest < ActiveRecord::TestCase end def test_compute_type_no_method_error - ActiveSupport::Dependencies.stubs(:constantize).raises(NoMethodError) + ActiveSupport::Dependencies.stubs(:safe_constantize).raises(NoMethodError) assert_raises NoMethodError do ActiveRecord::Base.send :compute_type, 'InvalidModel' end end + def test_compute_type_on_undefined_method + error = nil + begin + Class.new(Author) do + alias_method :foo, :bar + end + rescue => e + error = e + end + + ActiveSupport::Dependencies.stubs(:safe_constantize).raises(e) + + exception = assert_raises NameError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + assert_equal error.message, exception.message + end + def test_compute_type_argument_error - ActiveSupport::Dependencies.stubs(:constantize).raises(ArgumentError) + ActiveSupport::Dependencies.stubs(:safe_constantize).raises(ArgumentError) assert_raises ArgumentError do ActiveRecord::Base.send :compute_type, 'InvalidModel' end diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index ecad7c942f..c7531f5418 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -45,8 +45,8 @@ module ActiveRecord @cache = Marshal.load(Marshal.dump(@cache)) - assert_equal 12, @cache.columns('posts').size - assert_equal 12, @cache.columns_hash('posts').size + assert_equal 11, @cache.columns('posts').size + assert_equal 11, @cache.columns_hash('posts').size assert @cache.tables('posts') assert_equal 'id', @cache.primary_keys('posts') end diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index ab2a749ba8..07a182070b 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -19,6 +19,7 @@ class CounterCacheTest < ActiveRecord::TestCase class ::SpecialTopic < ::Topic has_many :special_replies, :foreign_key => 'parent_id' + has_many :lightweight_special_replies, -> { select('topics.id, topics.title') }, :foreign_key => 'parent_id', :class_name => 'SpecialReply' end class ::SpecialReply < ::Reply @@ -170,4 +171,13 @@ class CounterCacheTest < ActiveRecord::TestCase end assert_equal "'Topic' has no association called 'undefined_count'", e.message end + + test "reset counter works with select declared on association" do + special = SpecialTopic.create!(:title => 'Special') + SpecialTopic.increment_counter(:replies_count, special.id) + + assert_difference 'special.reload.replies_count', -1 do + SpecialTopic.reset_counters(special.id, :lightweight_special_replies) + end + end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 5d6601a881..0c77eedb52 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -169,7 +169,19 @@ class DirtyTest < ActiveRecord::TestCase pirate = Pirate.create!(:catchphrase => 'Yar!') pirate.catchphrase = 'Ahoy!' - pirate.reset_catchphrase! + assert_deprecated do + pirate.reset_catchphrase! + end + assert_equal "Yar!", pirate.catchphrase + assert_equal Hash.new, pirate.changes + assert !pirate.catchphrase_changed? + end + + def test_restore_attribute! + pirate = Pirate.create!(:catchphrase => 'Yar!') + pirate.catchphrase = 'Ahoy!' + + pirate.restore_catchphrase! assert_equal "Yar!", pirate.catchphrase assert_equal Hash.new, pirate.changes assert !pirate.catchphrase_changed? @@ -309,16 +321,14 @@ class DirtyTest < ActiveRecord::TestCase def test_attribute_will_change! pirate = Pirate.create!(:catchphrase => 'arr') - pirate.catchphrase << ' matey' assert !pirate.catchphrase_changed? - assert pirate.catchphrase_will_change! assert pirate.catchphrase_changed? - assert_equal ['arr matey', 'arr matey'], pirate.catchphrase_change + assert_equal ['arr', 'arr'], pirate.catchphrase_change - pirate.catchphrase << '!' + pirate.catchphrase << ' matey!' assert pirate.catchphrase_changed? - assert_equal ['arr matey', 'arr matey!'], pirate.catchphrase_change + assert_equal ['arr', 'arr matey!'], pirate.catchphrase_change end def test_association_assignment_changes_foreign_key @@ -400,7 +410,7 @@ class DirtyTest < ActiveRecord::TestCase def test_dup_objects_should_not_copy_dirty_flag_from_creator pirate = Pirate.create!(:catchphrase => "shiver me timbers") pirate_dup = pirate.dup - pirate_dup.reset_catchphrase! + pirate_dup.restore_catchphrase! pirate.catchphrase = "I love Rum" assert pirate.catchphrase_changed? assert !pirate_dup.catchphrase_changed? @@ -651,6 +661,27 @@ class DirtyTest < ActiveRecord::TestCase assert_not model.foo_changed? end + test "in place mutation detection" do + pirate = Pirate.create!(catchphrase: "arrrr") + pirate.catchphrase << " matey!" + + assert pirate.catchphrase_changed? + expected_changes = { + "catchphrase" => ["arrrr", "arrrr matey!"] + } + assert_equal(expected_changes, pirate.changes) + assert_equal("arrrr", pirate.catchphrase_was) + assert pirate.catchphrase_changed?(from: "arrrr") + assert_not pirate.catchphrase_changed?(from: "anything else") + assert pirate.changed_attributes.include?(:catchphrase) + + pirate.save! + pirate.reload + + assert_equal "arrrr matey!", pirate.catchphrase + assert_not pirate.changed? + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 409d9a82e2..638cffe0e6 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -141,5 +141,17 @@ module ActiveRecord ensure Topic.default_scopes = prev_default_scopes end + + def test_dup_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'parrots_pirates' + end + + record = klass.create! + + assert_nothing_raised do + record.dup + end + end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index bd77c412f6..95d006279d 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -51,7 +51,7 @@ class FinderTest < ActiveRecord::TestCase end def test_symbols_table_ref - Post.first # warm up + Post.where("author_id" => nil) # warm up x = Symbol.all_symbols.count Post.where("title" => {"xxxqqqq" => "bar"}) assert_equal x, Symbol.all_symbols.count @@ -144,8 +144,8 @@ class FinderTest < ActiveRecord::TestCase def test_exists_with_distinct_association_includes_limit_and_order author = Author.first - assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists? - assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists? + assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(0).exists? + assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(1).exists? end def test_exists_with_empty_table_and_no_args_given diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 937646b09a..6a8aff4b69 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -199,3 +199,5 @@ module InTimeZone ActiveRecord::Base.time_zone_aware_attributes = old_tz end end + +require 'mocha/setup' # FIXME: stop using mocha diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index c66eaf1ee1..bd3dd29f4d 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -176,8 +176,11 @@ module ActiveRecord end def test_create_table_with_timestamps_should_create_datetime_columns - connection.create_table table_name do |t| - t.timestamps + # FIXME: Remove the silence when we change the default `null` behavior + ActiveSupport::Deprecation.silence do + connection.create_table table_name do |t| + t.timestamps + end end created_columns = connection.columns(table_name) diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index a6d506b04a..777a48ad14 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -72,10 +72,24 @@ module ActiveRecord end end + def test_references_column_type_with_polymorphic_and_type + with_change_table do |t| + @connection.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string] + t.references :taggable, polymorphic: true, type: :string + end + end + + def test_remove_references_column_type_with_polymorphic_and_type + with_change_table do |t| + @connection.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string] + t.remove_references :taggable, polymorphic: true, type: :string + end + end + def test_timestamps_creates_updated_at_and_created_at with_change_table do |t| - @connection.expect :add_timestamps, nil, [:delete_me] - t.timestamps + @connection.expect :add_timestamps, nil, [:delete_me, null: true] + t.timestamps null: true end end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 93adbdd05b..763aa88f72 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -51,46 +51,46 @@ module ActiveRecord end end - # We specifically do a manual INSERT here, and then test only the SELECT - # functionality. This allows us to more easily catch INSERT being broken, - # but SELECT actually working fine. - def test_native_decimal_insert_manual_vs_automatic - correct_value = '0012345678901234567890.0123456789'.to_d - - connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' - - # Do a manual insertion - if current_adapter?(:OracleAdapter) - connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)" - elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings - connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')" - elsif current_adapter?(:PostgreSQLAdapter) - connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" - else - connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" - end + unless current_adapter?(:SQLite3Adapter) + # We specifically do a manual INSERT here, and then test only the SELECT + # functionality. This allows us to more easily catch INSERT being broken, + # but SELECT actually working fine. + def test_native_decimal_insert_manual_vs_automatic + correct_value = '0012345678901234567890.0123456789'.to_d + + connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' + + # Do a manual insertion + if current_adapter?(:OracleAdapter) + connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)" + elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings + connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')" + elsif current_adapter?(:PostgreSQLAdapter) + connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" + else + connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" + end - # SELECT - row = TestModel.first - assert_kind_of BigDecimal, row.wealth + # SELECT + row = TestModel.first + assert_kind_of BigDecimal, row.wealth - # If this assert fails, that means the SELECT is broken! - unless current_adapter?(:SQLite3Adapter) - assert_equal correct_value, row.wealth - end + # If this assert fails, that means the SELECT is broken! + unless current_adapter?(:SQLite3Adapter) + assert_equal correct_value, row.wealth + end - # Reset to old state - TestModel.delete_all + # Reset to old state + TestModel.delete_all - # Now use the Rails insertion - TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789") + # Now use the Rails insertion + TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789") - # SELECT - row = TestModel.first - assert_kind_of BigDecimal, row.wealth + # SELECT + row = TestModel.first + assert_kind_of BigDecimal, row.wealth - # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! - unless current_adapter?(:SQLite3Adapter) + # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! assert_equal correct_value, row.wealth end end @@ -121,54 +121,54 @@ module ActiveRecord end end - def test_native_types - add_column "test_models", "first_name", :string - add_column "test_models", "last_name", :string - add_column "test_models", "bio", :text - add_column "test_models", "age", :integer - add_column "test_models", "height", :float - add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' - add_column "test_models", "birthday", :datetime - add_column "test_models", "favorite_day", :date - add_column "test_models", "moment_of_truth", :datetime - add_column "test_models", "male", :boolean - - TestModel.create :first_name => 'bob', :last_name => 'bobsen', - :bio => "I was born ....", :age => 18, :height => 1.78, - :wealth => BigDecimal.new("12345678901234567890.0123456789"), - :birthday => 18.years.ago, :favorite_day => 10.days.ago, - :moment_of_truth => "1782-10-10 21:40:18", :male => true - - bob = TestModel.first - assert_equal 'bob', bob.first_name - assert_equal 'bobsen', bob.last_name - assert_equal "I was born ....", bob.bio - assert_equal 18, bob.age - - # Test for 30 significant digits (beyond the 16 of float), 10 of them - # after the decimal place. - - unless current_adapter?(:SQLite3Adapter) + unless current_adapter?(:SQLite3Adapter) + def test_native_types + add_column "test_models", "first_name", :string + add_column "test_models", "last_name", :string + add_column "test_models", "bio", :text + add_column "test_models", "age", :integer + add_column "test_models", "height", :float + add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' + add_column "test_models", "birthday", :datetime + add_column "test_models", "favorite_day", :date + add_column "test_models", "moment_of_truth", :datetime + add_column "test_models", "male", :boolean + + TestModel.create :first_name => 'bob', :last_name => 'bobsen', + :bio => "I was born ....", :age => 18, :height => 1.78, + :wealth => BigDecimal.new("12345678901234567890.0123456789"), + :birthday => 18.years.ago, :favorite_day => 10.days.ago, + :moment_of_truth => "1782-10-10 21:40:18", :male => true + + bob = TestModel.first + assert_equal 'bob', bob.first_name + assert_equal 'bobsen', bob.last_name + assert_equal "I was born ....", bob.bio + assert_equal 18, bob.age + + # Test for 30 significant digits (beyond the 16 of float), 10 of them + # after the decimal place. + assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth - end - assert_equal true, bob.male? + assert_equal true, bob.male? - assert_equal String, bob.first_name.class - assert_equal String, bob.last_name.class - assert_equal String, bob.bio.class - assert_equal Fixnum, bob.age.class - assert_equal Time, bob.birthday.class + assert_equal String, bob.first_name.class + assert_equal String, bob.last_name.class + assert_equal String, bob.bio.class + assert_equal Fixnum, bob.age.class + assert_equal Time, bob.birthday.class - if current_adapter?(:OracleAdapter) - # Oracle doesn't differentiate between date/time - assert_equal Time, bob.favorite_day.class - else - assert_equal Date, bob.favorite_day.class - end + if current_adapter?(:OracleAdapter) + # Oracle doesn't differentiate between date/time + assert_equal Time, bob.favorite_day.class + else + assert_equal Date, bob.favorite_day.class + end - assert_instance_of TrueClass, bob.male? - assert_kind_of BigDecimal, bob.wealth + assert_instance_of TrueClass, bob.male? + assert_kind_of BigDecimal, bob.wealth + end end if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 4e6d7963aa..e6aa901814 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -66,6 +66,9 @@ module ActiveRecord def test_mysql_rename_column_preserves_auto_increment rename_column "test_models", "id", "id_test" assert_equal "auto_increment", connection.columns("test_models").find { |c| c.name == "id_test" }.extra + TestModel.reset_column_information + ensure + rename_column "test_models", "id_test", "id" end end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 1c0134843b..e955beae1a 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -270,6 +270,31 @@ module ActiveRecord enable = @recorder.inverse_of :disable_extension, ['uuid-ossp'] assert_equal [:enable_extension, ['uuid-ossp'], nil], enable end + + def test_invert_add_foreign_key + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people] + assert_equal [:remove_foreign_key, [:dogs, :people]], enable + end + + def test_invert_add_foreign_key_with_column + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"] + assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable + end + + def test_invert_add_foreign_key_with_column_and_name + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] + assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable + end + + def test_remove_foreign_key_is_irreversible + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"] + end + + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"] + end + end end end end diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 62b60f7f7b..bea9d6b2c9 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -119,6 +119,30 @@ module ActiveRecord assert !connection.tables.include?('artists_musics') end + + def test_create_and_drop_join_table_with_common_prefix + with_table_cleanup do + connection.create_join_table 'audio_artists', 'audio_musics' + assert_includes connection.tables, 'audio_artists_musics' + + connection.drop_join_table 'audio_artists', 'audio_musics' + assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't" + end + end + + private + + def with_table_cleanup + tables_before = connection.tables + + yield + ensure + tables_after = connection.tables - tables_before + + tables_after.each do |table| + connection.execute "DROP TABLE #{table}" + end + end end end end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb new file mode 100644 index 0000000000..c985092b4c --- /dev/null +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -0,0 +1,242 @@ +require 'cases/helper' +require 'support/ddl_helper' +require 'support/schema_dumping_helper' + +if ActiveRecord::Base.connection.supports_foreign_keys? +module ActiveRecord + class Migration + class ForeignKeyTest < ActiveRecord::TestCase + include DdlHelper + include SchemaDumpingHelper + + class Rocket < ActiveRecord::Base + end + + class Astronaut < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "rockets" do |t| + t.string :name + end + + @connection.create_table "astronauts" do |t| + t.string :name + t.references :rocket + end + end + + teardown do + if defined?(@connection) + @connection.execute "DROP TABLE IF EXISTS astronauts" + @connection.execute "DROP TABLE IF EXISTS rockets" + end + end + + def test_foreign_keys + foreign_keys = @connection.foreign_keys("fk_test_has_fk") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "fk_test_has_fk", fk.from_table + assert_equal "fk_test_has_pk", fk.to_table + assert_equal "fk_id", fk.column + assert_equal "pk_id", fk.primary_key + assert_equal "fk_name", fk.name + end + + def test_add_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_match(/^fk_rails_.{10}$/, fk.name) + end + + def test_add_foreign_key_with_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_match(/^fk_rails_.{10}$/, fk.name) + end + + def test_add_foreign_key_with_non_standard_primary_key + with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do + @connection.add_foreign_key(:astronauts, :space_shuttles, + column: "rocket_id", primary_key: "pk", name: "custom_pk") + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "space_shuttles", fk.to_table + assert_equal "pk", fk.primary_key + + @connection.remove_foreign_key :astronauts, name: "custom_pk" + end + end + + def test_add_on_delete_restrict_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + # ON DELETE RESTRICT is the default on MySQL + assert_equal nil, fk.on_delete + else + assert_equal :restrict, fk.on_delete + end + end + + def test_add_on_delete_cascade_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :cascade, fk.on_delete + end + + def test_add_on_delete_nullify_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.on_delete + end + + def test_on_update_and_on_delete_raises_with_invalid_values + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid + end + + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid + end + end + + def test_add_foreign_key_with_on_update + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.on_update + end + + def test_remove_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, :rockets + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, column: "rocket_id" + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, name: "fancy_named_fk" + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_non_existing_foreign_key_raises + assert_raises ArgumentError do + @connection.remove_foreign_key :astronauts, :rockets + end + end + + def test_schema_dumping + @connection.add_foreign_key :astronauts, :rockets + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output + end + + def test_schema_dumping_with_options + output = dump_table_schema "fk_test_has_fk" + assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output + end + + def test_schema_dumping_on_delete_and_on_update_options + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade + + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output + end + + class CreateCitiesAndHousesMigration < ActiveRecord::Migration + def change + create_table("cities") { |t| } + + create_table("houses") do |t| + t.column :city_id, :integer + end + add_foreign_key :houses, :cities, column: "city_id" + end + end + + def test_add_foreign_key_is_reversible + migration = CreateCitiesAndHousesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("houses").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + end + end + end +end +else +module ActiveRecord + class Migration + class NoForeignKeySupportTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + def test_add_foreign_key_should_be_noop + @connection.add_foreign_key :clubs, :categories + end + + def test_remove_foreign_key_should_be_noop + @connection.remove_foreign_key :clubs, :categories + end + + def test_foreign_keys_should_raise_not_implemented + assert_raises NotImplementedError do + @connection.foreign_keys("clubs") + end + end + end + end +end +end diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb index e28feedcf9..4dad77e8fd 100644 --- a/activerecord/test/cases/migration/helper.rb +++ b/activerecord/test/cases/migration/helper.rb @@ -22,7 +22,7 @@ module ActiveRecord super @connection = ActiveRecord::Base.connection connection.create_table :test_models do |t| - t.timestamps + t.timestamps null: true end TestModel.reset_column_information diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 93c3bfae7a..ac932378fd 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -95,6 +95,12 @@ module ActiveRecord assert connection.index_exists?(:testings, [:foo, :bar]) end + def test_index_exists_with_custom_name_checks_columns + connection.add_index :testings, [:foo, :bar], name: "my_index" + assert connection.index_exists?(:testings, [:foo, :bar], name: "my_index") + assert_not connection.index_exists?(:testings, [:foo], name: "my_index") + end + def test_valid_index_options assert_raise ArgumentError do connection.add_index :testings, :foo, unqiue: true diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb index eff000e1a4..7afac83bd2 100644 --- a/activerecord/test/cases/migration/pending_migrations_test.rb +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -6,16 +6,19 @@ module ActiveRecord class PendingMigrationsTest < ActiveRecord::TestCase def setup super - @connection = MiniTest::Mock.new - @app = MiniTest::Mock.new - @pending = CheckPending.new(@app, @connection) + @connection = Minitest::Mock.new + @app = Minitest::Mock.new + conn = @connection + @pending = Class.new(CheckPending) { + define_method(:connection) { conn } + }.new(@app) @pending.instance_variable_set :@last_check, -1 # Force checking end def teardown - super assert @connection.verify assert @app.verify + super end def test_errors_if_pending diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index e9545f2cce..b8b4fa1135 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -55,6 +55,11 @@ module ActiveRecord assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id') end + def test_creates_reference_id_with_specified_type + add_reference table_name, :user, type: :string + assert column_exists?(table_name, :user_id, :string) + end + def test_deletes_reference_id_column remove_reference table_name, :supplier assert_not column_exists?(table_name, :supplier_id, :integer) diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index a52b58c4ac..ba39fb1dec 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -76,6 +76,16 @@ module ActiveRecord assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq end + + def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences + enable_uuid_ossp!(connection) + connection.create_table :cats, id: :uuid + assert_nothing_raised { rename_table :cats, :felines } + assert connection.table_exists? :felines + ensure + connection.drop_table :cats if connection.table_exists? :cats + connection.drop_table :felines if connection.table_exists? :felines + end end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 6b840e16bb..11338e1fb6 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -63,7 +63,7 @@ class MigrationTest < ActiveRecord::TestCase end Person.connection.remove_column("people", "first_name") rescue nil Person.connection.remove_column("people", "middle_name") rescue nil - Person.connection.add_column("people", "first_name", :string, :limit => 40) + Person.connection.add_column("people", "first_name", :string) Person.reset_column_information end @@ -561,7 +561,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? t.string :qualification, :experience t.integer :age, :default => 0 t.date :birthdate - t.timestamps + t.timestamps null: true end end @@ -895,4 +895,14 @@ class CopyMigrationsTest < ActiveRecord::TestCase ensure ActiveRecord::Base.logger = old end + + private + + def quietly + silence_stream(STDOUT) do + silence_stream(STDERR) do + yield + end + end + end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 1192ecd6b4..2170fe6118 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -20,7 +20,7 @@ require 'models/toy' require 'rexml/document' class PersistenceTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :author_addresses, :categorizations, :categories, :posts, :minivans, :pets, :toys # Oracle UPDATE does not support ORDER BY unless current_adapter?(:OracleAdapter) @@ -865,4 +865,16 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal 1, post[:wibble] assert_nil post.reload[:wibble] end + + def test_find_via_reload + post = Post.new + + assert post.new_record? + + post.id = 1 + post.reload + + assert_equal "Welcome to the weblog", post.title + assert_not post.new_record? + end end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index f43483d291..f19a6ea5e3 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -5,6 +5,7 @@ require 'models/subscriber' require 'models/movie' require 'models/keyboard' require 'models/mixed_case_monkey' +require 'models/dashboard' class PrimaryKeysTest < ActiveRecord::TestCase fixtures :topics, :subscribers, :movies, :mixed_case_monkeys @@ -134,14 +135,22 @@ class PrimaryKeysTest < ActiveRecord::TestCase end def test_primary_key_returns_value_if_it_exists + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers' + end + if ActiveRecord::Base.connection.supports_primary_key? - assert_equal 'id', ActiveRecord::Base.connection.primary_key('developers') + assert_equal 'id', klass.primary_key end end def test_primary_key_returns_nil_if_it_does_not_exist + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers_projects' + end + if ActiveRecord::Base.connection.supports_primary_key? - assert_nil ActiveRecord::Base.connection.primary_key('developers_projects') + assert_nil klass.primary_key end end @@ -156,6 +165,15 @@ class PrimaryKeysTest < ActiveRecord::TestCase MixedCaseMonkey.reset_primary_key assert_equal "monkeyID", MixedCaseMonkey.primary_key end + + def test_primary_key_update_with_custom_key_name + dashboard = Dashboard.create!(dashboard_id: '1') + dashboard.id = '2' + dashboard.save! + + dashboard = Dashboard.first + assert_equal '2', dashboard.id + end end class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 70d9b9dbf5..1d6ae2f67f 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -83,12 +83,10 @@ module ActiveRecord def test_quote_with_quoted_id assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil) - assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), 'foo') end def test_quote_nil assert_equal 'NULL', @quoter.quote(nil, nil) - assert_equal 'NULL', @quoter.quote(nil, 'foo') end def test_quote_true @@ -102,48 +100,39 @@ module ActiveRecord def test_quote_float float = 1.2 assert_equal float.to_s, @quoter.quote(float, nil) - assert_equal float.to_s, @quoter.quote(float, Object.new) end def test_quote_fixnum fixnum = 1 assert_equal fixnum.to_s, @quoter.quote(fixnum, nil) - assert_equal fixnum.to_s, @quoter.quote(fixnum, Object.new) end def test_quote_bignum bignum = 1 << 100 assert_equal bignum.to_s, @quoter.quote(bignum, nil) - assert_equal bignum.to_s, @quoter.quote(bignum, Object.new) end def test_quote_bigdecimal bigdec = BigDecimal.new((1 << 100).to_s) assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, nil) - assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, Object.new) end def test_dates_and_times @quoter.extend(Module.new { def quoted_date(value) 'lol' end }) assert_equal "'lol'", @quoter.quote(Date.today, nil) - assert_equal "'lol'", @quoter.quote(Date.today, Object.new) assert_equal "'lol'", @quoter.quote(Time.now, nil) - assert_equal "'lol'", @quoter.quote(Time.now, Object.new) assert_equal "'lol'", @quoter.quote(DateTime.now, nil) - assert_equal "'lol'", @quoter.quote(DateTime.now, Object.new) end def test_crazy_object crazy = Class.new.new expected = "'#{YAML.dump(crazy)}'" assert_equal expected, @quoter.quote(crazy, nil) - assert_equal expected, @quoter.quote(crazy, Object.new) end def test_crazy_object_calls_quote_string crazy = Class.new { def initialize; @lol = 'lo\l' end }.new assert_match "lo\\\\l", @quoter.quote(crazy, nil) - assert_match "lo\\\\l", @quoter.quote(crazy, Object.new) end def test_quote_string_no_column diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index acbd065649..84abaf0291 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -87,7 +87,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_klass_for_nested_class_name - reflection = MacroReflection.new(:company, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) + reflection = ActiveRecord::Reflection.create(:has_many, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) assert_nothing_raised do assert_equal MyApplication::Business::Company, reflection.klass end @@ -97,21 +97,21 @@ class ReflectionTest < ActiveRecord::TestCase ActiveSupport::Inflector.inflections do |inflect| inflect.irregular 'plural_irregular', 'plurales_irregulares' end - reflection = AssociationReflection.new(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base) + reflection = ActiveRecord::Reflection.create(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base) assert_equal 'PluralIrregular', reflection.class_name end def test_aggregation_reflection reflection_for_address = AggregateReflection.new( - :composed_of, :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer + :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer ) reflection_for_balance = AggregateReflection.new( - :composed_of, :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer + :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer ) reflection_for_gps_location = AggregateReflection.new( - :composed_of, :gps_location, nil, { }, Customer + :gps_location, nil, { }, Customer ) assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location) @@ -135,7 +135,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_many_reflection - reflection_for_clients = AssociationReflection.new(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm) + reflection_for_clients = ActiveRecord::Reflection.create(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm) assert_equal reflection_for_clients, Firm.reflect_on_association(:clients) @@ -147,7 +147,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_one_reflection - reflection_for_account = AssociationReflection.new(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) + reflection_for_account = ActiveRecord::Reflection.create(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) assert_equal reflection_for_account, Firm.reflect_on_association(:account) assert_equal Account, Firm.reflect_on_association(:account).klass @@ -284,12 +284,12 @@ class ReflectionTest < ActiveRecord::TestCase end def test_association_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author) + reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } through = Class.new(ActiveRecord::Reflection::ThroughReflection) { define_method(:source_reflection) { reflection } - }.new(:fuu, :edge, nil, {}, Author) + }.new(reflection) assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key } end @@ -299,7 +299,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_active_record_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, nil, {}, Edge) + reflection = ActiveRecord::Reflection.create(:has_many, :author, nil, {}, Edge) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key } end @@ -317,32 +317,28 @@ class ReflectionTest < ActiveRecord::TestCase end def test_default_association_validation - assert AssociationReflection.new(:has_many, :clients, nil, {}, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate? - assert !AssociationReflection.new(:has_one, :client, nil, {}, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, nil, {}, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, {}, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm).validate? + assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm).validate? end def test_always_validate_association_if_explicit - assert AssociationReflection.new(:has_one, :client, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :validate => true }, Firm).validate? end def test_validate_association_if_autosave - assert AssociationReflection.new(:has_one, :client, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true }, Firm).validate? end def test_never_validate_association_if_explicit - assert !AssociationReflection.new(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? end def test_foreign_key @@ -364,11 +360,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) reflection.stubs(:klass).returns(category) assert_equal 'categories_products', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) reflection.stubs(:klass).returns(product) assert_equal 'categories_products', reflection.join_table end @@ -377,11 +373,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) reflection.stubs(:klass).returns(category) assert_equal 'catalog_categories_products', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) reflection.stubs(:klass).returns(product) assert_equal 'catalog_categories_products', reflection.join_table end @@ -390,11 +386,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, page) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page) reflection.stubs(:klass).returns(category) assert_equal 'catalog_categories_content_pages', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :pages, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category) reflection.stubs(:klass).returns(page) assert_equal 'catalog_categories_content_pages', reflection.join_table end @@ -403,15 +399,47 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, { :join_table => 'product_categories' }, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product) reflection.stubs(:klass).returns(category) assert_equal 'product_categories', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, { :join_table => 'product_categories' }, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category) reflection.stubs(:klass).returns(product) assert_equal 'product_categories', reflection.join_table end + def test_includes_accepts_symbols + hotel = Hotel.create! + department = hotel.departments.create! + department.chefs.create! + + assert_nothing_raised do + assert_equal department.chefs, Hotel.includes([departments: :chefs]).first.chefs + end + end + + def test_includes_accepts_strings + hotel = Hotel.create! + department = hotel.departments.create! + department.chefs.create! + + assert_nothing_raised do + assert_equal department.chefs, Hotel.includes(['departments' => 'chefs']).first.chefs + end + end + + def test_reflect_on_association_accepts_symbols + assert_nothing_raised do + assert_equal Hotel.reflect_on_association(:departments).name, :departments + end + end + + def test_reflect_on_association_accepts_strings + assert_nothing_raised do + assert_equal Hotel.reflect_on_association("departments").name, :departments + end + end + private def assert_reflection(klass, association, options) assert reflection = klass.reflect_on_association(association) diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 937f226b1d..b4804aa9d7 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -6,10 +6,11 @@ require 'models/post' require 'models/comment' require 'models/edge' require 'models/topic' +require 'models/binary' module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors + fixtures :posts, :edges, :authors, :binaries def test_where_copies_bind_params author = authors(:david) @@ -60,6 +61,15 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end + def test_belongs_to_nested_where_with_relation + author = authors(:david) + + expected = Author.where(id: author ).joins(:posts) + actual = Author.where(posts: { author_id: Author.where(id: author.id) }).joins(:posts) + + assert_equal expected.to_a, actual.to_a + end + def test_polymorphic_shallow_where treasure = Treasure.new treasure.id = 1 @@ -179,5 +189,35 @@ module ActiveRecord assert_equal 4, Edge.where(blank).order("sink_id").to_a.size end end + + def test_where_with_integer_for_string_column + count = Post.where(:title => 0).count + assert_equal 0, count + end + + def test_where_with_float_for_string_column + count = Post.where(:title => 0.0).count + assert_equal 0, count + end + + def test_where_with_boolean_for_string_column + count = Post.where(:title => false).count + assert_equal 0, count + end + + def test_where_with_decimal_for_string_column + count = Post.where(:title => BigDecimal.new(0)).count + assert_equal 0, count + end + + def test_where_with_duration_for_string_column + count = Post.where(:title => 0.seconds).count + assert_equal 0, count + end + + def test_where_with_integer_for_binary_column + count = Binary.where(:data => 0).count + assert_equal 0, count + end end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index fb0b906c07..3280945d09 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -235,5 +235,33 @@ module ActiveRecord posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length end + + class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value + def type + :string + end + + def type_cast_from_database(value) + raise value unless value == "type cast for database" + "type cast from database" + end + + def type_cast_for_database(value) + raise value unless value == "value from user" + "type cast for database" + end + end + + class UpdateAllTestModel < ActiveRecord::Base + self.table_name = 'posts' + + attribute :body, EnsureRoundTripTypeCasting.new + end + + def test_update_all_goes_through_normal_type_casting + UpdateAllTestModel.update_all(body: "value from user", type: nil) # No STI + + assert_equal "type cast from database", UpdateAllTestModel.first.body + end end end diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index 2131b32a0c..d6decafad9 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -10,7 +10,7 @@ module ActiveRecord ]) end - def test_to_hash_returns_row_hashes + test "to_hash returns row_hashes" do assert_equal [ {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'}, {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'}, @@ -18,13 +18,13 @@ module ActiveRecord ], result.to_hash end - def test_each_with_block_returns_row_hashes + test "each with block returns row hashes" do result.each do |row| assert_equal ['col_1', 'col_2'], row.keys end end - def test_each_without_block_returns_an_enumerator + test "each without block returns an enumerator" do result.each.with_index do |row, index| assert_equal ['col_1', 'col_2'], row.keys assert_kind_of Integer, index @@ -32,9 +32,45 @@ module ActiveRecord end if Enumerator.method_defined? :size - def test_each_without_block_returns_a_sized_enumerator + test "each without block returns a sized enumerator" do assert_equal 3, result.each.size end end + + test "cast_values returns rows after type casting" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new, "col2" => Type::Float.new } + result = Result.new(columns, values, types) + + assert_equal [[1, 2.2], [3, 4.4]], result.cast_values + end + + test "cast_values uses identity type for unknown types" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new } + result = Result.new(columns, values, types) + + assert_equal [[1, "2.2"], [3, "4.4"]], result.cast_values + end + + test "cast_values returns single dimensional array if single column" do + values = [["1.1"], ["3.3"]] + columns = ["col1"] + types = { "col1" => Type::Integer.new } + result = Result.new(columns, values, types) + + assert_equal [1, 3], result.cast_values + end + + test "cast_values can receive types to use instead" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new, "col2" => Type::Float.new } + result = Result.new(columns, values, types) + + assert_equal [[1.1, 2.2], [3.3, 4.4]], result.cast_values("col1" => Type::Float.new) + end end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 5f02d39e32..066e6b6468 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -2,6 +2,8 @@ require "cases/helper" require 'support/schema_dumping_helper' class SchemaDumperTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + setup do ActiveRecord::SchemaMigration.create_table end @@ -21,6 +23,8 @@ class SchemaDumperTest < ActiveRecord::TestCase schema_info = ActiveRecord::Base.connection.dump_schema_information assert_match(/20100201010101.*20100301010101/m, schema_info) + ensure + ActiveRecord::SchemaMigration.delete_all end def test_magic_comment @@ -372,15 +376,28 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{create_table "subscribers", id: false}, output end + if ActiveRecord::Base.connection.supports_foreign_keys? + def test_foreign_keys_are_dumped_at_the_bottom_to_circumvent_dependency_issues + output = standard_dump + assert_match(/^\s+add_foreign_key "fk_test_has_fk"[^\n]+\n\s+add_foreign_key "lessons_students"/, output) + end + end + class CreateDogMigration < ActiveRecord::Migration def up + create_table("dog_owners") do |t| + end + create_table("dogs") do |t| t.column :name, :string + t.column :owner_id, :integer end add_index "dogs", [:name] + add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys? end def down drop_table("dogs") + drop_table("dog_owners") end end @@ -396,13 +413,17 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "foo_.+_bar"}, output assert_no_match %r{add_index "foo_.+_bar"}, output assert_no_match %r{create_table "schema_migrations"}, output + + if ActiveRecord::Base.connection.supports_foreign_keys? + assert_no_match %r{add_foreign_key "foo_.+_bar"}, output + assert_no_match %r{add_foreign_key "[^"]+", "foo_.+_bar"}, output + end ensure migration.migrate(:down) ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' $stdout = original end - end class SchemaDumperDefaultsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index d8a467ec4d..8e512e118a 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -11,6 +11,10 @@ require 'models/reference' class RelationScopingTest < ActiveRecord::TestCase fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects + setup do + developers(:david) + end + def test_reverse_order assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order end @@ -260,7 +264,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase end end -class HasManyScopingTest< ActiveRecord::TestCase +class HasManyScopingTest < ActiveRecord::TestCase fixtures :comments, :posts, :people, :references def setup @@ -306,7 +310,7 @@ class HasManyScopingTest< ActiveRecord::TestCase end end -class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase +class HasAndBelongsToManyScopingTest < ActiveRecord::TestCase fixtures :posts, :categories, :categories_posts def setup diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 186a1a2ade..f8d87a3661 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -3,10 +3,11 @@ require 'models/topic' require 'models/reply' require 'models/person' require 'models/traffic_light' +require 'models/post' require 'bcrypt' class SerializedAttributeTest < ActiveRecord::TestCase - fixtures :topics + fixtures :topics, :posts MyObject = Struct.new :attribute1, :attribute2 @@ -67,6 +68,40 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal(orig.content, clone.content) end + def test_serialized_json_attribute_returns_unserialized_value + Topic.serialize :content, JSON + my_post = posts(:welcome) + + t = Topic.new(content: my_post) + t.save! + t.reload + + assert_instance_of(Hash, t.content) + assert_equal(my_post.id, t.content["id"]) + assert_equal(my_post.title, t.content["title"]) + end + + def test_json_read_legacy_null + Topic.serialize :content, JSON + + # Force a row to have a JSON "null" instead of a database NULL (this is how + # null values are saved on 4.1 and before) + id = Topic.connection.insert "INSERT INTO topics (content) VALUES('null')" + t = Topic.find(id) + + assert_nil t.content + end + + def test_json_read_db_null + Topic.serialize :content, JSON + + # Force a row to have a database NULL instead of a JSON "null" + id = Topic.connection.insert "INSERT INTO topics (content) VALUES(NULL)" + t = Topic.find(id) + + assert_nil t.content + end + def test_serialized_attribute_declared_in_subclass hash = { 'important1' => 'value1', 'important2' => 'value2' } important_topic = ImportantTopic.create("important" => hash) diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 0f48c8d5fc..01d373b691 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -273,6 +273,19 @@ module ActiveRecord end end + class DatabaseTasksMigrateTest < ActiveRecord::TestCase + def test_migrate_receives_correct_env_vars + verbose, version = ENV['VERBOSE'], ENV['VERSION'] + + ENV['VERBOSE'] = 'false' + ENV['VERSION'] = '4' + + ActiveRecord::Migrator.expects(:migrate).with(ActiveRecord::Migrator.migrations_paths, 4) + ActiveRecord::Tasks::DatabaseTasks.migrate + ensure + ENV['VERBOSE'], ENV['VERSION'] = verbose, version + end + end class DatabaseTasksPurgeTest < ActiveRecord::TestCase include DatabaseTasksSetupper diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 3e3a2828f3..f58535f044 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' +if current_adapter?(:MysqlAdapter, :Mysql2Adapter) module ActiveRecord class MysqlDBCreateTest < ActiveRecord::TestCase def setup @@ -196,8 +197,8 @@ module ActiveRecord ActiveRecord::Base.stubs(:establish_connection).returns(true) end - def test_establishes_connection_to_test_database - ActiveRecord::Base.expects(:establish_connection).with(:test) + def test_establishes_connection_to_the_appropriate_database + ActiveRecord::Base.expects(:establish_connection).with(@configuration) ActiveRecord::Tasks::DatabaseTasks.purge @configuration end @@ -307,3 +308,4 @@ module ActiveRecord end end +end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 6ea225178f..0d574d071c 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' +if current_adapter?(:PostgreSQLAdapter) module ActiveRecord class PostgreSQLDBCreateTest < ActiveRecord::TestCase def setup @@ -241,3 +242,4 @@ module ActiveRecord end end +end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index da3471adf9..750d5e42dc 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' require 'pathname' +if current_adapter?(:SQLite3Adapter) module ActiveRecord class SqliteDBCreateTest < ActiveRecord::TestCase def setup @@ -189,3 +190,4 @@ module ActiveRecord end end end +end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index b6c5511849..4070216733 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -13,6 +13,23 @@ module ActiveRecord assert_equal expected.to_s, actual.to_s, message end + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) + end + def capture_sql SQLCounter.clear_log yield @@ -76,7 +93,7 @@ module ActiveRecord # ignored SQL, or better yet, use a different notification for the queries # instead examining the SQL content. oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im] - mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i] + mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /] postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im] diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 0472246f71..abf6becc17 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'support/ddl_helper' require 'models/developer' require 'models/owner' require 'models/pet' @@ -424,3 +425,21 @@ class TimestampTest < ActiveRecord::TestCase assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model) end end + +class TimestampsWithoutTransactionTest < ActiveRecord::TestCase + include DdlHelper + self.use_transactional_fixtures = false + + class TimestampAttributePost < ActiveRecord::Base + attr_accessor :created_at, :updated_at + end + + def test_do_not_write_timestamps_on_save_if_they_are_not_attributes + with_example_table ActiveRecord::Base.connection, "timestamp_attribute_posts", "id integer primary key" do + post = TimestampAttributePost.new(id: 1) + post.save! # should not try to assign and persist created_at, updated_at + assert_nil post.created_at + assert_nil post.updated_at + end + end +end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 3d64ecb464..a3f39804b7 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -129,6 +129,19 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal [:commit_on_update], @first.history end + def test_only_call_after_commit_on_top_level_transactions + @first.after_commit_block{|r| r.history << :after_commit} + assert @first.history.empty? + + @first.transaction do + @first.transaction(requires_new: true) do + @first.touch + end + assert @first.history.empty? + end + assert_equal [:after_commit], @first.history + end + def test_call_after_rollback_after_transaction_rollsback @first.after_commit_block{|r| r.history << :after_commit} @first.after_rollback_block{|r| r.history << :after_rollback} diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index de1f624191..9cfe041de2 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -80,6 +80,30 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_number_of_transactions_in_commit + num = nil + + Topic.connection.class_eval do + alias :real_commit_db_transaction :commit_db_transaction + define_method(:commit_db_transaction) do + num = transaction_manager.open_transactions + real_commit_db_transaction + end + end + + Topic.transaction do + @first.approved = true + @first.save! + end + + assert_equal 0, num + ensure + Topic.connection.class_eval do + remove_method :commit_db_transaction + alias :commit_db_transaction :real_commit_db_transaction rescue nil + end + end + def test_successful_with_instance_method @first.transaction do @first.approved = true @@ -424,6 +448,26 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_savepoints_name + Topic.transaction do + assert_nil Topic.connection.current_savepoint_name + assert_nil Topic.connection.current_transaction.savepoint_name + + Topic.transaction(requires_new: true) do + assert_equal "active_record_1", Topic.connection.current_savepoint_name + assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name + + Topic.transaction(requires_new: true) do + assert_equal "active_record_2", Topic.connection.current_savepoint_name + assert_equal "active_record_2", Topic.connection.current_transaction.savepoint_name + end + + assert_equal "active_record_1", Topic.connection.current_savepoint_name + assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name + end + end + end + def test_rollback_when_commit_raises Topic.connection.expects(:begin_db_transaction) Topic.connection.expects(:commit_db_transaction).raises('OH NOES') @@ -526,13 +570,13 @@ class TransactionTest < ActiveRecord::TestCase def test_transactions_state_from_rollback connection = Topic.connection - transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin + transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction assert transaction.open? assert !transaction.state.rolledback? assert !transaction.state.committed? - transaction.perform_rollback + transaction.rollback assert transaction.state.rolledback? assert !transaction.state.committed? @@ -540,13 +584,13 @@ class TransactionTest < ActiveRecord::TestCase def test_transactions_state_from_commit connection = Topic.connection - transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin + transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction assert transaction.open? assert !transaction.state.rolledback? assert !transaction.state.committed? - transaction.perform_commit + transaction.commit assert !transaction.state.rolledback? assert transaction.state.committed? diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb new file mode 100644 index 0000000000..da30de373e --- /dev/null +++ b/activerecord/test/cases/type/decimal_test.rb @@ -0,0 +1,38 @@ +require "cases/helper" + +module ActiveRecord + module Type + class DecimalTest < ActiveRecord::TestCase + def test_type_cast_decimal + type = Decimal.new + assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0")) + assert_equal BigDecimal.new("123"), type.type_cast_from_user(123.0) + assert_equal BigDecimal.new("1"), type.type_cast_from_user(:"1") + end + + def test_type_cast_decimal_from_float_with_large_precision + type = Decimal.new(precision: ::Float::DIG + 2) + assert_equal BigDecimal.new("123.0"), type.type_cast_from_user(123.0) + end + + def test_type_cast_decimal_from_rational_with_precision + type = Decimal.new(precision: 2) + assert_equal BigDecimal("0.33"), type.type_cast_from_user(Rational(1, 3)) + end + + def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36 + type = Decimal.new + assert_equal BigDecimal("0.333333333333333333E0"), type.type_cast_from_user(Rational(1, 3)) + end + + def test_type_cast_decimal_from_object_responding_to_d + value = Object.new + def value.to_d + BigDecimal.new("1") + end + type = Decimal.new + assert_equal BigDecimal("1"), type.type_cast_from_user(value) + end + end + end +end diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb new file mode 100644 index 0000000000..420177ed49 --- /dev/null +++ b/activerecord/test/cases/type/string_test.rb @@ -0,0 +1,36 @@ +require 'cases/helper' + +module ActiveRecord + class StringTypeTest < ActiveRecord::TestCase + test "type casting" do + type = Type::String.new + assert_equal "1", type.type_cast_from_user(true) + assert_equal "0", type.type_cast_from_user(false) + assert_equal "123", type.type_cast_from_user(123) + end + + test "values are duped coming out" do + s = "foo" + type = Type::String.new + assert_not_same s, type.type_cast_from_user(s) + assert_not_same s, type.type_cast_from_database(s) + end + + test "string mutations are detected" do + klass = Class.new(Base) + klass.table_name = 'authors' + + author = klass.create!(name: 'Sean') + assert_not author.changed? + + author.name << ' Griffin' + assert author.name_changed? + + author.save! + author.reload + + assert_equal 'Sean Griffin', author.name + assert_not author.changed? + end + end +end diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb index 961aae88cb..db4f78d354 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -35,13 +35,6 @@ module ActiveRecord assert_equal false, type.type_cast_from_user('SOMETHING RANDOM') end - def test_type_cast_string - type = Type::String.new - assert_equal "1", type.type_cast_from_user(true) - assert_equal "0", type.type_cast_from_user(false) - assert_equal "123", type.type_cast_from_user(123) - end - def test_type_cast_integer type = Type::Integer.new assert_equal 1, type.type_cast_from_user(1) @@ -102,13 +95,6 @@ module ActiveRecord assert_not type.changed?(nil, nil, nil) end - def test_type_cast_decimal - type = Type::Decimal.new - assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0")) - assert_equal BigDecimal.new("123"), type.type_cast_from_user(123.0) - assert_equal BigDecimal.new("1"), type.type_cast_from_user(:"1") - end - def test_type_cast_binary type = Type::Binary.new assert_equal nil, type.type_cast_from_user(nil) @@ -163,6 +149,12 @@ module ActiveRecord end end + def test_type_equality + assert_equal Type::Value.new, Type::Value.new + assert_not_equal Type::Value.new, Type::Integer.new + assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2) + end + if current_adapter?(:SQLite3Adapter) def test_binary_encoding type = SQLite3Binary.new diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 3db742c15b..268d7914b5 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -6,6 +6,7 @@ class I18nValidationTest < ActiveRecord::TestCase repair_validations(Topic, Reply) def setup + repair_validations(Topic, Reply) Reply.validates_presence_of(:title) @topic = Topic.new @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 3790d3c8cf..4f38849131 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -52,14 +52,15 @@ class PresenceValidationTest < ActiveRecord::TestCase end def test_validates_presence_doesnt_convert_to_array - Speedometer.validates_presence_of :dashboard + speedometer = Class.new(Speedometer) + speedometer.validates_presence_of :dashboard dash = Dashboard.new # dashboard has to_a method def dash.to_a; ['(/)', '(\)']; end - s = Speedometer.new + s = speedometer.new s.dashboard = dash assert_nothing_raised { s.valid? } diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb index c02b3241cd..2bbf0f23b3 100644 --- a/activerecord/test/cases/validations_repair_helper.rb +++ b/activerecord/test/cases/validations_repair_helper.rb @@ -13,7 +13,7 @@ module ActiveRecord end def repair_validations(*model_classes) - yield + yield if block_given? ensure model_classes.each do |k| k.clear_validators! diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index a6e1dc72e5..55804f9576 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -78,6 +78,20 @@ class ValidationsTest < ActiveRecord::TestCase assert_equal r, invalid.record end + def test_validate_with_bang + assert_raise(ActiveRecord::RecordInvalid) do + WrongReply.new.validate! + end + end + + def test_validate_with_bang_and_context + assert_raise(ActiveRecord::RecordInvalid) do + WrongReply.new.validate!(:special_case) + end + r = WrongReply.new(:title => "Valid title", :author_name => "secret", :content => "Good") + assert r.validate!(:special_case) + end + def test_exception_on_create_bang_many assert_raise(ActiveRecord::RecordInvalid) do WrongReply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }]) diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 9f1d110ddb..bce59b4fcd 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' require 'models/topic' +require 'models/reply' require 'models/post' require 'models/author' diff --git a/activerecord/test/fixtures/fk_test_has_pk.yml b/activerecord/test/fixtures/fk_test_has_pk.yml index c93952180b..73882bac41 100644 --- a/activerecord/test/fixtures/fk_test_has_pk.yml +++ b/activerecord/test/fixtures/fk_test_has_pk.yml @@ -1,2 +1,2 @@ first: - id: 1
\ No newline at end of file + pk_id: 1
\ No newline at end of file diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml index 7298096025..86d46f753a 100644 --- a/activerecord/test/fixtures/posts.yml +++ b/activerecord/test/fixtures/posts.yml @@ -4,7 +4,6 @@ welcome: title: Welcome to the weblog body: Such a lovely day comments_count: 2 - taggings_count: 1 tags_count: 1 type: Post @@ -14,7 +13,6 @@ thinking: title: So I was thinking body: Like I hopefully always am comments_count: 1 - taggings_count: 1 tags_count: 1 type: SpecialPost diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index a1cb8d62b6..3ea17c3abf 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -8,6 +8,7 @@ module ContactFakeColumns table_name => 'id' } + column :id, :integer column :name, :string column :age, :integer column :avatar, :binary diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index 3d7f0626e2..91e46f83e5 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -1,7 +1,8 @@ class Face < ActiveRecord::Base belongs_to :man, :inverse_of => :face belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face - belongs_to :polymorphic_man_without_inverse, :polymorphic => true + # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly` + belongs_to :poly_man_without_inverse, :polymorphic => true # These is a "broken" inverse_of for the purposes of testing belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb index a26491ce61..4fbb6b226b 100644 --- a/activerecord/test/models/man.rb +++ b/activerecord/test/models/man.rb @@ -1,7 +1,7 @@ class Man < ActiveRecord::Base has_one :face, :inverse_of => :man has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man - has_one :polymorphic_face_without_inverse, :class_name => 'Face', :as => :polymorphic_man_without_inverse + has_one :polymorphic_face_without_inverse, :class_name => 'Face', :as => :poly_man_without_inverse has_many :interests, :inverse_of => :man has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man # These are "broken" inverse_of associations for the purposes of testing diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 5f01ab0a82..a29858213b 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -88,7 +88,7 @@ class Post < ActiveRecord::Base has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' - has_many :taggings, :as => :taggable + has_many :taggings, :as => :taggable, :counter_cache => :tags_count has_many :tags, :through => :taggings do def add_joins_and_select select('tags.*, authors.id as author_id') diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index f91f2ad2e9..a6c05da26a 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -8,6 +8,6 @@ class Tagging < ActiveRecord::Base belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id' belongs_to :blue_tag, -> { where :tags => { :name => 'Blue' } }, :class_name => 'Tag', :foreign_key => :tag_id belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key - belongs_to :taggable, :polymorphic => true, :counter_cache => true + belongs_to :taggable, :polymorphic => true, :counter_cache => :tags_count has_many :things, :through => :taggable end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index fd85050dd4..98f2492ef8 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -84,6 +84,8 @@ ActiveRecord::Schema.define do create_table :author_addresses, force: true do |t| end + add_foreign_key :authors, :author_addresses + create_table :author_favorites, force: true do |t| t.column :author_id, :integer t.column :favorite_author_id, :integer @@ -136,7 +138,7 @@ ActiveRecord::Schema.define do t.integer :engines_count t.integer :wheels_count t.column :lock_version, :integer, null: false, default: 0 - t.timestamps + t.timestamps null: false end create_table :categories, force: true do |t| @@ -190,7 +192,7 @@ ActiveRecord::Schema.define do t.text :body, null: false end t.string :type - t.integer :taggings_count, default: 0 + t.integer :tags_count, default: 0 t.integer :children_count, default: 0 t.integer :parent_id t.references :author, polymorphic: true @@ -535,7 +537,7 @@ ActiveRecord::Schema.define do t.references :best_friend_of t.integer :insures, null: false, default: 0 t.timestamp :born_at - t.timestamps + t.timestamps null: false end create_table :peoples_treasures, id: false, force: true do |t| @@ -546,7 +548,7 @@ ActiveRecord::Schema.define do create_table :pets, primary_key: :pet_id, force: true do |t| t.string :name t.integer :owner_id, :integer - t.timestamps + t.timestamps null: false end create_table :pirates, force: true do |t| @@ -569,7 +571,6 @@ ActiveRecord::Schema.define do end t.string :type t.integer :comments_count, default: 0 - t.integer :taggings_count, default: 0 t.integer :taggings_with_delete_all_count, default: 0 t.integer :taggings_with_destroy_count, default: 0 t.integer :tags_count, default: 0 @@ -725,13 +726,13 @@ ActiveRecord::Schema.define do t.string :parent_title t.string :type t.string :group - t.timestamps + t.timestamps null: true end create_table :toys, primary_key: :toy_id, force: true do |t| t.string :name t.integer :pet_id, :integer - t.timestamps + t.timestamps null: false end create_table :traffic_lights, force: true do |t| @@ -780,8 +781,8 @@ ActiveRecord::Schema.define do t.integer :man_id t.integer :polymorphic_man_id t.string :polymorphic_man_type - t.integer :polymorphic_man_without_inverse_id - t.string :polymorphic_man_without_inverse_type + t.integer :poly_man_without_inverse_id + t.string :poly_man_without_inverse_type t.integer :horrible_polymorphic_man_id t.string :horrible_polymorphic_man_type end @@ -856,12 +857,11 @@ ActiveRecord::Schema.define do t.integer :fk_id, null: false end - create_table :fk_test_has_pk, force: true do |t| + create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t| end - execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})" - - execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})" + add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" + add_foreign_key :lessons_students, :students end create_table :overloaded_types, force: true do |t| diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index b7aff4f47d..b5552c2755 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -7,7 +7,7 @@ ActiveRecord::Schema.define do execute "DROP TABLE fk_test_has_pk" rescue nil execute <<_SQL CREATE TABLE 'fk_test_has_pk' ( - 'id' INTEGER NOT NULL PRIMARY KEY + 'pk_id' INTEGER NOT NULL PRIMARY KEY ); _SQL @@ -16,7 +16,7 @@ _SQL 'id' INTEGER NOT NULL PRIMARY KEY, 'fk_id' INTEGER NOT NULL, - FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('id') + FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id') ); _SQL -end
\ No newline at end of file +end diff --git a/activerecord/test/support/ddl_helper.rb b/activerecord/test/support/ddl_helper.rb index 0107babaaf..43cb235e01 100644 --- a/activerecord/test/support/ddl_helper.rb +++ b/activerecord/test/support/ddl_helper.rb @@ -1,8 +1,8 @@ module DdlHelper def with_example_table(connection, table_name, definition = nil) - connection.exec_query("CREATE TABLE #{table_name}(#{definition})") + connection.execute("CREATE TABLE #{table_name}(#{definition})") yield ensure - connection.exec_query("DROP TABLE #{table_name}") + connection.execute("DROP TABLE #{table_name}") end end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index d0a5ddcc1e..96bce53999 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,8 +1,82 @@ +* Fix rounding errors with #travel_to by resetting the usec on any passed time to zero, so we only travel + with per-second precision, not anything deeper than that. + + *DHH* + +* Fix ActiveSupport::TestCase not to order users' test cases by default. + If this change breaks your tests because your tests are order dependent, you need to explicitly call + ActiveSupport::TestCase.my_tests_are_order_dependent! at the top of your tests. + + *Akira Matsuda* + +* Fix DateTime comparison with DateTime::Infinity object. + + *Rafael Mendonça França* + +* Added Object#itself which returns the object itself. Useful when dealing with a chaining scenario, like Active Record scopes: + + Event.public_send(state.presence_in([ :trashed, :drafted ]) || :itself).order(:created_at) + + *DHH* + +* `Object#with_options` executes block in merging option context when + explicit receiver in not passed. + + *Pavel Pravosud* + +* Fixed a compatibility issue with the `Oj` gem when cherry-picking the file + `active_support/core_ext/object/json` without requiring `active_support/json`. + + Fixes #16131. + + *Godfrey Chan* + +* Make `Hash#with_indifferent_access` copy the default proc too. + + *arthurnn*, *Xanders* + +* Add `String#truncate_words` to truncate a string by a number of words. + + *Mohamed Osama* + +* Deprecate `capture` and `quietly`. + + These methods are not thread safe and may cause issues when used in threaded environments. + To avoid problems we are deprecating them. + + *Tom Meier* + +* `DateTime#to_f` now preserves the fractional seconds instead of always + rounding to `.0`. + + Fixes #15994. + + *John Paul Ashenfelter* + +* Add `Hash#transform_values` to simplify a common pattern where the values of a + hash must change, but the keys are left the same. + + *Sean Griffin* + +* Always instrument `ActiveSupport::Cache`. + + Since `ActiveSupport::Notifications` only instrument items when there + are subscriber we don't need to disable instrumentation. + + *Peter Wagenet* + +* Make the `apply_inflections` method case-insensitive when checking + whether a word is uncountable or not. + + *Robin Dupret* + * Make Dependencies pass a name to NameError error. + *arthurnn* * Fixed `ActiveSupport::Cache::FileStore` exploding with long paths. - *Adam Panzer / Michael Grosser* + + *Adam Panzer / Michael Grosser* * Fixed `ActiveSupport::TimeWithZone#-` so precision is not unnecessarily lost when working with objects with a nanosecond component. @@ -27,12 +101,12 @@ * Fixed precision error in NumberHelper when using Rationals. Before: - + ActiveSupport::NumberHelper.number_to_rounded Rational(1000, 3), precision: 2 #=> "330.00" - + After: - + ActiveSupport::NumberHelper.number_to_rounded Rational(1000, 3), precision: 2 #=> "333.33" @@ -127,7 +201,7 @@ *Bogdan Gusiev* -* Add `SecureRandom::uuid_v3` and `SecureRandom::uuid_v5` to support stable +* Add `Digest::UUID::uuid_v3` and `Digest::UUID::uuid_v5` to support stable UUID fixtures on PostgreSQL. *Roderick van Domburg* diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index f3625e8b79..c0b457c341 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |s| s.rdoc_options.concat ['--encoding', 'UTF-8'] - s.add_dependency 'i18n', '~> 0.6', '>= 0.6.9' + s.add_dependency 'i18n', '>= 0.7.0.dev', '< 0.8' s.add_dependency 'json', '~> 1.7', '>= 1.7.7' s.add_dependency 'tzinfo', '~> 1.1' s.add_dependency 'minitest', '~> 5.1' diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index 1fec1bea0d..d06f22ad5c 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -13,7 +13,7 @@ module ActiveSupport # can focus on the rest. # # bc = BacktraceCleaner.new - # bc.add_filter { |line| line.gsub(Rails.root, '') } # strip the Rails.root prefix + # bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix # bc.add_silencer { |line| line =~ /mongrel|rubygems/ } # skip any lines from mongrel or rubygems # bc.clean(exception.backtrace) # perform the cleanup # diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index a627fa8651..ff67a6828c 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -8,6 +8,7 @@ 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/deprecation' module ActiveSupport # See ActiveSupport::Cache::Store for documentation. @@ -178,14 +179,16 @@ module ActiveSupport @silence = previous_silence end - # Set to +true+ if cache stores should be instrumented. - # Default is +false+. + # :deprecated: def self.instrument=(boolean) - Thread.current[:instrument_cache_store] = boolean + ActiveSupport::Deprecation.warn "ActiveSupport::Cache.instrument= is deprecated and will be removed in Rails 5. Instrumentation is now always on so you can safely stop using it." + true end + # :deprecated: def self.instrument - Thread.current[:instrument_cache_store] || false + ActiveSupport::Deprecation.warn "ActiveSupport::Cache.instrument is deprecated and will be removed in Rails 5. Instrumentation is now always on so you can safely stop using it." + true end # Fetches data from the cache, using the given key. If there is data in @@ -234,7 +237,7 @@ module ActiveSupport # seconds. Because of extended life of the previous cache, other processes # will continue to use slightly stale data for a just a bit longer. In the # meantime that first process will go ahead and will write into cache the - # new value. After that all the processes will start getting new value. + # new value. After that all the processes will start getting the new value. # The key is to keep <tt>:race_condition_ttl</tt> small. # # If the process regenerating the entry errors out, the entry will be @@ -539,13 +542,9 @@ module ActiveSupport def instrument(operation, key, options = nil) log(operation, key, options) - if self.class.instrument - payload = { :key => key } - payload.merge!(options) if options.is_a?(Hash) - ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) } - else - yield(nil) - end + payload = { :key => key } + payload.merge!(options) if options.is_a?(Hash) + ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) } end def log(operation, key, options = nil) diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 06505bddf9..cd467e13f6 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -71,7 +71,8 @@ module ActiveSupport # order. # # If the callback chain was halted, returns +false+. Otherwise returns the - # result of the block, or +true+ if no block is given. + # result of the block, +nil+ if no callbacks have been set, or +true+ + # if callbacks have been set but no block is given. # # run_callbacks :save do # save @@ -415,15 +416,8 @@ module ActiveSupport # Procs:: A proc to call with the object. # Objects:: An object with a <tt>before_foo</tt> method on it to call. # - # All of these objects are compiled into methods and handled - # the same after this point: - # - # Symbols:: Already methods. - # Strings:: class_eval'd into methods. - # Procs:: using define_method compiled into methods. - # Objects:: - # a method is created that calls the before_foo method - # on the object. + # All of these objects are converted into a lambda and handled + # the same after this point. def make_lambda(filter) case filter when Symbol @@ -572,7 +566,7 @@ module ActiveSupport # # set_callback :save, :before, :before_meth # set_callback :save, :after, :after_meth, if: :condition - # set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff } + # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff } # # The second arguments indicates whether the callback is to be run +:before+, # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index 3529d57174..87ae052eb0 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -18,6 +18,11 @@ class Array # ["3", "4"] # ["5"] def in_groups_of(number, fill_with = nil) + if number.to_i <= 0 + raise ArgumentError, + "Group size must be a positive integer, was #{number.inspect}" + end + if fill_with == false collection = self else diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index 289ca12b5e..dc4e767e9d 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -161,7 +161,9 @@ class DateTime # Layers additional behavior on DateTime#<=> so that Time and # ActiveSupport::TimeWithZone instances can be compared with a DateTime. def <=>(other) - if other.respond_to? :to_datetime + if other.kind_of?(Infinity) + super + elsif other.respond_to? :to_datetime super other.to_datetime else nil diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb index 6ddfb72a0d..2a9c09fc29 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -71,9 +71,9 @@ class DateTime civil(year, month, day, hour, min, sec, offset) end - # Converts +self+ to a floating-point number of seconds since the Unix epoch. + # Converts +self+ to a floating-point number of seconds, including fractional microseconds, since the Unix epoch. def to_f - seconds_since_unix_epoch.to_f + seconds_since_unix_epoch.to_f + sec_fraction end # Converts +self+ to an integer number of seconds since the Unix epoch. diff --git a/activesupport/lib/active_support/core_ext/digest/uuid.rb b/activesupport/lib/active_support/core_ext/digest/uuid.rb new file mode 100644 index 0000000000..593c51bba2 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/digest/uuid.rb @@ -0,0 +1,51 @@ +require 'securerandom' + +module Digest + module UUID + DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + + # Generates a v5 non-random UUID (Universally Unique IDentifier). + # + # Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs. + # uuid_from_hash always generates the same UUID for a given name and namespace combination. + # + # See RFC 4122 for details of UUID at: http://www.ietf.org/rfc/rfc4122.txt + def self.uuid_from_hash(hash_class, uuid_namespace, name) + if hash_class == Digest::MD5 + version = 3 + elsif hash_class == Digest::SHA1 + version = 5 + else + raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}." + end + + hash = hash_class.new + hash.update(uuid_namespace) + hash.update(name) + + ary = hash.digest.unpack('NnnnnN') + ary[2] = (ary[2] & 0x0FFF) | (version << 12) + ary[3] = (ary[3] & 0x3FFF) | 0x8000 + + "%08x-%04x-%04x-%04x-%04x%08x" % ary + end + + # Convenience method for uuid_from_hash using Digest::MD5. + def self.uuid_v3(uuid_namespace, name) + self.uuid_from_hash(Digest::MD5, uuid_namespace, name) + end + + # Convenience method for uuid_from_hash using Digest::SHA1. + def self.uuid_v5(uuid_namespace, name) + self.uuid_from_hash(Digest::SHA1, uuid_namespace, name) + end + + # Convenience method for SecureRandom.uuid. + def self.uuid_v4 + SecureRandom.uuid + end + end +end diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index f68e1662f9..af4d1da0eb 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -6,3 +6,4 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/transform_values' diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index 8657f34be2..f4105f66b0 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -6,7 +6,8 @@ class Hash # hash.transform_keys{ |key| key.to_s.upcase } # # => {"NAME"=>"Rob", "AGE"=>"28"} def transform_keys - result = {} + return enum_for(:transform_keys) unless block_given? + result = self.class.new each_key do |key| result[yield(key)] = self[key] end @@ -16,6 +17,7 @@ class Hash # Destructively convert all keys using the block operations. # Same as transform_keys but modifies +self+. def transform_keys! + return enum_for(:transform_keys!) unless block_given? keys.each do |key| self[yield(key)] = delete(key) end diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb new file mode 100644 index 0000000000..e9bcce761f --- /dev/null +++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb @@ -0,0 +1,23 @@ +class Hash + # Returns a new hash with the results of running +block+ once for every value. + # The keys are unchanged. + # + # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } + # # => { a: 2, b: 4, c: 6 } + def transform_values + return enum_for(:transform_values) unless block_given? + result = self.class.new + each do |key, value| + result[key] = yield(value) + end + result + end + + # Destructive +transform_values+ + def transform_values! + return enum_for(:transform_values!) unless block_given? + each do |key, value| + self[key] = yield(value) + end + end +end diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index f3f8416905..80c531b694 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -31,9 +31,13 @@ module Kernel # For compatibility def silence_stderr #:nodoc: + ActiveSupport::Deprecation.warn( + "#silence_stderr is deprecated and will be removed in the next release" + ) #not thread-safe silence_stream(STDERR) { yield } end + # Deprecated : this method is not thread safe # Silences any stream for the duration of the block. # # silence_stream(STDOUT) do @@ -82,6 +86,9 @@ module Kernel # stream = capture(:stderr) { system('echo error 1>&2') } # stream # => "error\n" def capture(stream) + ActiveSupport::Deprecation.warn( + "#capture(stream) is deprecated and will be removed in the next release" + ) #not thread-safe stream = stream.to_s captured_stream = Tempfile.new(stream) stream_io = eval("$#{stream}") @@ -105,6 +112,9 @@ module Kernel # # This method is not thread-safe. def quietly + ActiveSupport::Deprecation.warn( + "#quietly is deprecated and will be removed in the next release" + ) #not thread-safe silence_stream(STDOUT) do silence_stream(STDERR) do yield diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb index f4f9152d6a..f1106cca9b 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/object/acts_like' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/object/deep_dup' +require 'active_support/core_ext/object/itself' require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/inclusion' diff --git a/activesupport/lib/active_support/core_ext/object/itself.rb b/activesupport/lib/active_support/core_ext/object/itself.rb new file mode 100644 index 0000000000..adedc20169 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/object/itself.rb @@ -0,0 +1,12 @@ +class Object + unless respond_to?(:itself) # TODO: Remove this file when we drop support for Ruby < 2.2 + # Returns the object itself. Useful when dealing with a chaining scenario, like Active Record scopes: + # + # Event.public_send(state.presence_in([ :trashed, :drafted ]) || :itself).order(:created_at) + # + # @return Object + def itself + self + end + end +end diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb index 5496692373..698b2d1920 100644 --- a/activesupport/lib/active_support/core_ext/object/json.rb +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -162,7 +162,7 @@ end class Time def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format + if ActiveSupport::JSON::Encoding.use_standard_json_time_format xmlschema(ActiveSupport::JSON::Encoding.time_precision) else %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) @@ -172,7 +172,7 @@ end class Date def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format + if ActiveSupport::JSON::Encoding.use_standard_json_time_format strftime("%Y-%m-%d") else strftime("%Y/%m/%d") @@ -182,7 +182,7 @@ end class DateTime def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format + if ActiveSupport::JSON::Encoding.use_standard_json_time_format xmlschema(ActiveSupport::JSON::Encoding.time_precision) else strftime('%Y/%m/%d %H:%M:%S %z') diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb index e65fc5bac1..684d4ef57e 100644 --- a/activesupport/lib/active_support/core_ext/object/to_param.rb +++ b/activesupport/lib/active_support/core_ext/object/to_param.rb @@ -1,60 +1 @@ -class Object - # Alias of <tt>to_s</tt>. - def to_param - to_s - end -end - -class NilClass - # Returns +self+. - def to_param - self - end -end - -class TrueClass - # Returns +self+. - def to_param - self - end -end - -class FalseClass - # Returns +self+. - def to_param - self - end -end - -class Array - # Calls <tt>to_param</tt> on all its elements and joins the result with - # slashes. This is used by <tt>url_for</tt> in Action Pack. - def to_param - collect { |e| e.to_param }.join '/' - end -end - -class Hash - # Returns a string representation of the receiver suitable for use as a URL - # query string: - # - # {name: 'David', nationality: 'Danish'}.to_param - # # => "name=David&nationality=Danish" - # - # An optional namespace can be passed to enclose the param names: - # - # {name: 'David', nationality: 'Danish'}.to_param('user') - # # => "user[name]=David&user[nationality]=Danish" - # - # The string pairs "key=value" that conform the query string - # are sorted lexicographically in ascending order. - # - # This method is also aliased as +to_query+. - def to_param(namespace = nil) - collect do |key, value| - unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty? - value.to_query(namespace ? "#{namespace}[#{key}]" : key) - end - end.compact.sort! * '&' - end -end +require 'active_support/core_ext/object/to_query' diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb index 172f06ed64..ccd568bbf5 100644 --- a/activesupport/lib/active_support/core_ext/object/to_query.rb +++ b/activesupport/lib/active_support/core_ext/object/to_query.rb @@ -1,17 +1,46 @@ -require 'active_support/core_ext/object/to_param' require 'cgi' class Object - # Converts an object into a string suitable for use as a URL query string, using the given <tt>key</tt> as the - # param name. - # - # Note: This method is defined as a default implementation for all Objects for Hash#to_query to work. + # Alias of <tt>to_s</tt>. + def to_param + to_s + end + + # Converts an object into a string suitable for use as a URL query string, + # using the given <tt>key</tt> as the param name. def to_query(key) "#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}" end end +class NilClass + # Returns +self+. + def to_param + self + end +end + +class TrueClass + # Returns +self+. + def to_param + self + end +end + +class FalseClass + # Returns +self+. + def to_param + self + end +end + class Array + # Calls <tt>to_param</tt> on all its elements and joins the result with + # slashes. This is used by <tt>url_for</tt> in Action Pack. + def to_param + collect { |e| e.to_param }.join '/' + end + # Converts an array into a string suitable for use as a URL query string, # using the given +key+ as the param name. # @@ -28,5 +57,28 @@ class Array end class Hash - alias_method :to_query, :to_param + # Returns a string representation of the receiver suitable for use as a URL + # query string: + # + # {name: 'David', nationality: 'Danish'}.to_query + # # => "name=David&nationality=Danish" + # + # An optional namespace can be passed to enclose key names: + # + # {name: 'David', nationality: 'Danish'}.to_query('user') + # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish" + # + # The string pairs "key=value" that conform the query string + # are sorted lexicographically in ascending order. + # + # This method is also aliased as +to_param+. + def to_query(namespace = nil) + collect do |key, value| + unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty? + value.to_query(namespace ? "#{namespace}[#{key}]" : key) + end + end.compact.sort! * '&' + end + + alias_method :to_param, :to_query end diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb index 42e388b065..42e87c4424 100644 --- a/activesupport/lib/active_support/core_ext/object/with_options.rb +++ b/activesupport/lib/active_support/core_ext/object/with_options.rb @@ -34,9 +34,22 @@ class Object # body i18n.t :body, user_name: user.name # end # + # When you don't pass an explicit receiver, it executes the whole block + # in merging options context: + # + # class Account < ActiveRecord::Base + # with_options dependent: :destroy do + # has_many :customers + # has_many :products + # has_many :invoices + # has_many :expenses + # end + # end + # # <tt>with_options</tt> can also be nested since the call is forwarded to its receiver. # Each nesting level will merge inherited defaults in addition to their own. - def with_options(options) - yield ActiveSupport::OptionMerger.new(self, options) + def with_options(options, &block) + option_merger = ActiveSupport::OptionMerger.new(self, options) + block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger) end end diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb deleted file mode 100644 index fec8f7c0ec..0000000000 --- a/activesupport/lib/active_support/core_ext/securerandom.rb +++ /dev/null @@ -1,47 +0,0 @@ -module SecureRandom - UUID_DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: - UUID_URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: - UUID_OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: - UUID_X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: - - # Generates a v5 non-random UUID (Universally Unique IDentifier). - # - # Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs. - # ::uuid_from_hash always generates the same UUID for a given name and namespace combination. - # - # See RFC 4122 for details of UUID at: http://www.ietf.org/rfc/rfc4122.txt - def self.uuid_from_hash(hash_class, uuid_namespace, name) - if hash_class == Digest::MD5 - version = 3 - elsif hash_class == Digest::SHA1 - version = 5 - else - raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}." - end - - hash = hash_class.new - hash.update(uuid_namespace) - hash.update(name) - - ary = hash.digest.unpack('NnnnnN') - ary[2] = (ary[2] & 0x0FFF) | (version << 12) - ary[3] = (ary[3] & 0x3FFF) | 0x8000 - - "%08x-%04x-%04x-%04x-%04x%08x" % ary - end - - # Convenience method for ::uuid_from_hash using Digest::MD5. - def self.uuid_v3(uuid_namespace, name) - self.uuid_from_hash(Digest::MD5, uuid_namespace, name) - end - - # Convenience method for ::uuid_from_hash using Digest::SHA1. - def self.uuid_v5(uuid_namespace, name) - self.uuid_from_hash(Digest::SHA1, uuid_namespace, name) - end - - class << self - # Alias for ::uuid. - alias_method :uuid_v4, :uuid - end -end diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index 49c0df6026..1dfaf76673 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -62,4 +62,28 @@ class String "#{self[0, stop]}#{omission}" end + + # Truncates a given +text+ after a given number of words (<tt>words_count</tt>): + # + # 'Once upon a time in a world far far away'.truncate_words(4) + # # => "Once upon a time..." + # + # Pass a string or regexp <tt>:separator</tt> to specify a different separator of words: + # + # 'Once<br>upon<br>a<br>time<br>in<br>a<br>world'.truncate_words(5, separator: '<br>') + # # => "Once<br>upon<br>a<br>time<br>in..." + # + # The last characters will be replaced with the <tt>:omission</tt> string (defaults to "..."): + # + # 'And they found that many people were sleeping better.'.truncate_words(5, omission: '... (continued)') + # # => "And they found that many... (continued)" + def truncate_words(words_count, options = {}) + sep = options[:separator] || /\s+/ + sep = Regexp.escape(sep.to_s) unless Regexp === sep + if self =~ /\A((?:.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m + $1 + (options[:omission] || '...') + else + dup + end + end end 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 46cd170c1d..c761325108 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -7,7 +7,7 @@ class ERB HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' } HTML_ESCAPE_REGEXP = /[&"'><]/ - HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ + HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/ JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u # A utility method for escaping HTML tag characters. diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index a8d12366cc..93a11d4586 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -187,7 +187,7 @@ module ActiveSupport #:nodoc: # top-level constant. def guess_for_anonymous(const_name) if Object.const_defined?(const_name) - raise NameError.new "#{const_name} cannot be autoloaded from an anonymous class or module", const_name.to_s + raise NameError.new "#{const_name} cannot be autoloaded from an anonymous class or module", const_name else Object end @@ -516,7 +516,7 @@ module ActiveSupport #:nodoc: end end - name_error = NameError.new("uninitialized constant #{qualified_name}", qualified_name) + name_error = NameError.new("uninitialized constant #{qualified_name}", const_name) name_error.set_backtrace(caller.reject {|l| l.starts_with? __FILE__ }) raise name_error end diff --git a/activesupport/lib/active_support/file_watcher.rb b/activesupport/lib/active_support/file_watcher.rb deleted file mode 100644 index 81e63e76a7..0000000000 --- a/activesupport/lib/active_support/file_watcher.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveSupport - class FileWatcher - class Backend - def initialize(path, watcher) - @watcher = watcher - @path = path - end - - def trigger(files) - @watcher.trigger(files) - end - end - - def initialize - @regex_matchers = {} - end - - def watch(pattern, &block) - @regex_matchers[pattern] = block - end - - def trigger(files) - trigger_files = Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } } - - files.each do |file, state| - @regex_matchers.each do |pattern, block| - trigger_files[block][state] << file if pattern === file - end - end - - trigger_files.each do |block, payload| - block.call payload - end - end - end -end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index e1eb81b8bc..3d8f2d572b 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -75,6 +75,7 @@ module ActiveSupport hash = hash.to_hash new(hash).tap do |new_hash| new_hash.default = hash.default + new_hash.default_proc = hash.default_proc if hash.default_proc end end @@ -245,11 +246,11 @@ module ActiveSupport # Convert to a regular hash with string keys. def to_hash - _new_hash= {} + _new_hash = Hash.new(default) each do |key, value| - _new_hash[convert_key(key)] = convert_value(value, for: :to_hash) + _new_hash[key] = convert_value(value, for: :to_hash) end - Hash.new(default).merge!(_new_hash) + _new_hash end protected diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 23cd6716e3..affcfb7398 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -8,8 +8,6 @@ module I18n config.i18n.railties_load_path = [] config.i18n.load_path = [] config.i18n.fallbacks = ActiveSupport::OrderedOptions.new - # Enforce I18n to check the available locales when setting a locale. - config.i18n.enforce_available_locales = true # Set the i18n configuration after initialization since a lot of # configuration is still usually done in application initializers. @@ -36,7 +34,7 @@ module I18n # Avoid issues with setting the default_locale by disabling available locales # check while configuring. enforce_available_locales = app.config.i18n.delete(:enforce_available_locales) - enforce_available_locales = I18n.enforce_available_locales unless I18n.enforce_available_locales.nil? + enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil? I18n.enforce_available_locales = false app.config.i18n.each do |setting, value| diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index eda0edff28..97401ccec7 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -160,7 +160,7 @@ module ActiveSupport # uncountable 'money', 'information' # uncountable %w( money information rice ) def uncountable(*words) - (@uncountables << words).flatten! + @uncountables += words.flatten.map(&:downcase) end # Specifies a humanized form of a string by a regular expression rule or diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 51720d0192..53022de549 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -303,8 +303,8 @@ module ActiveSupport def safe_constantize(camel_cased_word) constantize(camel_cased_word) rescue NameError => e - raise unless e.message =~ /(uninitialized constant|wrong constant name) #{const_regexp(camel_cased_word)}$/ || - e.name.to_s == camel_cased_word.to_s + raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) || + e.name.to_s == camel_cased_word.to_s) rescue ArgumentError => e raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/ end @@ -348,10 +348,11 @@ module ActiveSupport private - # Mount a regular expression that will match part by part of the constant. + # Mounts a regular expression, returned as a string to ease interpolation, + # that will match part by part the given constant. # - # const_regexp("Foo::Bar::Baz") # => /Foo(::Bar(::Baz)?)?/ - # const_regexp("::") # => /::/ + # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?" + # const_regexp("::") # => "::" def const_regexp(camel_cased_word) #:nodoc: parts = camel_cased_word.split("::") diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index b019ad0dec..92ab6fe648 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -40,6 +40,7 @@ module ActiveSupport # Options: # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'. + # * <tt>:digest</tt> - String of digest to use for signing. Default is +SHA1+. # * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+. def initialize(secret, *signature_key_or_options) options = signature_key_or_options.extract_options! @@ -47,7 +48,7 @@ module ActiveSupport @secret = secret @sign_secret = sign_secret @cipher = options[:cipher] || 'aes-256-cbc' - @verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer) + @verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer) @serializer = options[:serializer] || Marshal end diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index ea3cdcd024..89009d1f55 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -213,7 +213,8 @@ module ActiveSupport end # Ruby >= 2.1 has String#scrub, which is faster than the workaround used for < 2.1. - if '<3'.respond_to?(:scrub) + # Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars. + if '<3'.respond_to?(:scrub) && !defined?(Rubinius) # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent # resulting in a valid UTF-8 string. # @@ -335,7 +336,7 @@ module ActiveSupport begin @codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, 'rb') { |f| Marshal.load f.read } rescue => e - raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable") + raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable") end # Redefine the === method so we can write shorter rules for grapheme cluster breaks @@ -367,6 +368,7 @@ module ActiveSupport private def apply_mapping(string, mapping) #:nodoc: + database.codepoints string.each_codepoint.map do |codepoint| cp = database.codepoints[codepoint] if cp and (ncp = cp.send(mapping)) and ncp > 0 @@ -384,7 +386,6 @@ module ActiveSupport def database @database ||= UnicodeDatabase.new end - end end end diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index e6c125bfdd..0df599b692 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -1,3 +1,5 @@ +gem 'minitest' # make sure we get the gem, not stdlib +require 'minitest' require 'active_support/testing/tagged_logging' require 'active_support/testing/setup_and_teardown' require 'active_support/testing/assertions' @@ -9,25 +11,15 @@ require 'active_support/testing/time_helpers' require 'active_support/core_ext/kernel/reporting' require 'active_support/deprecation' -begin - silence_warnings { require 'mocha/setup' } -rescue LoadError -end - module ActiveSupport class TestCase < ::Minitest::Test Assertion = Minitest::Assertion - alias_method :method_name, :name - - $tags = {} - def self.for_tag(tag) - yield if $tags[tag] + class << self + alias :my_tests_are_order_dependent! :i_suck_and_my_tests_are_order_dependent! end - # FIXME: we have tests that depend on run order, we should fix that and - # remove this method call. - self.i_suck_and_my_tests_are_order_dependent! + alias_method :method_name, :name include ActiveSupport::Testing::TaggedLogging include ActiveSupport::Testing::SetupAndTeardown diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 908af176be..68bda35980 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -70,14 +70,24 @@ module ActiveSupport exit! else Tempfile.open("isolation") do |tmpfile| - ENV["ISOLATION_TEST"] = self.class.name - ENV["ISOLATION_OUTPUT"] = tmpfile.path + env = { + ISOLATION_TEST: self.class.name, + ISOLATION_OUTPUT: tmpfile.path + } load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ") - `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")}` - - ENV.delete("ISOLATION_TEST") - ENV.delete("ISOLATION_OUTPUT") + orig_args = ORIG_ARGV.join(" ") + test_opts = "-n#{self.class.name}##{self.name}" + command = "#{Gem.ruby} #{load_paths} #{$0} #{orig_args} #{test_opts}" + + # IO.popen lets us pass env in a cross-platform way + child = IO.popen([env, command]) + + begin + Process.wait(child.pid) + rescue Errno::ECHILD # The child process may exit before we wait + nil + end return tmpfile.read.unpack("m")[0] end diff --git a/activesupport/lib/active_support/testing/tagged_logging.rb b/activesupport/lib/active_support/testing/tagged_logging.rb index f4cee64091..843ce4a867 100644 --- a/activesupport/lib/active_support/testing/tagged_logging.rb +++ b/activesupport/lib/active_support/testing/tagged_logging.rb @@ -6,7 +6,7 @@ module ActiveSupport attr_writer :tagged_logger def before_setup - if tagged_logger + if tagged_logger && tagged_logger.info? heading = "#{self.class}: #{name}" divider = '-' * heading.size tagged_logger.info divider diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index eefa84262e..1112c6e0b1 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -78,6 +78,10 @@ module ActiveSupport # or <tt>Date.today</tt>, in order to honor the application time zone # please always use <tt>Time.current</tt> and <tt>Date.current</tt>.) # + # Note that the usec for the time passed will be set to 0 to prevent rounding + # errors with external services, like MySQL (which will round instead of floor, + # leading to off-by-one-second errors). + # # This method also accepts a block, which will return the current time back to its original # state at the end of the block: # @@ -90,7 +94,7 @@ module ActiveSupport if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime) now = date_or_time.midnight.to_time else - now = date_or_time.to_time + now = date_or_time.to_time.change(usec: 0) end simple_stubs.stub_object(Time, :now, now) diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 0b393e0c7a..7ffcae6007 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -36,3 +36,5 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +require 'mocha/setup' # FIXME: stop using mocha diff --git a/activesupport/test/clean_backtrace_test.rb b/activesupport/test/clean_backtrace_test.rb index dd67a45cf6..05580352a9 100644 --- a/activesupport/test/clean_backtrace_test.rb +++ b/activesupport/test/clean_backtrace_test.rb @@ -34,6 +34,11 @@ class BacktraceCleanerSilencerTest < ActiveSupport::TestCase [ "/other/class.rb" ], @bc.clean([ "/mongrel/class.rb", "/other/class.rb", "/mongrel/stuff.rb" ]) end + + test "backtrace cleaner should allow removing silencer" do + @bc.remove_silencers! + assert_equal ["/mongrel/stuff.rb"], @bc.clean(["/mongrel/stuff.rb"]) + end end class BacktraceCleanerMultipleSilencersTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/array/access_test.rb b/activesupport/test/core_ext/array/access_test.rb new file mode 100644 index 0000000000..f14f64421d --- /dev/null +++ b/activesupport/test/core_ext/array/access_test.rb @@ -0,0 +1,30 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' + +class AccessTest < ActiveSupport::TestCase + def test_from + assert_equal %w( a b c d ), %w( a b c d ).from(0) + assert_equal %w( c d ), %w( a b c d ).from(2) + assert_equal %w(), %w( a b c d ).from(10) + assert_equal %w( d e ), %w( a b c d e ).from(-2) + assert_equal %w(), %w( a b c d e ).from(-10) + end + + def test_to + assert_equal %w( a ), %w( a b c d ).to(0) + assert_equal %w( a b c ), %w( a b c d ).to(2) + assert_equal %w( a b c d ), %w( a b c d ).to(10) + assert_equal %w( a b c ), %w( a b c d ).to(-2) + assert_equal %w(), %w( a b c ).to(-10) + end + + def test_specific_accessor + array = (1..42).to_a + + assert_equal array[1], array.second + assert_equal array[2], array.third + assert_equal array[3], array.fourth + assert_equal array[4], array.fifth + assert_equal array[41], array.forty_two + end +end diff --git a/activesupport/test/core_ext/array/conversions_test.rb b/activesupport/test/core_ext/array/conversions_test.rb new file mode 100644 index 0000000000..577b889410 --- /dev/null +++ b/activesupport/test/core_ext/array/conversions_test.rb @@ -0,0 +1,197 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' +require 'active_support/core_ext/big_decimal' +require 'active_support/core_ext/hash' +require 'active_support/core_ext/string' + +class ToSentenceTest < ActiveSupport::TestCase + def test_plain_array_to_sentence + assert_equal "", [].to_sentence + assert_equal "one", ['one'].to_sentence + assert_equal "one and two", ['one', 'two'].to_sentence + assert_equal "one, two, and three", ['one', 'two', 'three'].to_sentence + end + + def test_to_sentence_with_words_connector + assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(words_connector: ' ') + assert_equal "one & two, and three", ['one', 'two', 'three'].to_sentence(words_connector: ' & ') + assert_equal "onetwo, and three", ['one', 'two', 'three'].to_sentence(words_connector: nil) + end + + def test_to_sentence_with_last_word_connector + assert_equal "one, two, and also three", ['one', 'two', 'three'].to_sentence(last_word_connector: ', and also ') + assert_equal "one, twothree", ['one', 'two', 'three'].to_sentence(last_word_connector: nil) + assert_equal "one, two three", ['one', 'two', 'three'].to_sentence(last_word_connector: ' ') + assert_equal "one, two and three", ['one', 'two', 'three'].to_sentence(last_word_connector: ' and ') + end + + def test_two_elements + assert_equal "one and two", ['one', 'two'].to_sentence + assert_equal "one two", ['one', 'two'].to_sentence(two_words_connector: ' ') + end + + def test_one_element + assert_equal "one", ['one'].to_sentence + end + + def test_one_element_not_same_object + elements = ["one"] + assert_not_equal elements[0].object_id, elements.to_sentence.object_id + end + + def test_one_non_string_element + assert_equal '1', [1].to_sentence + end + + def test_does_not_modify_given_hash + options = { words_connector: ' ' } + assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(options) + assert_equal({ words_connector: ' ' }, options) + end + + def test_with_blank_elements + assert_equal ", one, , two, and three", [nil, 'one', '', 'two', 'three'].to_sentence + end + + def test_with_invalid_options + exception = assert_raise ArgumentError do + ['one', 'two'].to_sentence(passing: 'invalid option') + end + + assert_equal exception.message, "Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale" + end +end + +class ToSTest < ActiveSupport::TestCase + class TestDB + @@counter = 0 + def id + @@counter += 1 + end + end + + def test_to_s_db + collection = [TestDB.new, TestDB.new, TestDB.new] + + assert_equal "null", [].to_s(:db) + assert_equal "1,2,3", collection.to_s(:db) + end +end + +class ToXmlTest < ActiveSupport::TestCase + def test_to_xml_with_hash_elements + xml = [ + { name: "David", age: 26, age_in_millis: 820497600000 }, + { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') } + ].to_xml(skip_instruct: true, indent: 0) + + assert_equal '<objects type="array"><object>', xml.first(30) + assert xml.include?(%(<age type="integer">26</age>)), xml + assert xml.include?(%(<age-in-millis type="integer">820497600000</age-in-millis>)), xml + assert xml.include?(%(<name>David</name>)), xml + assert xml.include?(%(<age type="integer">31</age>)), xml + assert xml.include?(%(<age-in-millis type="decimal">1.0</age-in-millis>)), xml + assert xml.include?(%(<name>Jason</name>)), xml + end + + def test_to_xml_with_non_hash_elements + xml = [1, 2, 3].to_xml(skip_instruct: true, indent: 0) + + assert_equal '<fixnums type="array"><fixnum', xml.first(29) + assert xml.include?(%(<fixnum type="integer">2</fixnum>)), xml + end + + def test_to_xml_with_non_hash_different_type_elements + xml = [1, 2.0, '3'].to_xml(skip_instruct: true, indent: 0) + + assert_equal '<objects type="array"><object', xml.first(29) + assert xml.include?(%(<object type="integer">1</object>)), xml + assert xml.include?(%(<object type="float">2.0</object>)), xml + assert xml.include?(%(object>3</object>)), xml + end + + def test_to_xml_with_dedicated_name + xml = [ + { name: "David", age: 26, age_in_millis: 820497600000 }, { name: "Jason", age: 31 } + ].to_xml(skip_instruct: true, indent: 0, root: "people") + + assert_equal '<people type="array"><person>', xml.first(29) + end + + def test_to_xml_with_options + xml = [ + { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" } + ].to_xml(skip_instruct: true, skip_types: true, indent: 0) + + assert_equal "<objects><object>", xml.first(17) + assert xml.include?(%(<street-address>Paulina</street-address>)) + assert xml.include?(%(<name>David</name>)) + assert xml.include?(%(<street-address>Evergreen</street-address>)) + assert xml.include?(%(<name>Jason</name>)) + end + + def test_to_xml_with_indent_set + xml = [ + { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" } + ].to_xml(skip_instruct: true, skip_types: true, indent: 4) + + assert_equal "<objects>\n <object>", xml.first(22) + assert xml.include?(%(\n <street-address>Paulina</street-address>)) + assert xml.include?(%(\n <name>David</name>)) + assert xml.include?(%(\n <street-address>Evergreen</street-address>)) + assert xml.include?(%(\n <name>Jason</name>)) + end + + def test_to_xml_with_dasherize_false + xml = [ + { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" } + ].to_xml(skip_instruct: true, skip_types: true, indent: 0, dasherize: false) + + assert_equal "<objects><object>", xml.first(17) + assert xml.include?(%(<street_address>Paulina</street_address>)) + assert xml.include?(%(<street_address>Evergreen</street_address>)) + end + + def test_to_xml_with_dasherize_true + xml = [ + { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" } + ].to_xml(skip_instruct: true, skip_types: true, indent: 0, dasherize: true) + + assert_equal "<objects><object>", xml.first(17) + assert xml.include?(%(<street-address>Paulina</street-address>)) + assert xml.include?(%(<street-address>Evergreen</street-address>)) + end + + def test_to_xml_with_instruct + xml = [ + { name: "David", age: 26, age_in_millis: 820497600000 }, + { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') } + ].to_xml(skip_instruct: false, indent: 0) + + assert_match(/^<\?xml [^>]*/, xml) + assert_equal 0, xml.rindex(/<\?xml /) + end + + def test_to_xml_with_block + xml = [ + { name: "David", age: 26, age_in_millis: 820497600000 }, + { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') } + ].to_xml(skip_instruct: true, indent: 0) do |builder| + builder.count 2 + end + + assert xml.include?(%(<count>2</count>)), xml + end + + def test_to_xml_with_empty + xml = [].to_xml + assert_match(/type="array"\/>/, xml) + end + + def test_to_xml_dups_options + options = { skip_instruct: true } + [].to_xml(options) + # :builder, etc, shouldn't be added to options + assert_equal({ skip_instruct: true }, options) + end +end diff --git a/activesupport/test/core_ext/array/extract_options_test.rb b/activesupport/test/core_ext/array/extract_options_test.rb new file mode 100644 index 0000000000..0481a507cf --- /dev/null +++ b/activesupport/test/core_ext/array/extract_options_test.rb @@ -0,0 +1,45 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' +require 'active_support/core_ext/hash' + +class ExtractOptionsTest < ActiveSupport::TestCase + class HashSubclass < Hash + end + + class ExtractableHashSubclass < Hash + def extractable_options? + true + end + end + + def test_extract_options + assert_equal({}, [].extract_options!) + assert_equal({}, [1].extract_options!) + assert_equal({ a: :b }, [{ a: :b }].extract_options!) + assert_equal({ a: :b }, [1, { a: :b }].extract_options!) + end + + def test_extract_options_doesnt_extract_hash_subclasses + hash = HashSubclass.new + hash[:foo] = 1 + array = [hash] + options = array.extract_options! + assert_equal({}, options) + assert_equal([hash], array) + end + + def test_extract_options_extracts_extractable_subclass + hash = ExtractableHashSubclass.new + hash[:foo] = 1 + array = [hash] + options = array.extract_options! + assert_equal({ foo: 1 }, options) + assert_equal([], array) + end + + def test_extract_options_extracts_hash_with_indifferent_access + array = [{ foo: 1 }.with_indifferent_access] + options = array.extract_options! + assert_equal(1, options[:foo]) + end +end diff --git a/activesupport/test/core_ext/array/grouping_test.rb b/activesupport/test/core_ext/array/grouping_test.rb new file mode 100644 index 0000000000..2eb0f05141 --- /dev/null +++ b/activesupport/test/core_ext/array/grouping_test.rb @@ -0,0 +1,126 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' + +class GroupingTest < ActiveSupport::TestCase + def setup + Fixnum.send :private, :/ # test we avoid Integer#/ (redefined by mathn) + end + + def teardown + Fixnum.send :public, :/ + end + + def test_in_groups_of_with_perfect_fit + groups = [] + ('a'..'i').to_a.in_groups_of(3) do |group| + groups << group + end + + assert_equal [%w(a b c), %w(d e f), %w(g h i)], groups + assert_equal [%w(a b c), %w(d e f), %w(g h i)], ('a'..'i').to_a.in_groups_of(3) + end + + def test_in_groups_of_with_padding + groups = [] + ('a'..'g').to_a.in_groups_of(3) do |group| + groups << group + end + + assert_equal [%w(a b c), %w(d e f), ['g', nil, nil]], groups + end + + def test_in_groups_of_pads_with_specified_values + groups = [] + + ('a'..'g').to_a.in_groups_of(3, 'foo') do |group| + groups << group + end + + assert_equal [%w(a b c), %w(d e f), %w(g foo foo)], groups + end + + def test_in_groups_of_without_padding + groups = [] + + ('a'..'g').to_a.in_groups_of(3, false) do |group| + groups << group + end + + assert_equal [%w(a b c), %w(d e f), %w(g)], groups + end + + def test_in_groups_returned_array_size + array = (1..7).to_a + + 1.upto(array.size + 1) do |number| + assert_equal number, array.in_groups(number).size + end + end + + def test_in_groups_with_empty_array + assert_equal [[], [], []], [].in_groups(3) + end + + def test_in_groups_with_block + array = (1..9).to_a + groups = [] + + array.in_groups(3) do |group| + groups << group + end + + assert_equal array.in_groups(3), groups + end + + def test_in_groups_with_perfect_fit + assert_equal [[1, 2, 3], [4, 5, 6], [7, 8, 9]], + (1..9).to_a.in_groups(3) + end + + def test_in_groups_with_padding + array = (1..7).to_a + + assert_equal [[1, 2, 3], [4, 5, nil], [6, 7, nil]], + array.in_groups(3) + assert_equal [[1, 2, 3], [4, 5, 'foo'], [6, 7, 'foo']], + array.in_groups(3, 'foo') + end + + def test_in_groups_without_padding + assert_equal [[1, 2, 3], [4, 5], [6, 7]], + (1..7).to_a.in_groups(3, false) + end + + def test_in_groups_invalid_argument + assert_raises(ArgumentError) { [].in_groups_of(0) } + assert_raises(ArgumentError) { [].in_groups_of(-1) } + assert_raises(ArgumentError) { [].in_groups_of(nil) } + end +end + +class SplitTest < ActiveSupport::TestCase + def test_split_with_empty_array + assert_equal [[]], [].split(0) + end + + def test_split_with_argument + a = [1, 2, 3, 4, 5] + assert_equal [[1, 2], [4, 5]], a.split(3) + assert_equal [[1, 2, 3, 4, 5]], a.split(0) + assert_equal [1, 2, 3, 4, 5], a + end + + def test_split_with_block + a = (1..10).to_a + assert_equal [[1, 2], [4, 5], [7, 8], [10]], a.split { |i| i % 3 == 0 } + assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9 ,10], a + end + + def test_split_with_edge_values + a = [1, 2, 3, 4, 5] + assert_equal [[], [2, 3, 4, 5]], a.split(1) + assert_equal [[1, 2, 3, 4], []], a.split(5) + assert_equal [[], [2, 3, 4], []], a.split { |i| i == 1 || i == 5 } + assert_equal [1, 2, 3, 4, 5], a + end +end diff --git a/activesupport/test/core_ext/array/prepend_append_test.rb b/activesupport/test/core_ext/array/prepend_append_test.rb new file mode 100644 index 0000000000..762aa69b2b --- /dev/null +++ b/activesupport/test/core_ext/array/prepend_append_test.rb @@ -0,0 +1,12 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' + +class PrependAppendTest < ActiveSupport::TestCase + def test_append + assert_equal [1, 2], [1].append(2) + end + + def test_prepend + assert_equal [2, 1], [1].prepend(2) + end +end diff --git a/activesupport/test/core_ext/array/wrap_test.rb b/activesupport/test/core_ext/array/wrap_test.rb new file mode 100644 index 0000000000..baf426506f --- /dev/null +++ b/activesupport/test/core_ext/array/wrap_test.rb @@ -0,0 +1,77 @@ +require 'abstract_unit' +require 'active_support/core_ext/array' + +class WrapTest < ActiveSupport::TestCase + class FakeCollection + def to_ary + ["foo", "bar"] + end + end + + class Proxy + def initialize(target) @target = target end + def method_missing(*a) @target.send(*a) end + end + + class DoubtfulToAry + def to_ary + :not_an_array + end + end + + class NilToAry + def to_ary + nil + end + end + + def test_array + ary = %w(foo bar) + assert_same ary, Array.wrap(ary) + end + + def test_nil + assert_equal [], Array.wrap(nil) + end + + def test_object + o = Object.new + assert_equal [o], Array.wrap(o) + end + + def test_string + assert_equal ["foo"], Array.wrap("foo") + end + + def test_string_with_newline + assert_equal ["foo\nbar"], Array.wrap("foo\nbar") + end + + def test_object_with_to_ary + assert_equal ["foo", "bar"], Array.wrap(FakeCollection.new) + end + + def test_proxy_object + p = Proxy.new(Object.new) + assert_equal [p], Array.wrap(p) + end + + def test_proxy_to_object_with_to_ary + p = Proxy.new(FakeCollection.new) + assert_equal [p], Array.wrap(p) + end + + def test_struct + o = Struct.new(:foo).new(123) + assert_equal [o], Array.wrap(o) + end + + def test_wrap_returns_wrapped_if_to_ary_returns_nil + o = NilToAry.new + assert_equal [o], Array.wrap(o) + end + + def test_wrap_does_not_complain_if_to_ary_does_not_return_an_array + assert_equal DoubtfulToAry.new.to_ary, Array.wrap(DoubtfulToAry.new) + end +end diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb deleted file mode 100644 index bd1b818717..0000000000 --- a/activesupport/test/core_ext/array_ext_test.rb +++ /dev/null @@ -1,482 +0,0 @@ -require 'abstract_unit' -require 'active_support/core_ext/array' -require 'active_support/core_ext/big_decimal' -require 'active_support/core_ext/hash' -require 'active_support/core_ext/object/conversions' -require 'active_support/core_ext/string' - -class ArrayExtAccessTests < ActiveSupport::TestCase - def test_from - assert_equal %w( a b c d ), %w( a b c d ).from(0) - assert_equal %w( c d ), %w( a b c d ).from(2) - assert_equal %w(), %w( a b c d ).from(10) - assert_equal %w( d e ), %w( a b c d e ).from(-2) - assert_equal %w(), %w( a b c d e ).from(-10) - end - - def test_to - assert_equal %w( a ), %w( a b c d ).to(0) - assert_equal %w( a b c ), %w( a b c d ).to(2) - assert_equal %w( a b c d ), %w( a b c d ).to(10) - assert_equal %w( a b c ), %w( a b c d ).to(-2) - assert_equal %w(), %w( a b c ).to(-10) - end - - def test_second_through_tenth - array = (1..42).to_a - - assert_equal array[1], array.second - assert_equal array[2], array.third - assert_equal array[3], array.fourth - assert_equal array[4], array.fifth - assert_equal array[41], array.forty_two - end -end - -class ArrayExtToParamTests < ActiveSupport::TestCase - class ToParam < String - def to_param - "#{self}1" - end - end - - def test_string_array - assert_equal '', %w().to_param - assert_equal 'hello/world', %w(hello world).to_param - assert_equal 'hello/10', %w(hello 10).to_param - end - - def test_number_array - assert_equal '10/20', [10, 20].to_param - end - - def test_to_param_array - assert_equal 'custom1/param1', [ToParam.new('custom'), ToParam.new('param')].to_param - end -end - -class ArrayExtToSentenceTests < ActiveSupport::TestCase - def test_plain_array_to_sentence - assert_equal "", [].to_sentence - assert_equal "one", ['one'].to_sentence - assert_equal "one and two", ['one', 'two'].to_sentence - assert_equal "one, two, and three", ['one', 'two', 'three'].to_sentence - end - - def test_to_sentence_with_words_connector - assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(:words_connector => ' ') - assert_equal "one & two, and three", ['one', 'two', 'three'].to_sentence(:words_connector => ' & ') - assert_equal "onetwo, and three", ['one', 'two', 'three'].to_sentence(:words_connector => nil) - end - - def test_to_sentence_with_last_word_connector - assert_equal "one, two, and also three", ['one', 'two', 'three'].to_sentence(:last_word_connector => ', and also ') - assert_equal "one, twothree", ['one', 'two', 'three'].to_sentence(:last_word_connector => nil) - assert_equal "one, two three", ['one', 'two', 'three'].to_sentence(:last_word_connector => ' ') - assert_equal "one, two and three", ['one', 'two', 'three'].to_sentence(:last_word_connector => ' and ') - end - - def test_two_elements - assert_equal "one and two", ['one', 'two'].to_sentence - assert_equal "one two", ['one', 'two'].to_sentence(:two_words_connector => ' ') - end - - def test_one_element - assert_equal "one", ['one'].to_sentence - end - - def test_one_element_not_same_object - elements = ["one"] - assert_not_equal elements[0].object_id, elements.to_sentence.object_id - end - - def test_one_non_string_element - assert_equal '1', [1].to_sentence - end - - def test_does_not_modify_given_hash - options = { words_connector: ' ' } - assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(options) - assert_equal({ words_connector: ' ' }, options) - end - - def test_with_blank_elements - assert_equal ", one, , two, and three", [nil, 'one', '', 'two', 'three'].to_sentence - end -end - -class ArrayExtToSTests < ActiveSupport::TestCase - def test_to_s_db - collection = [ - Class.new { def id() 1 end }.new, - Class.new { def id() 2 end }.new, - Class.new { def id() 3 end }.new - ] - - assert_equal "null", [].to_s(:db) - assert_equal "1,2,3", collection.to_s(:db) - end -end - -class ArrayExtGroupingTests < ActiveSupport::TestCase - def setup - Fixnum.send :private, :/ # test we avoid Integer#/ (redefined by mathn) - end - - def teardown - Fixnum.send :public, :/ - end - - def test_in_groups_of_with_perfect_fit - groups = [] - ('a'..'i').to_a.in_groups_of(3) do |group| - groups << group - end - - assert_equal [%w(a b c), %w(d e f), %w(g h i)], groups - assert_equal [%w(a b c), %w(d e f), %w(g h i)], ('a'..'i').to_a.in_groups_of(3) - end - - def test_in_groups_of_with_padding - groups = [] - ('a'..'g').to_a.in_groups_of(3) do |group| - groups << group - end - - assert_equal [%w(a b c), %w(d e f), ['g', nil, nil]], groups - end - - def test_in_groups_of_pads_with_specified_values - groups = [] - - ('a'..'g').to_a.in_groups_of(3, 'foo') do |group| - groups << group - end - - assert_equal [%w(a b c), %w(d e f), ['g', 'foo', 'foo']], groups - end - - def test_in_groups_of_without_padding - groups = [] - - ('a'..'g').to_a.in_groups_of(3, false) do |group| - groups << group - end - - assert_equal [%w(a b c), %w(d e f), ['g']], groups - end - - def test_in_groups_returned_array_size - array = (1..7).to_a - - 1.upto(array.size + 1) do |number| - assert_equal number, array.in_groups(number).size - end - end - - def test_in_groups_with_empty_array - assert_equal [[], [], []], [].in_groups(3) - end - - def test_in_groups_with_block - array = (1..9).to_a - groups = [] - - array.in_groups(3) do |group| - groups << group - end - - assert_equal array.in_groups(3), groups - end - - def test_in_groups_with_perfect_fit - assert_equal [[1, 2, 3], [4, 5, 6], [7, 8, 9]], - (1..9).to_a.in_groups(3) - end - - def test_in_groups_with_padding - array = (1..7).to_a - - assert_equal [[1, 2, 3], [4, 5, nil], [6, 7, nil]], - array.in_groups(3) - assert_equal [[1, 2, 3], [4, 5, 'foo'], [6, 7, 'foo']], - array.in_groups(3, 'foo') - end - - def test_in_groups_without_padding - assert_equal [[1, 2, 3], [4, 5], [6, 7]], - (1..7).to_a.in_groups(3, false) - end -end - -class ArraySplitTests < ActiveSupport::TestCase - def test_split_with_empty_array - assert_equal [[]], [].split(0) - end - - def test_split_with_argument - a = [1, 2, 3, 4, 5] - assert_equal [[1, 2], [4, 5]], a.split(3) - assert_equal [[1, 2, 3, 4, 5]], a.split(0) - assert_equal [1, 2, 3, 4, 5], a - end - - def test_split_with_block - a = (1..10).to_a - assert_equal [[1, 2], [4, 5], [7, 8], [10]], a.split { |i| i % 3 == 0 } - assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9 ,10], a - end - - def test_split_with_edge_values - a = [1, 2, 3, 4, 5] - assert_equal [[], [2, 3, 4, 5]], a.split(1) - assert_equal [[1, 2, 3, 4], []], a.split(5) - assert_equal [[], [2, 3, 4], []], a.split { |i| i == 1 || i == 5 } - assert_equal [1, 2, 3, 4, 5], a - end -end - -class ArrayToXmlTests < ActiveSupport::TestCase - def test_to_xml_with_hash_elements - xml = [ - { :name => "David", :age => 26, :age_in_millis => 820497600000 }, - { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') } - ].to_xml(:skip_instruct => true, :indent => 0) - - assert_equal '<objects type="array"><object>', xml.first(30) - assert xml.include?(%(<age type="integer">26</age>)), xml - assert xml.include?(%(<age-in-millis type="integer">820497600000</age-in-millis>)), xml - assert xml.include?(%(<name>David</name>)), xml - assert xml.include?(%(<age type="integer">31</age>)), xml - assert xml.include?(%(<age-in-millis type="decimal">1.0</age-in-millis>)), xml - assert xml.include?(%(<name>Jason</name>)), xml - end - - def test_to_xml_with_non_hash_elements - xml = [1, 2, 3].to_xml(:skip_instruct => true, :indent => 0) - - assert_equal '<fixnums type="array"><fixnum', xml.first(29) - assert xml.include?(%(<fixnum type="integer">2</fixnum>)), xml - end - - def test_to_xml_with_non_hash_different_type_elements - xml = [1, 2.0, '3'].to_xml(:skip_instruct => true, :indent => 0) - - assert_equal '<objects type="array"><object', xml.first(29) - assert xml.include?(%(<object type="integer">1</object>)), xml - assert xml.include?(%(<object type="float">2.0</object>)), xml - assert xml.include?(%(object>3</object>)), xml - end - - def test_to_xml_with_dedicated_name - xml = [ - { :name => "David", :age => 26, :age_in_millis => 820497600000 }, { :name => "Jason", :age => 31 } - ].to_xml(:skip_instruct => true, :indent => 0, :root => "people") - - assert_equal '<people type="array"><person>', xml.first(29) - end - - def test_to_xml_with_options - xml = [ - { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } - ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0) - - assert_equal "<objects><object>", xml.first(17) - assert xml.include?(%(<street-address>Paulina</street-address>)) - assert xml.include?(%(<name>David</name>)) - assert xml.include?(%(<street-address>Evergreen</street-address>)) - assert xml.include?(%(<name>Jason</name>)) - end - - def test_to_xml_with_indent_set - xml = [ - { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } - ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 4) - - assert_equal "<objects>\n <object>", xml.first(22) - assert xml.include?(%(\n <street-address>Paulina</street-address>)) - assert xml.include?(%(\n <name>David</name>)) - assert xml.include?(%(\n <street-address>Evergreen</street-address>)) - assert xml.include?(%(\n <name>Jason</name>)) - end - - def test_to_xml_with_dasherize_false - xml = [ - { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } - ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => false) - - assert_equal "<objects><object>", xml.first(17) - assert xml.include?(%(<street_address>Paulina</street_address>)) - assert xml.include?(%(<street_address>Evergreen</street_address>)) - end - - def test_to_xml_with_dasherize_true - xml = [ - { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" } - ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => true) - - assert_equal "<objects><object>", xml.first(17) - assert xml.include?(%(<street-address>Paulina</street-address>)) - assert xml.include?(%(<street-address>Evergreen</street-address>)) - end - - def test_to_xml_with_instruct - xml = [ - { :name => "David", :age => 26, :age_in_millis => 820497600000 }, - { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') } - ].to_xml(:skip_instruct => false, :indent => 0) - - assert_match(/^<\?xml [^>]*/, xml) - assert_equal 0, xml.rindex(/<\?xml /) - end - - def test_to_xml_with_block - xml = [ - { :name => "David", :age => 26, :age_in_millis => 820497600000 }, - { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') } - ].to_xml(:skip_instruct => true, :indent => 0) do |builder| - builder.count 2 - end - - assert xml.include?(%(<count>2</count>)), xml - end - - def test_to_xml_with_empty - xml = [].to_xml - assert_match(/type="array"\/>/, xml) - end - - def test_to_xml_dups_options - options = {:skip_instruct => true} - [].to_xml(options) - # :builder, etc, shouldn't be added to options - assert_equal({:skip_instruct => true}, options) - end -end - -class ArrayExtractOptionsTests < ActiveSupport::TestCase - class HashSubclass < Hash - end - - class ExtractableHashSubclass < Hash - def extractable_options? - true - end - end - - def test_extract_options - assert_equal({}, [].extract_options!) - assert_equal({}, [1].extract_options!) - assert_equal({:a=>:b}, [{:a=>:b}].extract_options!) - assert_equal({:a=>:b}, [1, {:a=>:b}].extract_options!) - end - - def test_extract_options_doesnt_extract_hash_subclasses - hash = HashSubclass.new - hash[:foo] = 1 - array = [hash] - options = array.extract_options! - assert_equal({}, options) - assert_equal [hash], array - end - - def test_extract_options_extracts_extractable_subclass - hash = ExtractableHashSubclass.new - hash[:foo] = 1 - array = [hash] - options = array.extract_options! - assert_equal({:foo => 1}, options) - assert_equal [], array - end - - def test_extract_options_extracts_hwia - hash = [{:foo => 1}.with_indifferent_access] - options = hash.extract_options! - assert_equal 1, options[:foo] - end -end - -class ArrayWrapperTests < ActiveSupport::TestCase - class FakeCollection - def to_ary - ["foo", "bar"] - end - end - - class Proxy - def initialize(target) @target = target end - def method_missing(*a) @target.send(*a) end - end - - class DoubtfulToAry - def to_ary - :not_an_array - end - end - - class NilToAry - def to_ary - nil - end - end - - def test_array - ary = %w(foo bar) - assert_same ary, Array.wrap(ary) - end - - def test_nil - assert_equal [], Array.wrap(nil) - end - - def test_object - o = Object.new - assert_equal [o], Array.wrap(o) - end - - def test_string - assert_equal ["foo"], Array.wrap("foo") - end - - def test_string_with_newline - assert_equal ["foo\nbar"], Array.wrap("foo\nbar") - end - - def test_object_with_to_ary - assert_equal ["foo", "bar"], Array.wrap(FakeCollection.new) - end - - def test_proxy_object - p = Proxy.new(Object.new) - assert_equal [p], Array.wrap(p) - end - - def test_proxy_to_object_with_to_ary - p = Proxy.new(FakeCollection.new) - assert_equal [p], Array.wrap(p) - end - - def test_struct - o = Struct.new(:foo).new(123) - assert_equal [o], Array.wrap(o) - end - - def test_wrap_returns_wrapped_if_to_ary_returns_nil - o = NilToAry.new - assert_equal [o], Array.wrap(o) - end - - def test_wrap_does_not_complain_if_to_ary_does_not_return_an_array - assert_equal DoubtfulToAry.new.to_ary, Array.wrap(DoubtfulToAry.new) - end -end - -class ArrayPrependAppendTest < ActiveSupport::TestCase - def test_append - assert_equal [1, 2], [1].append(2) - end - - def test_prepend - assert_equal [2, 1], [1].prepend(2) - end -end diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb index 2c08b46791..74319ecd09 100644 --- a/activesupport/test/core_ext/date_time_ext_test.rb +++ b/activesupport/test/core_ext/date_time_ext_test.rb @@ -338,6 +338,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase def test_to_f assert_equal 946684800.0, DateTime.civil(2000).to_f assert_equal 946684800.0, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_f + assert_equal 946684800.5, DateTime.civil(1999,12,31,19,0,0.5,Rational(-5,24)).to_f end def test_to_i diff --git a/activesupport/test/core_ext/digest/uuid_test.rb b/activesupport/test/core_ext/digest/uuid_test.rb new file mode 100644 index 0000000000..08e0a1d6e1 --- /dev/null +++ b/activesupport/test/core_ext/digest/uuid_test.rb @@ -0,0 +1,24 @@ +require 'abstract_unit' +require 'active_support/core_ext/digest/uuid' + +class DigestUUIDExt < ActiveSupport::TestCase + def test_v3_uuids + assert_equal "3d813cbb-47fb-32ba-91df-831e1593ac29", Digest::UUID.uuid_v3(Digest::UUID::DNS_NAMESPACE, "www.widgets.com") + assert_equal "86df55fb-428e-3843-8583-ba3c05f290bc", Digest::UUID.uuid_v3(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com") + assert_equal "8c29ab0e-a2dc-3482-b5eb-20cb2e2387a1", Digest::UUID.uuid_v3(Digest::UUID::OID_NAMESPACE, "1.2.3") + assert_equal "ee49149d-53a4-304a-890b-468229f6afc3", Digest::UUID.uuid_v3(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + def test_v5_uuids + assert_equal "21f7f8de-8051-5b89-8680-0195ef798b6a", Digest::UUID.uuid_v5(Digest::UUID::DNS_NAMESPACE, "www.widgets.com") + assert_equal "4e570fd8-186d-5a74-90f0-4d28e34673a1", Digest::UUID.uuid_v5(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com") + assert_equal "42d5e23b-3a02-5135-85c6-52d1102f1f00", Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "1.2.3") + assert_equal "fd5b2ddf-bcfe-58b6-90d6-db50f74db527", Digest::UUID.uuid_v5(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") + end + + def test_invalid_hash_class + assert_raise ArgumentError do + Digest::UUID.uuid_from_hash(Digest::SHA2, Digest::UUID::OID_NAMESPACE, '1.2.3') + end + end +end diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 328521bdb5..31af3c4521 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -162,6 +162,10 @@ class DurationTest < ActiveSupport::TestCase assert_equal counter, 60 end + def test_as_json + assert_equal 172800, 2.days.as_json + end + def test_to_json assert_equal '172800', 2.days.to_json end diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb new file mode 100644 index 0000000000..a7e12117f3 --- /dev/null +++ b/activesupport/test/core_ext/hash/transform_keys_test.rb @@ -0,0 +1,32 @@ +require 'abstract_unit' +require 'active_support/core_ext/hash/keys' + +class TransformKeysTest < ActiveSupport::TestCase + test "transform_keys returns a new hash with the keys computed from the block" do + original = { a: 'a', b: 'b' } + mapped = original.transform_keys { |k| "#{k}!".to_sym } + + assert_equal({ a: 'a', b: 'b' }, original) + assert_equal({ a!: 'a', b!: 'b' }, mapped) + end + + test "transform_keys! modifies the keys of the original" do + original = { a: 'a', b: 'b' } + mapped = original.transform_keys! { |k| "#{k}!".to_sym } + + assert_equal({ a!: 'a', b!: 'b' }, original) + assert_same original, mapped + end + + test "transform_keys returns an Enumerator if no block is given" do + original = { a: 'a', b: 'b' } + enumerator = original.transform_keys + assert_equal Enumerator, enumerator.class + end + + test "transform_keys is chainable with Enumerable methods" do + original = { a: 'a', b: 'b' } + mapped = original.transform_keys.with_index { |k, i| [k, i].join.to_sym } + assert_equal({ a0: 'a', b1: 'b' }, mapped) + end +end diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb new file mode 100644 index 0000000000..45ed11fef7 --- /dev/null +++ b/activesupport/test/core_ext/hash/transform_values_test.rb @@ -0,0 +1,61 @@ +require 'abstract_unit' +require 'active_support/core_ext/hash/indifferent_access' +require 'active_support/core_ext/hash/transform_values' + +class TransformValuesTest < ActiveSupport::TestCase + test "transform_values returns a new hash with the values computed from the block" do + original = { a: 'a', b: 'b' } + mapped = original.transform_values { |v| v + '!' } + + assert_equal({ a: 'a', b: 'b' }, original) + assert_equal({ a: 'a!', b: 'b!' }, mapped) + end + + test "transform_values! modifies the values of the original" do + original = { a: 'a', b: 'b' } + mapped = original.transform_values! { |v| v + '!' } + + assert_equal({ a: 'a!', b: 'b!' }, original) + assert_same original, mapped + end + + test "indifferent access is still indifferent after mapping values" do + original = { a: 'a', b: 'b' }.with_indifferent_access + mapped = original.transform_values { |v| v + '!' } + + assert_equal 'a!', mapped[:a] + assert_equal 'a!', mapped['a'] + end + + # This is to be consistent with the behavior of Ruby's built in methods + # (e.g. #select, #reject) as of 2.2 + test "default values do not persist during mapping" do + original = Hash.new('foo') + original[:a] = 'a' + mapped = original.transform_values { |v| v + '!' } + + assert_equal 'a!', mapped[:a] + assert_nil mapped[:b] + end + + test "default procs do not persist after mapping" do + original = Hash.new { 'foo' } + original[:a] = 'a' + mapped = original.transform_values { |v| v + '!' } + + assert_equal 'a!', mapped[:a] + assert_nil mapped[:b] + end + + test "transform_values returns an Enumerator if no block is given" do + original = { a: 'a', b: 'b' } + enumerator = original.transform_values + assert_equal Enumerator, enumerator.class + end + + test "transform_values is chainable with Enumerable methods" do + original = { a: 'a', b: 'b' } + mapped = original.transform_values.with_index { |v, i| [v, i].join } + assert_equal({ a: 'a0', b: 'b1' }, mapped) + end +end diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index eb8e87cc31..5e9fdfd872 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -586,6 +586,8 @@ class HashExtTest < ActiveSupport::TestCase roundtrip = mixed_with_default.with_indifferent_access.to_hash assert_equal @strings, roundtrip assert_equal '1234', roundtrip.default + + # Ensure nested hashes are not HashWithIndiffereneAccess new_to_hash = @nested_mixed.with_indifferent_access.to_hash assert_not new_to_hash.instance_of?(HashWithIndifferentAccess) assert_not new_to_hash["a"].instance_of?(HashWithIndifferentAccess) @@ -706,7 +708,7 @@ class HashExtTest < ActiveSupport::TestCase { :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny) end assert_equal "Unknown key: :failore. Valid keys are: :failure, :funny", exception.message - + exception = assert_raise ArgumentError do { :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure ]) end @@ -1500,12 +1502,32 @@ class HashToXmlTest < ActiveSupport::TestCase end end + def test_from_xml_array_one + expected = { 'numbers' => { 'type' => 'Array', 'value' => '1' }} + assert_equal expected, Hash.from_xml('<numbers type="Array"><value>1</value></numbers>') + end + + def test_from_xml_array_many + expected = { 'numbers' => { 'type' => 'Array', 'value' => [ '1', '2' ] }} + assert_equal expected, Hash.from_xml('<numbers type="Array"><value>1</value><value>2</value></numbers>') + end + def test_from_trusted_xml_allows_symbol_and_yaml_types expected = { 'product' => { 'name' => :value }} assert_equal expected, Hash.from_trusted_xml('<product><name type="symbol">value</name></product>') assert_equal expected, Hash.from_trusted_xml('<product><name type="yaml">:value</name></product>') end + def test_should_use_default_proc_for_unknown_key + hash_wia = HashWithIndifferentAccess.new { 1 + 2 } + assert_equal 3, hash_wia[:new_key] + end + + def test_should_use_default_proc_if_no_key_is_supplied + hash_wia = HashWithIndifferentAccess.new { 1 + 2 } + assert_equal 3, hash_wia.default + end + def test_should_use_default_value_for_unknown_key hash_wia = HashWithIndifferentAccess.new(3) assert_equal 3, hash_wia[:new_key] @@ -1533,6 +1555,17 @@ class HashToXmlTest < ActiveSupport::TestCase assert_equal 3, hash_wia.default end + def test_should_copy_the_default_proc_when_converting_to_hash_with_indifferent_access + hash = Hash.new do + 2 + 1 + end + assert_equal 3, hash[:foo] + + hash_wia = hash.with_indifferent_access + assert_equal 3, hash_wia[:foo] + assert_equal 3, hash_wia[:bar] + end + # The XML builder seems to fail miserably when trying to tag something # with the same name as a Kernel method (throw, test, loop, select ...) def test_kernel_method_names_to_xml diff --git a/activesupport/test/core_ext/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb index d8bf81d02b..a87af0007c 100644 --- a/activesupport/test/core_ext/kernel_test.rb +++ b/activesupport/test/core_ext/kernel_test.rb @@ -30,14 +30,6 @@ class KernelTest < ActiveSupport::TestCase end - def test_silence_stderr - old_stderr_position = STDERR.tell - silence_stderr { STDERR.puts 'hello world' } - assert_equal old_stderr_position, STDERR.tell - rescue Errno::ESPIPE - # Skip if we can't STDERR.tell - end - def test_silence_stream old_stream_position = STDOUT.tell silence_stream(STDOUT) { STDOUT.puts 'hello world' } @@ -56,9 +48,11 @@ class KernelTest < ActiveSupport::TestCase def test_quietly old_stdout_position, old_stderr_position = STDOUT.tell, STDERR.tell - quietly do - puts 'see me, feel me' - STDERR.puts 'touch me, heal me' + assert_deprecated do + quietly do + puts 'see me, feel me' + STDERR.puts 'touch me, heal me' + end end assert_equal old_stdout_position, STDOUT.tell assert_equal old_stderr_position, STDERR.tell @@ -66,10 +60,6 @@ class KernelTest < ActiveSupport::TestCase # Skip if we can't STDERR.tell end - def test_silence_stderr_with_return_value - assert_equal 1, silence_stderr { 1 } - end - def test_class_eval o = Object.new class << o; @x = 1; end @@ -77,10 +67,18 @@ class KernelTest < ActiveSupport::TestCase end def test_capture - assert_equal 'STDERR', capture(:stderr) { $stderr.print 'STDERR' } - assert_equal 'STDOUT', capture(:stdout) { print 'STDOUT' } - assert_equal "STDERR\n", capture(:stderr) { system('echo STDERR 1>&2') } - assert_equal "STDOUT\n", capture(:stdout) { system('echo STDOUT') } + assert_deprecated do + assert_equal 'STDERR', capture(:stderr) { $stderr.print 'STDERR' } + end + assert_deprecated do + assert_equal 'STDOUT', capture(:stdout) { print 'STDOUT' } + end + assert_deprecated do + assert_equal "STDERR\n", capture(:stderr) { system('echo STDERR 1>&2') } + end + assert_deprecated do + assert_equal "STDOUT\n", capture(:stdout) { system('echo STDOUT') } + end end end diff --git a/activesupport/test/core_ext/load_error_test.rb b/activesupport/test/core_ext/load_error_test.rb index 31863d0aca..5f804c749b 100644 --- a/activesupport/test/core_ext/load_error_test.rb +++ b/activesupport/test/core_ext/load_error_test.rb @@ -14,6 +14,15 @@ class TestMissingSourceFile < ActiveSupport::TestCase assert_equal 'nor/this/one.rb', e.path end end + + def test_is_missing + begin load 'nor_does_this_one' + rescue MissingSourceFile => e + assert e.is_missing?('nor_does_this_one') + assert e.is_missing?('nor_does_this_one.rb') + assert_not e.is_missing?('some_other_file') + end + end end class TestLoadError < ActiveSupport::TestCase @@ -29,4 +38,4 @@ class TestLoadError < ActiveSupport::TestCase assert_equal 'nor/this/one.rb', e.path end end -end
\ No newline at end of file +end diff --git a/activesupport/test/core_ext/object/acts_like_test.rb b/activesupport/test/core_ext/object/acts_like_test.rb new file mode 100644 index 0000000000..e68b1d23cb --- /dev/null +++ b/activesupport/test/core_ext/object/acts_like_test.rb @@ -0,0 +1,33 @@ +require 'abstract_unit' +require 'active_support/core_ext/object' + +class ObjectTests < ActiveSupport::TestCase + class DuckTime + def acts_like_time? + true + end + end + + def test_duck_typing + object = Object.new + time = Time.now + date = Date.today + dt = DateTime.new + duck = DuckTime.new + + assert !object.acts_like?(:time) + assert !object.acts_like?(:date) + + assert time.acts_like?(:time) + assert !time.acts_like?(:date) + + assert !date.acts_like?(:time) + assert date.acts_like?(:date) + + assert dt.acts_like?(:time) + assert dt.acts_like?(:date) + + assert duck.acts_like?(:time) + assert !duck.acts_like?(:date) + end +end diff --git a/activesupport/test/core_ext/object/instance_variables_test.rb b/activesupport/test/core_ext/object/instance_variables_test.rb new file mode 100644 index 0000000000..9f4c5dc4f1 --- /dev/null +++ b/activesupport/test/core_ext/object/instance_variables_test.rb @@ -0,0 +1,31 @@ +require 'abstract_unit' +require 'active_support/core_ext/object' + +class ObjectInstanceVariableTest < ActiveSupport::TestCase + def setup + @source, @dest = Object.new, Object.new + @source.instance_variable_set(:@bar, 'bar') + @source.instance_variable_set(:@baz, 'baz') + end + + def test_instance_variable_names + assert_equal %w(@bar @baz), @source.instance_variable_names.sort + end + + def test_instance_values + assert_equal({'bar' => 'bar', 'baz' => 'baz'}, @source.instance_values) + end + + def test_instance_exec_passes_arguments_to_block + assert_equal %w(hello goodbye), 'hello'.instance_exec('goodbye') { |v| [self, v] } + end + + def test_instance_exec_with_frozen_obj + assert_equal %w(olleh goodbye), 'hello'.freeze.instance_exec('goodbye') { |v| [reverse, v] } + end + + def test_instance_exec_nested + assert_equal %w(goodbye olleh bar), 'hello'.instance_exec('goodbye') { |arg| + [arg] + instance_exec('bar') { |v| [reverse, v] } } + end +end diff --git a/activesupport/test/core_ext/object/itself_test.rb b/activesupport/test/core_ext/object/itself_test.rb new file mode 100644 index 0000000000..65db0ddf40 --- /dev/null +++ b/activesupport/test/core_ext/object/itself_test.rb @@ -0,0 +1,9 @@ +require 'abstract_unit' +require 'active_support/core_ext/object' + +class Object::ItselfTest < ActiveSupport::TestCase + test 'itself returns self' do + object = 'fun' + assert_equal object, object.itself + end +end diff --git a/activesupport/test/core_ext/object/json_cherry_pick_test.rb b/activesupport/test/core_ext/object/json_cherry_pick_test.rb new file mode 100644 index 0000000000..2f7ea3a497 --- /dev/null +++ b/activesupport/test/core_ext/object/json_cherry_pick_test.rb @@ -0,0 +1,42 @@ +require 'abstract_unit' + +# These test cases were added to test that cherry-picking the json extensions +# works correctly, primarily for dependencies problems reported in #16131. They +# need to be executed in isolation to reproduce the scenario correctly, because +# other test cases might have already loaded additional dependencies. + +class JsonCherryPickTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def test_time_as_json + require_or_skip 'active_support/core_ext/object/json' + + expected = Time.new(2004, 7, 25) + actual = Time.parse(expected.as_json) + + assert_equal expected, actual + end + + def test_date_as_json + require_or_skip 'active_support/core_ext/object/json' + + expected = Date.new(2004, 7, 25) + actual = Date.parse(expected.as_json) + + assert_equal expected, actual + end + + def test_datetime_as_json + require_or_skip 'active_support/core_ext/object/json' + + expected = DateTime.new(2004, 7, 25) + actual = DateTime.parse(expected.as_json) + + assert_equal expected, actual + end + + private + def require_or_skip(file) + require(file) || skip("'#{file}' was already loaded") + end +end diff --git a/activesupport/test/core_ext/object/to_param_test.rb b/activesupport/test/core_ext/object/to_param_test.rb index bd7c6c422a..30a7557dc2 100644 --- a/activesupport/test/core_ext/object/to_param_test.rb +++ b/activesupport/test/core_ext/object/to_param_test.rb @@ -2,6 +2,12 @@ require 'abstract_unit' require 'active_support/core_ext/object/to_param' class ToParamTest < ActiveSupport::TestCase + class CustomString < String + def to_param + "custom-#{ self }" + end + end + def test_object foo = Object.new def foo.to_s; 'foo' end @@ -16,4 +22,16 @@ class ToParamTest < ActiveSupport::TestCase assert_equal true, true.to_param assert_equal false, false.to_param end + + def test_array + # Empty Array + assert_equal '', [].to_param + + array = [1, 2, 3, 4] + assert_equal "1/2/3/4", array.to_param + + # Array of different objects + array = [1, '3', { a: 1, b: 2 }, nil, true, false, CustomString.new('object')] + assert_equal "1/3/a=1&b=2//true/false/custom-object", array.to_param + end end diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb index 7457c4655a..09cab3ed35 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -46,6 +46,10 @@ class ToQueryTest < ActiveSupport::TestCase :person => {:id => [20, 10]} end + def test_empty_array + assert_equal "person%5B%5D=", [].to_query('person') + end + def test_nested_empty_hash assert_equal '', {}.to_query @@ -61,6 +65,16 @@ class ToQueryTest < ActiveSupport::TestCase {a: [], b: 3} end + def test_hash_with_namespace + hash = { name: 'Nakshay', nationality: 'Indian' } + assert_equal "user%5Bname%5D=Nakshay&user%5Bnationality%5D=Indian", hash.to_query('user') + end + + def test_hash_sorted_lexicographically + hash = { type: 'human', name: 'Nakshay' } + assert_equal "name=Nakshay&type=human", hash.to_query + end + private def assert_query_equal(expected, actual) assert_equal expected.split('&'), actual.to_query.split('&') diff --git a/activesupport/test/core_ext/object_and_class_ext_test.rb b/activesupport/test/core_ext/object/try_test.rb index 0f454fdd95..8b754ced53 100644 --- a/activesupport/test/core_ext/object_and_class_ext_test.rb +++ b/activesupport/test/core_ext/object/try_test.rb @@ -1,70 +1,5 @@ require 'abstract_unit' -require 'active_support/time' require 'active_support/core_ext/object' -require 'active_support/core_ext/class/subclasses' - -class ObjectTests < ActiveSupport::TestCase - class DuckTime - def acts_like_time? - true - end - end - - def test_duck_typing - object = Object.new - time = Time.now - date = Date.today - dt = DateTime.new - duck = DuckTime.new - - assert !object.acts_like?(:time) - assert !object.acts_like?(:date) - - assert time.acts_like?(:time) - assert !time.acts_like?(:date) - - assert !date.acts_like?(:time) - assert date.acts_like?(:date) - - assert dt.acts_like?(:time) - assert dt.acts_like?(:date) - - assert duck.acts_like?(:time) - assert !duck.acts_like?(:date) - end -end - -class ObjectInstanceVariableTest < ActiveSupport::TestCase - def setup - @source, @dest = Object.new, Object.new - @source.instance_variable_set(:@bar, 'bar') - @source.instance_variable_set(:@baz, 'baz') - end - - def test_instance_variable_names - assert_equal %w(@bar @baz), @source.instance_variable_names.sort - end - - def test_instance_values - object = Object.new - object.instance_variable_set :@a, 1 - object.instance_variable_set :@b, 2 - assert_equal({'a' => 1, 'b' => 2}, object.instance_values) - end - - def test_instance_exec_passes_arguments_to_block - assert_equal %w(hello goodbye), 'hello'.instance_exec('goodbye') { |v| [self, v] } - end - - def test_instance_exec_with_frozen_obj - assert_equal %w(olleh goodbye), 'hello'.freeze.instance_exec('goodbye') { |v| [reverse, v] } - end - - def test_instance_exec_nested - assert_equal %w(goodbye olleh bar), 'hello'.instance_exec('goodbye') { |arg| - [arg] + instance_exec('bar') { |v| [reverse, v] } } - end -end class ObjectTryTest < ActiveSupport::TestCase def setup @@ -141,7 +76,7 @@ class ObjectTryTest < ActiveSupport::TestCase assert_raise(NoMethodError) { klass.new.try!(:private_method) } end - + def test_try_with_private_method klass = Class.new do private diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 150e6b65fb..98c4ec6b5e 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -12,10 +12,11 @@ class RangeTest < ActiveSupport::TestCase date_range = Time.utc(2005, 12, 10, 15, 30)..Time.utc(2005, 12, 10, 17, 30) assert_equal "BETWEEN '2005-12-10 15:30:00' AND '2005-12-10 17:30:00'", date_range.to_s(:db) end - + def test_date_range assert_instance_of Range, DateTime.new..DateTime.new assert_instance_of Range, DateTime::Infinity.new..DateTime::Infinity.new + assert_instance_of Range, DateTime.new..DateTime::Infinity.new end def test_overlaps_last_inclusive @@ -116,4 +117,9 @@ class RangeTest < ActiveSupport::TestCase datetime = DateTime.now assert ((datetime - 1.hour)..datetime).each {} end + + def test_date_time_with_step + datetime = DateTime.now + assert ((datetime - 1.hour)..datetime).step(1) {} + end end diff --git a/activesupport/test/core_ext/securerandom_test.rb b/activesupport/test/core_ext/securerandom_test.rb deleted file mode 100644 index 71980f6910..0000000000 --- a/activesupport/test/core_ext/securerandom_test.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'abstract_unit' -require 'active_support/core_ext/securerandom' - -class SecureRandomExt < ActiveSupport::TestCase - def test_v3_uuids - assert_equal "3d813cbb-47fb-32ba-91df-831e1593ac29", SecureRandom.uuid_v3(SecureRandom::UUID_DNS_NAMESPACE, "www.widgets.com") - assert_equal "86df55fb-428e-3843-8583-ba3c05f290bc", SecureRandom.uuid_v3(SecureRandom::UUID_URL_NAMESPACE, "http://www.widgets.com") - assert_equal "8c29ab0e-a2dc-3482-b5eb-20cb2e2387a1", SecureRandom.uuid_v3(SecureRandom::UUID_OID_NAMESPACE, "1.2.3") - assert_equal "ee49149d-53a4-304a-890b-468229f6afc3", SecureRandom.uuid_v3(SecureRandom::UUID_X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") - end - - def test_v5_uuids - assert_equal "21f7f8de-8051-5b89-8680-0195ef798b6a", SecureRandom.uuid_v5(SecureRandom::UUID_DNS_NAMESPACE, "www.widgets.com") - assert_equal "4e570fd8-186d-5a74-90f0-4d28e34673a1", SecureRandom.uuid_v5(SecureRandom::UUID_URL_NAMESPACE, "http://www.widgets.com") - assert_equal "42d5e23b-3a02-5135-85c6-52d1102f1f00", SecureRandom.uuid_v5(SecureRandom::UUID_OID_NAMESPACE, "1.2.3") - assert_equal "fd5b2ddf-bcfe-58b6-90d6-db50f74db527", SecureRandom.uuid_v5(SecureRandom::UUID_X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US") - end - - def test_uuid_v4_alias - assert_equal SecureRandom.method(:uuid_v4), SecureRandom.method(:uuid) - end - - def test_invalid_hash_class - assert_raise ArgumentError do - SecureRandom.uuid_from_hash(Digest::SHA2, SecureRandom::UUID_OID_NAMESPACE, '1.2.3') - end - end -end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 515144245a..d77e6be595 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -216,19 +216,40 @@ class StringInflectionsTest < ActiveSupport::TestCase assert_equal "Hello Wor...", "Hello World!!".truncate(12) end - def test_truncate_with_omission_and_seperator + def test_truncate_with_omission_and_separator assert_equal "Hello[...]", "Hello World!".truncate(10, :omission => "[...]") assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => ' ') assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => ' ') assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => ' ') end - def test_truncate_with_omission_and_regexp_seperator + def test_truncate_with_omission_and_regexp_separator assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => /\s/) assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => /\s/) assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => /\s/) end + def test_truncate_words + assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3) + assert_equal "Hello Big...", "Hello Big World!".truncate_words(2) + end + + def test_truncate_words_with_omission + assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3, :omission => "[...]") + assert_equal "Hello Big[...]", "Hello Big World!".truncate_words(2, :omission => "[...]") + end + + def test_truncate_words_with_separator + assert_equal "Hello<br>Big<br>World!...", "Hello<br>Big<br>World!<br>".truncate_words(3, :separator => '<br>') + assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :separator => '<br>') + assert_equal "Hello\n<br>Big...", "Hello\n<br>Big<br>Wide<br>World!".truncate_words(2, :separator => '<br>') + end + + def test_truncate_words_with_separator_and_omission + assert_equal "Hello<br>Big<br>World![...]", "Hello<br>Big<br>World!<br>".truncate_words(3, :omission => "[...]", :separator => '<br>') + assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :omission => "[...]", :separator => '<br>') + end + def test_truncate_multibyte assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding(Encoding::UTF_8), "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding(Encoding::UTF_8).truncate(10) @@ -754,6 +775,14 @@ class OutputSafetyTest < ActiveSupport::TestCase string = "<b>hello</b>".html_safe assert_equal string, ERB::Util.html_escape(string) end + + test "ERB::Util.html_escape_once only escapes once" do + string = '1 < 2 & 3' + escaped_string = "1 < 2 & 3" + + assert_equal escaped_string, ERB::Util.html_escape_once(string) + assert_equal escaped_string, ERB::Util.html_escape_once(escaped_string) + end end class StringExcludeTest < ActiveSupport::TestCase diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 75599a71c3..3000da8da4 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -367,6 +367,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase end def test_acts_like_time + assert @twz.acts_like_time? assert @twz.acts_like?(:time) assert ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone).acts_like?(:time) end diff --git a/activesupport/test/core_ext/uri_ext_test.rb b/activesupport/test/core_ext/uri_ext_test.rb index 03e388dd7a..43a5997ddd 100644 --- a/activesupport/test/core_ext/uri_ext_test.rb +++ b/activesupport/test/core_ext/uri_ext_test.rb @@ -7,7 +7,7 @@ class URIExtTest < ActiveSupport::TestCase def test_uri_decode_handle_multibyte str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese. - parser = URI::Parser.new + parser = URI.parser assert_equal str, parser.unescape(parser.escape(str)) end end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index ef0955e1a8..5fc3de651a 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -47,18 +47,22 @@ class DependenciesTest < ActiveSupport::TestCase end def test_tracking_loaded_files - require_dependency 'dependencies/service_one' - require_dependency 'dependencies/service_two' - assert_equal 2, ActiveSupport::Dependencies.loaded.size + with_loading do + require_dependency 'dependencies/service_one' + require_dependency 'dependencies/service_two' + assert_equal 2, ActiveSupport::Dependencies.loaded.size + end ensure Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne) Object.send(:remove_const, :ServiceTwo) if Object.const_defined?(:ServiceTwo) end def test_tracking_identical_loaded_files - require_dependency 'dependencies/service_one' - require_dependency 'dependencies/service_one' - assert_equal 1, ActiveSupport::Dependencies.loaded.size + with_loading do + require_dependency 'dependencies/service_one' + require_dependency 'dependencies/service_one' + assert_equal 1, ActiveSupport::Dependencies.loaded.size + end ensure Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne) end @@ -367,11 +371,11 @@ class DependenciesTest < ActiveSupport::TestCase with_autoloading_fixtures do e = assert_raise(NameError) { A::DoesNotExist.nil? } assert_equal "uninitialized constant A::DoesNotExist", e.message - assert_equal "A::DoesNotExist", e.name + assert_equal :DoesNotExist, e.name e = assert_raise(NameError) { A::B::DoesNotExist.nil? } assert_equal "uninitialized constant A::B::DoesNotExist", e.message - assert_equal "A::B::DoesNotExist", e.name + assert_equal :DoesNotExist, e.name end end @@ -539,7 +543,7 @@ class DependenciesTest < ActiveSupport::TestCase mod = Module.new e = assert_raise(NameError) { mod::E } assert_equal 'E cannot be autoloaded from an anonymous class or module', e.message - assert_equal 'E', e.name + assert_equal :E, e.name end end @@ -990,11 +994,4 @@ class DependenciesTest < ActiveSupport::TestCase ensure ActiveSupport::Dependencies.hook! end - -private - def remove_constants(*constants) - constants.each do |constant| - Object.send(:remove_const, constant) if Object.const_defined?(constant) - end - end end diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index ee1c69502e..7aff56cbad 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -355,4 +355,21 @@ class DeprecationTest < ActiveSupport::TestCase end deprecator end + + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) + end end diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index eb8b0d878e..58fdea0972 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -509,6 +509,14 @@ class InflectorTest < ActiveSupport::TestCase end end + def test_inflections_with_uncountable_words + ActiveSupport::Inflector.inflections do |inflect| + inflect.uncountable "HTTP" + end + + assert_equal "HTTP", ActiveSupport::Inflector.pluralize("HTTP") + end + # Dups the singleton and yields, restoring the original inflections later. # Use this in tests what modify the state of the singleton. # diff --git a/activesupport/test/key_generator_test.rb b/activesupport/test/key_generator_test.rb index 525082d313..f7e8e9a795 100644 --- a/activesupport/test/key_generator_test.rb +++ b/activesupport/test/key_generator_test.rb @@ -29,4 +29,34 @@ class KeyGeneratorTest < ActiveSupport::TestCase end end +class CachingKeyGeneratorTest < ActiveSupport::TestCase + def setup + @secret = SecureRandom.hex(64) + @generator = ActiveSupport::KeyGenerator.new(@secret, :iterations=>2) + @caching_generator = ActiveSupport::CachingKeyGenerator.new(@generator) + end + + test "Generating a cached key for same salt and key size" do + derived_key = @caching_generator.generate_key("some_salt", 32) + cached_key = @caching_generator.generate_key("some_salt", 32) + + assert_equal derived_key, cached_key + assert_equal derived_key.object_id, cached_key.object_id + end + + test "Does not cache key for different salt" do + derived_key = @caching_generator.generate_key("some_salt", 32) + different_salt_key = @caching_generator.generate_key("other_salt", 32) + + assert_not_equal derived_key, different_salt_key + end + + test "Does not cache key for different length" do + derived_key = @caching_generator.generate_key("some_salt", 32) + different_length_key = @caching_generator.generate_key("some_salt", 64) + + assert_not_equal derived_key, different_length_key + end +end + end diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 659fceb852..73f640e8e5 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -3,19 +3,12 @@ require 'abstract_unit' require 'multibyte_test_helpers' require 'active_support/core_ext/string/multibyte' -class String - def __method_for_multibyte_testing_with_integer_result; 1; end - def __method_for_multibyte_testing; 'result'; end - def __method_for_multibyte_testing!; 'result'; end - def __method_for_multibyte_testing_that_returns_nil!; end -end - class MultibyteCharsTest < ActiveSupport::TestCase include MultibyteTestHelpers def setup @proxy_class = ActiveSupport::Multibyte::Chars - @chars = @proxy_class.new UNICODE_STRING + @chars = @proxy_class.new UNICODE_STRING.dup end def test_wraps_the_original_string @@ -24,6 +17,8 @@ class MultibyteCharsTest < ActiveSupport::TestCase end def test_should_allow_method_calls_to_string + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; 'result'; end } + assert_nothing_raised do @chars.__method_for_multibyte_testing end @@ -33,28 +28,36 @@ class MultibyteCharsTest < ActiveSupport::TestCase end def test_forwarded_method_calls_should_return_new_chars_instance + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; 'result'; end } + assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing assert_not_equal @chars.object_id, @chars.__method_for_multibyte_testing.object_id end def test_forwarded_bang_method_calls_should_return_the_original_chars_instance_when_result_is_not_nil + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing!; 'result'; end } + assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing! assert_equal @chars.object_id, @chars.__method_for_multibyte_testing!.object_id end def test_forwarded_bang_method_calls_should_return_nil_when_result_is_nil + @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_that_returns_nil!; end } + assert_nil @chars.__method_for_multibyte_testing_that_returns_nil! end def test_methods_are_forwarded_to_wrapped_string_for_byte_strings original_encoding = BYTE_STRING.encoding assert_equal BYTE_STRING.length, BYTE_STRING.mb_chars.length - ensure - BYTE_STRING.force_encoding(original_encoding) end def test_forwarded_method_with_non_string_result_should_be_returned_vertabim - assert_equal ''.__method_for_multibyte_testing_with_integer_result, @chars.__method_for_multibyte_testing_with_integer_result + 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 } + + assert_equal str.__method_for_multibyte_testing_with_integer_result, @chars.__method_for_multibyte_testing_with_integer_result end def test_should_concatenate @@ -103,7 +106,6 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase @chars = UNICODE_STRING.dup.mb_chars # Ruby 1.9 only supports basic whitespace @whitespace = "\n\t " - @byte_order_mark = [65279].pack('U') end def test_split_should_return_an_array_of_chars_instances diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb index 6ab8fa28ee..aba81b8248 100644 --- a/activesupport/test/multibyte_conformance_test.rb +++ b/activesupport/test/multibyte_conformance_test.rb @@ -10,7 +10,6 @@ require 'tmpdir' class Downloader def self.download(from, to) unless File.exist?(to) - $stderr.puts "Downloading #{from} to #{to}" unless File.exist?(File.dirname(to)) system "mkdir -p #{File.dirname(to)}" end @@ -22,6 +21,7 @@ class Downloader end end end + true end end @@ -31,11 +31,15 @@ class MultibyteConformanceTest < ActiveSupport::TestCase UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd" UNIDATA_FILE = '/NormalizationTest.txt' CACHE_DIR = File.join(Dir.tmpdir, 'cache') + FileUtils.mkdir_p(CACHE_DIR) + RUN_P = begin + Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE) + rescue + end def setup - FileUtils.mkdir_p(CACHE_DIR) - Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE) @proxy = ActiveSupport::Multibyte::Chars + skip "Unable to download test data" unless RUN_P end def test_normalizations_C @@ -126,4 +130,4 @@ class MultibyteConformanceTest < ActiveSupport::TestCase def inspect_codepoints(str) str.to_s.unpack("U*").map{|cp| cp.to_s(16) }.join(' ') end -end
\ No newline at end of file +end diff --git a/activesupport/test/multibyte_proxy_test.rb b/activesupport/test/multibyte_proxy_test.rb new file mode 100644 index 0000000000..d8ffd7ca9c --- /dev/null +++ b/activesupport/test/multibyte_proxy_test.rb @@ -0,0 +1,34 @@ +# encoding: utf-8 + +require 'abstract_unit' + +class MultibyteProxyText < ActiveSupport::TestCase + class AsciiOnlyEncoder + attr_reader :wrapped_string + alias to_s wrapped_string + + def initialize(string) + @wrapped_string = string.gsub(/[^\u0000-\u007F]/, '?') + end + end + + def with_custom_encoder(encoder) + original_proxy_class = ActiveSupport::Multibyte.proxy_class + + begin + ActiveSupport::Multibyte.proxy_class = encoder + + yield + ensure + ActiveSupport::Multibyte.proxy_class = original_proxy_class + end + end + + test "custom multibyte encoder" do + with_custom_encoder(AsciiOnlyEncoder) do + assert_equal "s?me string 123", "søme string 123".mb_chars.to_s + end + + assert_equal "søme string 123", "søme string 123".mb_chars.to_s + end +end diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb index fdbe2f4350..90af2dadd4 100644 --- a/activesupport/test/multibyte_test_helpers.rb +++ b/activesupport/test/multibyte_test_helpers.rb @@ -1,9 +1,9 @@ # encoding: utf-8 module MultibyteTestHelpers - UNICODE_STRING = 'こにちわ' - ASCII_STRING = 'ohayo' - BYTE_STRING = "\270\236\010\210\245".force_encoding("ASCII-8BIT") + UNICODE_STRING = 'こにちわ'.freeze + ASCII_STRING = 'ohayo'.freeze + BYTE_STRING = "\270\236\010\210\245".force_encoding("ASCII-8BIT").freeze def chars(str) ActiveSupport::Multibyte::Chars.new(str) diff --git a/activesupport/test/option_merger_test.rb b/activesupport/test/option_merger_test.rb index 9d139b61b8..4c0364e68b 100644 --- a/activesupport/test/option_merger_test.rb +++ b/activesupport/test/option_merger_test.rb @@ -79,6 +79,15 @@ class OptionMergerTest < ActiveSupport::TestCase assert_equal ActiveSupport::OptionMerger, ActiveSupport::OptionMerger.new('', '').class end + def test_option_merger_implicit_receiver + @options.with_options foo: "bar" do + merge! fizz: "buzz" + end + + expected = { hello: "world", foo: "bar", fizz: "buzz" } + assert_equal expected, @options + end + private def method_with_options(options = {}) options diff --git a/activesupport/test/subscriber_test.rb b/activesupport/test/subscriber_test.rb index 21e4ba0cee..a88d8d9eba 100644 --- a/activesupport/test/subscriber_test.rb +++ b/activesupport/test/subscriber_test.rb @@ -49,6 +49,6 @@ class SubscriberTest < ActiveSupport::TestCase def test_does_not_attach_private_methods ActiveSupport::Notifications.instrument("private_party.doodle") - assert_equal TestSubscriber.events, [] + assert_equal [], TestSubscriber.events end end diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb index 6f63a8a725..b74bc9bf20 100644 --- a/activesupport/test/test_test.rb +++ b/activesupport/test/test_test.rb @@ -188,7 +188,7 @@ class TimeHelperTest < ActiveSupport::TestCase expected_time = Time.now + 1.day travel 1.day - assert_equal expected_time, Time.now + assert_equal expected_time.to_s(:db), Time.now.to_s(:db) assert_equal expected_time.to_date, Date.today end @@ -196,11 +196,11 @@ class TimeHelperTest < ActiveSupport::TestCase expected_time = Time.now + 1.day travel 1.day do - assert_equal expected_time, Time.now + assert_equal expected_time.to_s(:db), Time.now.to_s(:db) assert_equal expected_time.to_date, Date.today end - assert_not_equal expected_time, Time.now + assert_not_equal expected_time.to_s(:db), Time.now.to_s(:db) assert_not_equal expected_time.to_date, Date.today end @@ -235,4 +235,11 @@ class TimeHelperTest < ActiveSupport::TestCase assert_not_equal expected_time, Time.now assert_not_equal Date.new(2004, 11, 24), Date.today end + + def test_travel_to_will_reset_the_usec_to_avoid_mysql_rouding + travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do + assert_equal 50, Time.now.sec + assert_equal 0, Time.now.usec + end + end end diff --git a/ci/travis.rb b/ci/travis.rb index 956a01dbee..db6e41ecfa 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -21,7 +21,8 @@ class Build 'amo' => 'activemodel', 'as' => 'activesupport', 'ar' => 'activerecord', - 'av' => 'actionview' + 'av' => 'actionview', + 'aj' => 'activejob' } attr_reader :component, :options diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index 4cd4a9d70c..2770fc73e7 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,16 +1,16 @@ * Change Posts to Articles in Getting Started sample application in order to better align with the actual guides. - * John Kelly Ferguson* + *John Kelly Ferguson* * Update all Rails 4.1.0 references to 4.1.1 within the guides and code. - * John Kelly Ferguson* + *John Kelly Ferguson* * Split up rows in the Explain Queries table of the ActiveRecord Querying section in order to improve readability. - * John Kelly Ferguson* + *John Kelly Ferguson* * Change all non-HTTP method 'post' references to 'article'. diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb index d44fd9196a..20c64b4a85 100644 --- a/guides/bug_report_templates/action_controller_master.rb +++ b/guides/bug_report_templates/action_controller_master.rb @@ -2,6 +2,9 @@ unless File.exist?('Gemfile') File.write('Gemfile', <<-GEMFILE) source 'https://rubygems.org' gem 'rails', github: 'rails/rails' + gem 'arel', github: 'rails/arel' + gem 'rack', github: 'rack/rack' + gem 'i18n', github: 'svenfuchs/i18n' GEMFILE system 'bundle' diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb index d95354e12d..e7f5d0d5ff 100644 --- a/guides/bug_report_templates/active_record_master.rb +++ b/guides/bug_report_templates/active_record_master.rb @@ -3,6 +3,8 @@ unless File.exist?('Gemfile') source 'https://rubygems.org' gem 'rails', github: 'rails/rails' gem 'arel', github: 'rails/arel' + gem 'rack', github: 'rack/rack' + gem 'i18n', github: 'svenfuchs/i18n' gem 'sqlite3' GEMFILE diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb index 489aa3ea7a..8a908a4339 100644 --- a/guides/rails_guides/levenshtein.rb +++ b/guides/rails_guides/levenshtein.rb @@ -1,31 +1,39 @@ module RailsGuides module Levenshtein - # Based on the pseudocode in http://en.wikipedia.org/wiki/Levenshtein_distance - def self.distance(s1, s2) - s = s1.unpack('U*') - t = s2.unpack('U*') - m = s.length - n = t.length + # This code is based directly on the Text gem implementation + # Returns a value representing the "cost" of transforming str1 into str2 + def self.distance str1, str2 + s = str1 + t = str2 + n = s.length + m = t.length + max = n/2 - # matrix initialization - d = [] - 0.upto(m) { |i| d << [i] } - 0.upto(n) { |j| d[0][j] = j } + return m if (0 == n) + return n if (0 == m) + return n if (n - m).abs > max - # distance computation - 1.upto(m) do |i| - 1.upto(n) do |j| - cost = s[i] == t[j] ? 0 : 1 - d[i][j] = [ - d[i-1][j] + 1, # deletion - d[i][j-1] + 1, # insertion - d[i-1][j-1] + cost, # substitution - ].min + d = (0..m).to_a + x = nil + + str1.each_char.each_with_index do |char1,i| + e = i+1 + + str2.each_char.each_with_index do |char2,j| + cost = (char1 == char2) ? 0 : 1 + x = [ + d[j+1] + 1, # insertion + e + 1, # deletion + d[j] + cost # substitution + ].min + d[j] = e + e = x end + + d[m] = x end - # all done - return d[m][n] + return x end end end diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md index 822943d81e..5f4bdaaa8f 100644 --- a/guides/source/4_1_release_notes.md +++ b/guides/source/4_1_release_notes.md @@ -157,7 +157,7 @@ By default, these preview classes live in `test/mailers/previews`. This can be configured using the `preview_path` option. See its -[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActionMailer/Base.html) +[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Previewing+emails) for a detailed write up. ### Active Record enums diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md index be007f93a7..e8d1dc361a 100644 --- a/guides/source/4_2_release_notes.md +++ b/guides/source/4_2_release_notes.md @@ -25,105 +25,183 @@ guide. Major Features -------------- +### Foreign key support + +The migration DSL now supports adding and removing foreign keys. They are dumped +to `schema.rb` as well. At this time, only the `mysql`, `mysql2` and `postgresql` +adapters support foreign keys. + +```ruby +# add a foreign key to `articles.author_id` referencing `authors.id` +add_foreign_key :articles, :authors + +# add a foreign key to `articles.author_id` referencing `users.lng_id` +add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id" + +# remove the foreign key on `accounts.branch_id` +remove_foreign_key :accounts, :branches + +# remove the foreign key on `accounts.owner_id` +remove_foreign_key :accounts, column: :owner_id +``` + +See the API documentation on +[add_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key) +and +[remove_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_foreign_key) +for a full description. Railties -------- -Please refer to the -[Changelog](https://github.com/rails/rails/blob/4-2-stable/railties/CHANGELOG.md) -for detailed changes. +Please refer to the [Changelog][railties] for detailed changes. ### Removals -* The `rails application` command has been removed without replacement. - ([Pull Request](https://github.com/rails/rails/pull/11616)) +* The `rails application` command has been removed without replacement. + ([Pull Request](https://github.com/rails/rails/pull/11616)) + +### Deprecations + +* Deprecated `Rails::Rack::LogTailer` without replacement. + ([Commit](https://github.com/rails/rails/commit/84a13e019e93efaa8994b3f8303d635a7702dbce)) ### Notable changes -* Introduced `bin/setup` script to bootstrap an application. - ([Pull Request](https://github.com/rails/rails/pull/15189)) +* Introduced `--skip-gems` option in the app generator to skip gems such as + `turbolinks` and `coffee-rails` that does not have their own specific flags. + ([Commit](https://github.com/rails/rails/commit/10565895805887d4faf004a6f71219da177f78b7)) + +* Introduced `bin/setup` script to bootstrap an application. + ([Pull Request](https://github.com/rails/rails/pull/15189)) -* Changed default value for `config.assets.digest` to `true` in development. - ([Pull Request](https://github.com/rails/rails/pull/15155)) +* Changed default value for `config.assets.digest` to `true` in development. + ([Pull Request](https://github.com/rails/rails/pull/15155)) -* Introduced an API to register new extensions for `rake notes`. - ([Pull Request](https://github.com/rails/rails/pull/14379)) +* Introduced an API to register new extensions for `rake notes`. + ([Pull Request](https://github.com/rails/rails/pull/14379)) -* Introduced `Rails.gem_version` as a convenience method to return `Gem::Version.new(Rails.version)`. - ([Pull Request](https://github.com/rails/rails/pull/14101)) +* Introduced `Rails.gem_version` as a convenience method to return `Gem::Version.new(Rails.version)`. + ([Pull Request](https://github.com/rails/rails/pull/14101)) + +* Introduced an `after_bundle` callback in the Rails templates. + ([Pull Request](https://github.com/rails/rails/pull/16359)) Action Pack ----------- -Please refer to the -[Changelog](https://github.com/rails/rails/blob/4-2-stable/actionpack/CHANGELOG.md) -for detailed changes. +Please refer to the [Changelog][action-pack] for detailed changes. + +### Removals + +* Removed deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError` + in favor of `AbstractController::Helpers::MissingHelperError`. + ([Commit](https://github.com/rails/rails/commit/a1ddde15ae0d612ff2973de9cf768ed701b594e8)) ### Deprecations -* Deprecated support for setting the `to:` option of a router to a symbol or a - string that does not contain a `#` character: +* Deprecated support for setting the `:to` option of a router to a symbol or a + string that does not contain a `#` character: - get '/posts', to: MyRackApp => (No change necessary) - get '/posts', to: 'post#index' => (No change necessary) - get '/posts', to: 'posts' => get '/posts', controller: :posts - get '/posts', to: :index => get '/posts', action: :index + ```ruby + get '/posts', to: MyRackApp => (No change necessary) + get '/posts', to: 'post#index' => (No change necessary) + get '/posts', to: 'posts' => get '/posts', controller: :posts + get '/posts', to: :index => get '/posts', action: :index + ``` - ([Commit](https://github.com/rails/rails/commit/cc26b6b7bccf0eea2e2c1a9ebdcc9d30ca7390d9)) + ([Commit](https://github.com/rails/rails/commit/cc26b6b7bccf0eea2e2c1a9ebdcc9d30ca7390d9)) ### Notable changes -* The `*_filter` family methods has been removed from the documentation. Their - usage are discouraged in favor of the `*_action` family methods: +* `render nothing: true` or rendering a `nil` body no longer add a single + space padding to the response body. + ([Pull Request](https://github.com/rails/rails/pull/14883)) + +* Introduced the `always_permitted_parameters` option to configure which + parameters are permitted globally. The default value of this configuration + is `['controller', 'action']`. + ([Pull Request](https://github.com/rails/rails/pull/15933)) + +* The `*_filter` family methods has been removed from the documentation. Their + usage are discouraged in favor of the `*_action` family methods: + + ``` + after_filter => after_action + append_after_filter => append_after_action + append_around_filter => append_around_action + append_before_filter => append_before_action + around_filter => around_action + before_filter => before_action + prepend_after_filter => prepend_after_action + prepend_around_filter => prepend_around_action + prepend_before_filter => prepend_before_action + skip_after_filter => skip_after_action + skip_around_filter => skip_around_action + skip_before_filter => skip_before_action + skip_filter => skip_action_callback + ``` + + If your application is depending on these methods, you should use the + replacement `*_action` methods instead. These methods will be deprecated in + the future and eventually removed from Rails. + + (Commit [1](https://github.com/rails/rails/commit/6c5f43bab8206747a8591435b2aa0ff7051ad3de), + [2](https://github.com/rails/rails/commit/489a8f2a44dc9cea09154ee1ee2557d1f037c7d4)) - after_filter => after_action - append_after_filter => append_after_action - append_around_filter => append_around_action - append_before_filter => append_before_action - around_filter => around_action - before_filter => before_action - prepend_after_filter => prepend_after_action - prepend_around_filter => prepend_around_action - prepend_before_filter => prepend_before_action - skip_after_filter => skip_after_action - skip_around_filter => skip_around_action - skip_before_filter => skip_before_action - skip_filter => skip_action_callback +* Added HTTP method `MKCALENDAR` from RFC-4791 + ([Pull Request](https://github.com/rails/rails/pull/15121)) + +* `*_fragment.action_controller` notifications now include the controller and action name + in the payload. + ([Pull Request](https://github.com/rails/rails/pull/14137)) + +* Segments that are passed into URL helpers are now automatically escaped. + ([Commit](https://github.com/rails/rails/commit/5460591f0226a9d248b7b4f89186bd5553e7768f)) + +* Improved Routing Error page with fuzzy matching for route search. + ([Pull Request](https://github.com/rails/rails/pull/14619)) + +* Added option to disable logging of CSRF failures. + ([Pull Request](https://github.com/rails/rails/pull/14280)) + + +Action View +------------- - If your application is depending on these methods, you should use the - replacement `*_action` methods instead. These methods will be deprecated in - the future and eventually removed from Rails. - (Commit [1](https://github.com/rails/rails/commit/6c5f43bab8206747a8591435b2aa0ff7051ad3de), - [2](https://github.com/rails/rails/commit/489a8f2a44dc9cea09154ee1ee2557d1f037c7d4)) +Please refer to the [Changelog][action-view] for detailed changes. -* Added HTTP method `MKCALENDAR` from RFC-4791 - ([Pull Request](https://github.com/rails/rails/pull/15121)) +### Deprecations -* `*_fragment.action_controller` notifications now include the controller and action name - in the payload. - ([Pull Request](https://github.com/rails/rails/pull/14137)) +* Deprecated `AbstractController::Base.parent_prefixes`. + Override `AbstractController::Base.local_prefixes` when you want to change + where to find views. + ([Pull Request](https://github.com/rails/rails/pull/15026)) -* Segments that are passed into URL helpers are now automatically escaped. - ([Commit](https://github.com/rails/rails/commit/5460591f0226a9d248b7b4f89186bd5553e7768f)) +* Deprecated `ActionView::Digestor#digest(name, format, finder, options = {})`, + arguments should be passed as a hash instead. + ([Pull Request](https://github.com/rails/rails/pull/14243)) -* Improved Routing Error page with fuzzy matching for route search. - ([Pull Request](https://github.com/rails/rails/pull/14619)) +### Notable changes -* Added option to disable logging of CSRF failures. - ([Pull Request](https://github.com/rails/rails/pull/14280)) +* The form helpers no longer generate a `<div>` element with inline CSS around + the hidden fields. + ([Pull Request](https://github.com/rails/rails/pull/14738)) Action Mailer ------------- -Please refer to the -[Changelog](https://github.com/rails/rails/blob/4-2-stable/actionmailer/CHANGELOG.md) -for detailed changes. +Please refer to the [Changelog][action-mailer] for detailed changes. ### Notable changes +* Added the `show_previews` configuration option for enabling mailer previews + outside of the development environment. + ([Pull Request](https://github.com/rails/rails/pull/15970)) + Active Record ------------- @@ -132,112 +210,193 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md) for detailed changes. +### Removals + +* Removed `cache_attributes` and friends. All attributes are cached. + ([Pull Request](https://github.com/rails/rails/pull/15429)) + +* Removed deprecated method `ActiveRecord::Base.quoted_locking_column`. + ([Pull Request](https://github.com/rails/rails/pull/15612)) + +* Removed deprecated `ActiveRecord::Migrator.proper_table_name`. Use the + `proper_table_name` instance method on `ActiveRecord::Migration` instead. + ([Pull Request](https://github.com/rails/rails/pull/15512)) + +* Removed unused `:timestamp` type. Transparently alias it to `:datetime` + in all cases. Fixes inconsistencies when column types are sent outside of + `ActiveRecord`, such as for XML Serialization. + ([Pull Request](https://github.com/rails/rails/pull/15184)) + ### Deprecations -* Deprecated using `.joins`, `.preload` and `.eager_load` with associations that - depends on the instance state (i.e. those defined with a scope that takes an - argument) without replacement. - ([Commit](https://github.com/rails/rails/commit/ed56e596a0467390011bc9d56d462539776adac1)) +* Deprecated broken support for automatic detection of counter caches on + `has_many :through` associations. You should instead manually specify the + counter cache on the `has_many` and `belongs_to` associations for the + through records. + ([Pull Request](https://github.com/rails/rails/pull/15754)) + +* Deprecated `serialized_attributes` without replacement. + ([Pull Request](https://github.com/rails/rails/pull/15704)) -* Deprecated passing Active Record objects to `.find` or `.exists?`. Call `#id` - on the objects first. - (Commit [1](https://github.com/rails/rails/commit/d92ae6ccca3bcfd73546d612efaea011270bd270), - [2](https://github.com/rails/rails/commit/d35f0033c7dec2b8d8b52058fb8db495d49596f7)) +* Deprecated returning `nil` from `column_for_attribute` when no column + exists. It will return a null object in Rails 5.0 + ([Pull Request](https://github.com/rails/rails/pull/15878)) -* Deprecated half-baked support for PostgreSQL range values with excluding - beginnings. We currently map PostgreSQL ranges to Ruby ranges. This conversion - is not fully possible because the Ruby range does not support excluded - beginnings. +* Deprecated using `.joins`, `.preload` and `.eager_load` with associations + that depends on the instance state (i.e. those defined with a scope that + takes an argument) without replacement. + ([Commit](https://github.com/rails/rails/commit/ed56e596a0467390011bc9d56d462539776adac1)) - The current solution of incrementing the beginning is not correct and is now - deprecated. For subtypes where we don't know how to increment (e.g. `#succ` - is not defined) it will raise an `ArgumentError` for ranges with excluding - beginnings. +* Deprecated passing Active Record objects to `.find` or `.exists?`. Call + `#id` on the objects first. + (Commit [1](https://github.com/rails/rails/commit/d92ae6ccca3bcfd73546d612efaea011270bd270), + [2](https://github.com/rails/rails/commit/d35f0033c7dec2b8d8b52058fb8db495d49596f7)) - ([Commit](https://github.com/rails/rails/commit/91949e48cf41af9f3e4ffba3e5eecf9b0a08bfc3)) +* Deprecated half-baked support for PostgreSQL range values with excluding + beginnings. We currently map PostgreSQL ranges to Ruby ranges. This conversion + is not fully possible because the Ruby range does not support excluded + beginnings. + + The current solution of incrementing the beginning is not correct + and is now deprecated. For subtypes where we don't know how to increment + (e.g. `#succ` is not defined) it will raise an `ArgumentError` for ranges + with excluding beginnings. + + ([Commit](https://github.com/rails/rails/commit/91949e48cf41af9f3e4ffba3e5eecf9b0a08bfc3)) ### Notable changes -* Added support for `#pretty_print` in `ActiveRecord::Base` objects. - ([Pull Request](https://github.com/rails/rails/pull/15172)) +* Added a `:required` option to singular associations, which defines a + presence validation on the association. + ([Pull Request](https://github.com/rails/rails/pull/16056)) + +* Introduced `ActiveRecord::Base#validate!` that raises `RecordInvalid` if the + record is invalid. + ([Pull Request](https://github.com/rails/rails/pull/8639)) -* PostgreSQL and SQLite adapters no longer add a default limit of 255 characters - on string columns. - ([Pull Request](https://github.com/rails/rails/pull/14579)) +* `ActiveRecord::Base#reload` now behaves the same as `m = Model.find(m.id)`, + meaning that it no longer retains the extra attributes from custom + `select`s. + ([Pull Request](https://github.com/rails/rails/pull/15866)) -* `sqlite3:///some/path` now resolves to the absolute system path `/some/path`. - For relative paths, use `sqlite3:some/path` instead. (Previously, `sqlite3:///some/path` - resolved to the relative path `some/path`. This behaviour was deprecated on - Rails 4.1.) - ([Pull Request](https://github.com/rails/rails/pull/14569)) +* Introduced the `bin/rake db:purge` task to empty the database for the + current environment. + ([Commit](https://github.com/rails/rails/commit/e2f232aba15937a4b9d14bd91e0392c6d55be58d)) -* Introduced `#validate` as an alias for `#valid?`. - ([Pull Request](https://github.com/rails/rails/pull/14456)) +* `ActiveRecord::Dirty` now detects in-place changes to mutable values. + Serialized attributes on ActiveRecord models will no longer save when + unchanged. This also works with other types such as string columns and json + columns on PostgreSQL. + (Pull Requests [1](https://github.com/rails/rails/pull/15674), + [2](https://github.com/rails/rails/pull/15786), + [3](https://github.com/rails/rails/pull/15788)) -* `#touch` now accepts multiple attributes to be touched at once. - ([Pull Request](https://github.com/rails/rails/pull/14423)) +* Added support for `#pretty_print` in `ActiveRecord::Base` objects. + ([Pull Request](https://github.com/rails/rails/pull/15172)) -* Added support for fractional seconds for MySQL 5.6 and above. - (Pull Request [1](https://github.com/rails/rails/pull/8240), [2](https://github.com/rails/rails/pull/14359)) +* PostgreSQL and SQLite adapters no longer add a default limit of 255 + characters on string columns. + ([Pull Request](https://github.com/rails/rails/pull/14579)) -* Added support for the `citext` column type in PostgreSQL adapter. - ([Pull Request](https://github.com/rails/rails/pull/12523)) +* `sqlite3:///some/path` now resolves to the absolute system path + `/some/path`. For relative paths, use `sqlite3:some/path` instead. + (Previously, `sqlite3:///some/path` resolved to the relative path + `some/path`. This behaviour was deprecated on Rails 4.1.) + ([Pull Request](https://github.com/rails/rails/pull/14569)) + +* Introduced `#validate` as an alias for `#valid?`. + ([Pull Request](https://github.com/rails/rails/pull/14456)) + +* `#touch` now accepts multiple attributes to be touched at once. + ([Pull Request](https://github.com/rails/rails/pull/14423)) + +* Added support for fractional seconds for MySQL 5.6 and above. + (Pull Request [1](https://github.com/rails/rails/pull/8240), + [2](https://github.com/rails/rails/pull/14359)) + +* Added support for the `citext` column type in PostgreSQL adapter. + ([Pull Request](https://github.com/rails/rails/pull/12523)) + +* Added support for user-created range types in PostgreSQL adapter. + ([Commit](https://github.com/rails/rails/commit/4cb47167e747e8f9dc12b0ddaf82bdb68c03e032)) -* Added support for user-created range types in PostgreSQL adapter. - ([Commit](https://github.com/rails/rails/commit/4cb47167e747e8f9dc12b0ddaf82bdb68c03e032)) Active Model ------------ -Please refer to the -[Changelog](https://github.com/rails/rails/blob/4-2-stable/activemodel/CHANGELOG.md) -for detailed changes. +Please refer to the [Changelog][active-model] for detailed changes. + +### Removals + +* Removed deprecated `Validator#setup` without replacement. + ([Pull Request](https://github.com/rails/rails/pull/15617)) ### Notable changes -* Introduced `#validate` as an alias for `#valid?`. - ([Pull Request](https://github.com/rails/rails/pull/14456)) +* Introduced `undo_changes` method in `ActiveModel::Dirty` to restore the + changed (dirty) attributes to their previous values. + ([Pull Request](https://github.com/rails/rails/pull/14861)) + +* `has_secure_password` now verifies that the given password is less than 72 + characters if validations are enabled. + ([Pull Request](https://github.com/rails/rails/pull/15708)) + +* Introduced `#validate` as an alias for `#valid?`. + ([Pull Request](https://github.com/rails/rails/pull/14456)) Active Support -------------- -Please refer to the -[Changelog](https://github.com/rails/rails/blob/4-2-stable/activesupport/CHANGELOG.md) -for detailed changes. +Please refer to the [Changelog][active-support] for detailed changes. ### Removals -* Removed deprecated `Numeric#ago`, `Numeric#until`, `Numeric#since`, - `Numeric#from_now`. ([Commit](https://github.com/rails/rails/commit/f1eddea1e3f6faf93581c43651348f48b2b7d8bb)) +* Removed deprecated `Numeric#ago`, `Numeric#until`, `Numeric#since`, + `Numeric#from_now`. + ([Commit](https://github.com/rails/rails/commit/f1eddea1e3f6faf93581c43651348f48b2b7d8bb)) -* Removed deprecated string based terminators for `ActiveSupport::Callbacks`. - ([Pull Request](https://github.com/rails/rails/pull/15100)) +* Removed deprecated string based terminators for `ActiveSupport::Callbacks`. + ([Pull Request](https://github.com/rails/rails/pull/15100)) ### Deprecations -* Deprecated `Class#superclass_delegating_accessor`, use `Class#class_attribute` - instead. ([Pull Request](https://github.com/rails/rails/pull/14271)) +* Deprecated `Class#superclass_delegating_accessor`, use + `Class#class_attribute` instead. + ([Pull Request](https://github.com/rails/rails/pull/14271)) -* Deprecated `ActiveSupport::SafeBuffer#prepend!` as `ActiveSupport::SafeBuffer#prepend` - now performs the same function. ([Pull Request](https://github.com/rails/rails/pull/14529)) +* Deprecated `ActiveSupport::SafeBuffer#prepend!` as + `ActiveSupport::SafeBuffer#prepend` now performs the same function. + ([Pull Request](https://github.com/rails/rails/pull/14529)) ### Notable changes -* The `humanize` inflector helper now strips any leading underscores. - ([Commit](https://github.com/rails/rails/commit/daaa21bc7d20f2e4ff451637423a25ff2d5e75c7)) +* Added `Hash#transform_values` and `Hash#transform_values!` to simplify a + common pattern where the values of a hash must change, but the keys are left + the same. + ([Pull Request](https://github.com/rails/rails/pull/15819)) + +* The `humanize` inflector helper now strips any leading underscores. + ([Commit](https://github.com/rails/rails/commit/daaa21bc7d20f2e4ff451637423a25ff2d5e75c7)) -* Added `SecureRandom::uuid_v3` and `SecureRandom::uuid_v5`. - ([Pull Request](https://github.com/rails/rails/pull/12016)) +* Introduce `Concern#class_methods` as an alternative to + `module ClassMethods`, as well as `Kernel#concern` to avoid the + `module Foo; extend ActiveSupport::Concern; end` boilerplate. + ([Commit](https://github.com/rails/rails/commit/b16c36e688970df2f96f793a759365b248b582ad)) -* Introduce `Concern#class_methods` as an alternative to `module ClassMethods`, - as well as `Kernel#concern` to avoid the `module Foo; extend ActiveSupport::Concern; end` - boilerplate. ([Commit](https://github.com/rails/rails/commit/b16c36e688970df2f96f793a759365b248b582ad)) Credits ------- See the [full list of contributors to Rails](http://contributors.rubyonrails.org/) for -the many people who spent many hours making Rails, the stable and robust -framework it is. Kudos to all of them. +the many people who spent many hours making Rails the stable and robust +framework it is today. Kudos to all of them. + +[railties]: https://github.com/rails/rails/blob/4-2-stable/railties/CHANGELOG.md +[action-pack]: https://github.com/rails/rails/blob/4-2-stable/actionpack/CHANGELOG.md +[action-view]: https://github.com/rails/rails/blob/4-2-stable/actionview/CHANGELOG.md +[action-mailer]: https://github.com/rails/rails/blob/4-2-stable/actionmailer/CHANGELOG.md +[active-record]: https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md +[active-model]: https://github.com/rails/rails/blob/4-2-stable/activemodel/CHANGELOG.md +[active-support]: https://github.com/rails/rails/blob/4-2-stable/activesupport/CHANGELOG.md diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb index 6ec3aa78a4..f84f1cb376 100644 --- a/guides/source/_welcome.html.erb +++ b/guides/source/_welcome.html.erb @@ -10,10 +10,10 @@ </p> <% else %> <p> - These are the new guides for Rails 4.1 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>. + These are the new guides for Rails 4.2 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>. These guides are designed to make you immediately productive with Rails, and to help you understand how all of the pieces fit together. </p> <% end %> <p> - The guides for earlier releases: <a href="http://guides.rubyonrails.org/v4.1.1/">Rails 4.1.1</a>, <a href="http://guides.rubyonrails.org/v4.0.5/">Rails 4.0.5</a>, <a href="http://guides.rubyonrails.org/v3.2.18/">Rails 3.2.18</a> and <a href="http://guides.rubyonrails.org/v2.3.11/">Rails 2.3.11</a>. + The guides for earlier releases: <a href="http://guides.rubyonrails.org/v4.1.4/">Rails 4.1.4</a>, <a href="http://guides.rubyonrails.org/v4.0.8/">Rails 4.0.8</a>, <a href="http://guides.rubyonrails.org/v3.2.19/">Rails 3.2.19</a> and <a href="http://guides.rubyonrails.org/v2.3.11/">Rails 2.3.11</a>. </p> diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index cb1c1c653d..9ad9319255 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -414,6 +414,22 @@ globally in `config/application.rb`: config.action_mailer.default_url_options = { host: 'example.com' } ``` +Because of this behavior you cannot use any of the `*_path` helpers inside of +an email. Instead you will need to use the associated `*_url` helper. For example +instead of using + +``` +<%= link_to 'welcome', welcome_path %> +``` + +You will need to use: + +``` +<%= link_to 'welcome', welcome_url %> +``` + +By using the full URL, your links will now work in your emails. + #### generating URLs with `url_for` You need to pass the `only_path: false` option when using `url_for`. This will diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md new file mode 100644 index 0000000000..ae5d21d546 --- /dev/null +++ b/guides/source/active_job_basics.md @@ -0,0 +1,253 @@ +Active Job Basics +================= + +This guide provides you with all you need to get started in creating, +enqueueing and executing background jobs. + +After reading this guide, you will know: + +* How to create jobs. +* How to enqueue jobs. +* How to run jobs in the background. +* How to send emails from your application async. + +-------------------------------------------------------------------------------- + +Introduction +------------ + +Active Job is a framework for declaring jobs and making them run on a variety +of queueing backends. These jobs can be everything from regularly scheduled +clean-ups, billing charges, or mailings. Anything that can be chopped up +into small units of work and run in parallel, really. + + +The Purpose of the Active Job +----------------------------- +The main point is to ensure that all Rails apps will have a job infrastructure +in place, even if it's in the form of an "immediate runner". We can then have +framework features and other gems build on top of that, without having to +worry about API differences between various job runners such as Delayed Job +and Resque. Picking your queuing backend becomes more of an operational concern, +then. And you'll be able to switch between them without having to rewrite your jobs. + + +Creating a Job +-------------- + +This section will provide a step-by-step guide to creating a job and enqueue it. + +### Create the Job + +Active Job provides a Rails generator to create jobs. The following will create a +job in app/jobs: + +```bash +$ bin/rails generate job guests_cleanup +create app/jobs/guests_cleanup_job.rb +``` + +You can also create a job that will run on a specific queue: + +```bash +$ bin/rails generate job guests_cleanup --queue urgent +create app/jobs/guests_cleanup_job.rb +``` + +As you can see, you can generate jobs just like you use other generators with +Rails. + +If you don't want to use a generator, you could create your own file inside of +app/jobs, just make sure that it inherits from `ActiveJob::Base`. + +Here's how a job looks like: + +```ruby +class GuestsCleanupJob < ActiveJob::Base + queue_as :default + + def perform + # Do something later + end +end +``` + +### Enqueue the Job + +Enqueue a job like so: + +```ruby +MyJob.enqueue record # Enqueue a job to be performed as soon the queueing system is free. +``` + +```ruby +MyJob.enqueue_at Date.tomorrow.noon, record # Enqueue a job to be performed tomorrow at noon. +``` + +```ruby +MyJob.enqueue_in 1.week, record # Enqueue a job to be performed 1 week from now. +``` + +That's it! + + +Job Execution +------------- + +If not adapter is set, the job is immediately executed. + +### Backends + +Active Job has adapters for the following queueing backends: + +* [Backburner](https://github.com/nesquena/backburner) +* [Delayed Job](https://github.com/collectiveidea/delayed_job) +* [Qu](https://github.com/bkeepers/qu) +* [Que](https://github.com/chanks/que) +* [QueueClassic](https://github.com/ryandotsmith/queue_classic) +* [Resque 1.x](https://github.com/resque/resque) +* [Sidekiq](https://github.com/mperham/sidekiq) +* [Sneakers](https://github.com/jondot/sneakers) +* [Sucker Punch](https://github.com/brandonhilkert/sucker_punch) + +#### Backends Features + +| | Async | Queues | Delayed | Priorities | Timeout | Retries | +|-----------------------|-------|---------|---------|-------------|---------|---------| +| **Backburner** | Yes | Yes | Yes | Yes | Job | Global | +| **Delayed Job** | Yes | Yes | Yes | Job | Global | Global | +| **Que** | Yes | Yes | Yes | Job | No | Job | +| **Queue Classic** | Yes | Yes | Gem | No | No | No | +| **Resque** | Yes | Yes | Gem | Queue | Global | ? | +| **Sidekiq** | Yes | Yes | Yes | Queue | No | Job | +| **Sneakers** | Yes | Yes | No | Queue | Queue | No | +| **Sucker Punch** | Yes | Yes | Yes | No | No | No | +| **Active Job** | Yes | Yes | WIP | No | No | No | +| **Active Job Inline** | No | Yes | N/A | N/A | N/A | N/A | + +### Change Backends + +You can easy change your adapter: + +```ruby +# be sure to have the adapter gem in your Gemfile and follow the adapter specific +# installation and deployment instructions +YourApp::Application.config.active_job.queue_adapter = :sidekiq +``` + +Queues +------ + +Most of the adapters supports multiple queues. With Active Job you can schedule the job +to run on a specific queue: + +```ruby +class GuestsCleanupJob < ActiveJob::Base + queue_as :low_priority + #.... +end +``` + +NOTE: Make sure your queueing backend "listens" on your queue name. For some backends +you need to specify the queues to listen to. + + +Callbacks +--------- + +Active Job provides hooks during the lifecycle of a job. Callbacks allows you to trigger +logic during the lifecycle of a job. + +### Available callbacks + +* before_enqueue +* around_enqueue +* after_enqueue +* before_perform +* around_perform +* after_perform + +### Usage + +```ruby +class GuestsCleanupJob < ActiveJob::Base + queue_as :default + + before_enqueue do |job| + # do somthing with the job instance + end + + around_perform do |job, block| + # do something before perform + block.call + # do something after perform + end + + def perform + # Do something later + end +end +``` + +ActionMailer +------------ +One of the most common jobs in a modern web application is sending emails outside +of the request-response cycle, so the user doesn't have to wait on it. Active Job +is integrated with Action Mailer so you can easily send emails async: + +```ruby +# Instead of the classic +UserMailer.welcome(@user).deliver + +# use #deliver later to send the email async +UserMailer.welcome(@user).deliver_later +``` + +GlobalID +-------- +Active Job supports GlobalID for parameters. This makes it possible +to pass live Active Record objects to your job instead of class/id pairs, which +you then have to manually deserialize. Before, jobs would look like this: + +```ruby +class TrashableCleanupJob + def perform(trashable_class, trashable_id, depth) + trashable = trashable_class.constantize.find(trashable_id) + trashable.cleanup(depth) + end +end +``` + +Now you can simply do: + +```ruby +class TrashableCleanupJob + def perform(trashable, depth) + trashable.cleanup(depth) + end +end +``` + +This works with any class that mixes in ActiveModel::GlobalIdentification, which +by default has been mixed into Active Model classes. + + +Exceptions +---------- +Active Job provides a way to catch exceptions raised during the execution of the +job: + +```ruby + +class GuestsCleanupJob < ActiveJob::Base + queue_as :default + + rescue_from(ActiveRecord:NotFound) do |exception| + # do something with the exception + end + + def perform + # Do something later + end +end +``` diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md index 0019d08328..3eaeeff389 100644 --- a/guides/source/active_model_basics.md +++ b/guides/source/active_model_basics.md @@ -198,3 +198,26 @@ person.valid? # => true person.token = nil person.valid? # => raises ActiveModel::StrictValidationFailed ``` + +### ActiveModel::Naming + +Naming adds a number of class methods which make the naming and routing +easier to manage. The module defines the `model_name` class method which +will define a number of accessors using some `ActiveSupport::Inflector` methods. + +```ruby +class Person + extend ActiveModel::Naming +end + +Person.model_name.name # => "Person" +Person.model_name.singular # => "person" +Person.model_name.plural # => "people" +Person.model_name.element # => "person" +Person.model_name.human # => "Person" +Person.model_name.collection # => "people" +Person.model_name.param_key # => "person" +Person.model_name.i18n_key # => :person +Person.model_name.route_key # => "people" +Person.model_name.singular_route_key # => "person" +``` diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index 21022f1abb..eff93ce41d 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -310,10 +310,10 @@ models and validate that an attribute value is not empty, is unique and not already in the database, follows a specific format and many more. Validation is a very important issue to consider when persisting to the database, so -the methods `create`, `save` and `update` take it into account when +the methods `save` and `update` take it into account when running: they return `false` when validation fails and they didn't actually perform any operation on the database. All of these have a bang counterpart (that -is, `create!`, `save!` and `update!`), which are stricter in that +is, `save!` and `update!`), which are stricter in that they raise the exception `ActiveRecord::RecordInvalid` if validation fails. A quick example to illustrate: @@ -322,8 +322,9 @@ class User < ActiveRecord::Base validates :name, presence: true end -User.create # => false -User.create! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank +user = User.new +user.save # => false +user.save! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank ``` You can learn more about validations in the [Active Record Validations diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index f0ae3c729e..9c7e60cbb0 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -15,7 +15,7 @@ After reading this guide, you will know: The Object Life Cycle --------------------- -During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this <em>object life cycle</em> so that you can control your application and its data. +During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this *object life cycle* so that you can control your application and its data. Callbacks allow you to trigger logic before or after an alteration of an object's state. diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index fd2125424b..229c6ee458 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -442,16 +442,54 @@ is reversible. Column modifiers can be applied when creating or changing a column: * `limit` Sets the maximum size of the `string/text/binary/integer` fields. -* `precision` Defines the precision for the `decimal` fields, representing the total number of digits in the number. -* `scale` Defines the scale for the `decimal` fields, representing the number of digits after the decimal point. +* `precision` Defines the precision for the `decimal` fields, representing the +total number of digits in the number. +* `scale` Defines the scale for the `decimal` fields, representing the +number of digits after the decimal point. * `polymorphic` Adds a `type` column for `belongs_to` associations. * `null` Allows or disallows `NULL` values in the column. -* `default` Allows to set a default value on the column. NOTE: If using a dynamic value (such as date), the default will only be calculated the first time (e.g. on the date the migration is applied.) +* `default` Allows to set a default value on the column. Note that if you +are using a dynamic value (such as a date), the default will only be calculated +the first time (i.e. on the date the migration is applied). * `index` Adds an index for the column. Some adapters may support additional options; see the adapter specific API docs for further information. +### Foreign Keys + +While it's not required you might want to add foreign key constraints to +[guarantee referential integrity](#active-record-and-referential-integrity). + +```ruby +add_foreign_key :articles, :authors +``` + +This adds a new foreign key to the `author_id` column of the `articles` +table. The key references the `id` column of the `articles` table. If the +column names can not be derived from the table names, you can use the +`:column` and `:primary_key` options. + +Rails will generate a name for every foreign key starting with +`fk_rails_` followed by 10 random characters. +There is a `:name` option to specify a different name if needed. + +NOTE: Active Record only supports single column foreign keys. `execute` and +`structure.sql` are required to use composite foreign keys. + +Removing a foreign key is easy as well: + +```ruby +# let Active Record figure out the column name +remove_foreign_key :accounts, :branches + +# remove foreign key for a specific column +remove_foreign_key :accounts, column: :owner_id + +# remove foreign key by name +remove_foreign_key :accounts, name: :special_fk_name +``` + ### When Helpers aren't Enough If the helpers provided by Active Record aren't enough you can use the `execute` @@ -482,6 +520,7 @@ definitions: * `add_index` * `add_reference` * `add_timestamps` +* `add_foreign_key` * `create_table` * `create_join_table` * `drop_table` (must supply a block) @@ -507,24 +546,23 @@ migration what else to do when reverting it. For example: ```ruby class ExampleMigration < ActiveRecord::Migration def change - create_table :products do |t| - t.references :category + create_table :distributors do |t| + t.string :zipcode end reversible do |dir| dir.up do - #add a foreign key + # add a CHECK constraint execute <<-SQL - ALTER TABLE products - ADD CONSTRAINT fk_products_categories - FOREIGN KEY (category_id) - REFERENCES categories(id) + ALTER TABLE distributors + ADD CONSTRAINT zipchk + CHECK (char_length(zipcode) = 5) NO INHERIT; SQL end dir.down do execute <<-SQL - ALTER TABLE products - DROP FOREIGN KEY fk_products_categories + ALTER TABLE distributors + DROP CONSTRAINT zipchk SQL end end @@ -538,7 +576,7 @@ end Using `reversible` will ensure that the instructions are executed in the right order too. If the previous example migration is reverted, the `down` block will be run after the `home_page_url` column is removed and -right before the table `products` is dropped. +right before the table `distributors` is dropped. Sometimes your migration will do something which is just plain irreversible; for example, it might destroy some data. In such cases, you can raise @@ -561,16 +599,15 @@ made in the `up` method. The example in the `reversible` section is equivalent t ```ruby class ExampleMigration < ActiveRecord::Migration def up - create_table :products do |t| - t.references :category + create_table :distributors do |t| + t.string :zipcode end - # add a foreign key + # add a CHECK constraint execute <<-SQL - ALTER TABLE products - ADD CONSTRAINT fk_products_categories - FOREIGN KEY (category_id) - REFERENCES categories(id) + ALTER TABLE distributors + ADD CONSTRAINT zipchk + CHECK (char_length(zipcode) = 5); SQL add_column :users, :home_page_url, :string @@ -582,11 +619,11 @@ class ExampleMigration < ActiveRecord::Migration remove_column :users, :home_page_url execute <<-SQL - ALTER TABLE products - DROP FOREIGN KEY fk_products_categories + ALTER TABLE distributors + DROP CONSTRAINT zipchk SQL - drop_table :products + drop_table :distributors end end ``` @@ -617,43 +654,27 @@ end The `revert` method also accepts a block of instructions to reverse. This could be useful to revert selected parts of previous migrations. For example, let's imagine that `ExampleMigration` is committed and it -is later decided it would be best to serialize the product list instead. -One could write: +is later decided it would be best to use Active Record validations, +in place of the `CHECK` constraint, to verify the zipcode. ```ruby -class SerializeProductListMigration < ActiveRecord::Migration +class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration def change - add_column :categories, :product_list - - reversible do |dir| - dir.up do - # transfer data from Products to Category#product_list - end - dir.down do - # create Products from Category#product_list - end - end - revert do # copy-pasted code from ExampleMigration - create_table :products do |t| - t.references :category - end - reversible do |dir| dir.up do - #add a foreign key + # add a CHECK constraint execute <<-SQL - ALTER TABLE products - ADD CONSTRAINT fk_products_categories - FOREIGN KEY (category_id) - REFERENCES categories(id) + ALTER TABLE distributors + ADD CONSTRAINT zipchk + CHECK (char_length(zipcode) = 5); SQL end dir.down do execute <<-SQL - ALTER TABLE products - DROP FOREIGN KEY fk_products_categories + ALTER TABLE distributors + DROP CONSTRAINT zipchk SQL end end @@ -918,10 +939,10 @@ that Active Record supports. This could be very useful if you were to distribute an application that is able to run against multiple databases. There is however a trade-off: `db/schema.rb` cannot express database specific -items such as foreign key constraints, triggers, or stored procedures. While in -a migration you can execute custom SQL statements, the schema dumper cannot -reconstitute those statements from the database. If you are using features like -this, then you should set the schema format to `:sql`. +items such as triggers, or stored procedures. While in a migration you can +execute custom SQL statements, the schema dumper cannot reconstitute those +statements from the database. If you are using features like this, then you +should set the schema format to `:sql`. Instead of using Active Record's schema dumper, the database's structure will be dumped using a tool specific to the database (via the `db:structure:dump` @@ -948,7 +969,7 @@ Active Record and Referential Integrity --------------------------------------- The Active Record way claims that intelligence belongs in your models, not in -the database. As such, features such as triggers or foreign key constraints, +the database. As such, features such as triggers or constraints, which push some of that intelligence back into the database, are not heavily used. @@ -957,14 +978,10 @@ which models can enforce data integrity. The `:dependent` option on associations allows models to automatically destroy child objects when the parent is destroyed. Like anything which operates at the application level, these cannot guarantee referential integrity and so some people augment them -with foreign key constraints in the database. - -Although Active Record does not provide any tools for working directly with -such features, the `execute` method can be used to execute arbitrary SQL. You -can also use a gem like -[foreigner](https://github.com/matthuhiggins/foreigner) which adds foreign key -support to Active Record (including support for dumping foreign keys in -`db/schema.rb`). +with [foreign key constraints](#foreign-keys) in the database. + +Although Active Record does not provide all the tools for working directly with +such features, the `execute` method can be used to execute arbitrary SQL. Migrations and Seed Data ------------------------ diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 486e7b80ff..35467fe95b 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -93,9 +93,9 @@ The primary operation of `Model.find(options)` can be summarized as: Active Record provides several different ways of retrieving a single object. -#### Using a Primary Key +#### `find` -Using `Model.find(primary_key)`, you can retrieve the object corresponding to the specified _primary key_ that matches any supplied options. For example: +Using the `find` method, you can retrieve the object corresponding to the specified _primary key_ that matches any supplied options. For example: ```ruby # Find the client with primary key (id) 10. @@ -109,119 +109,103 @@ The SQL equivalent of the above is: SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1 ``` -`Model.find(primary_key)` will raise an `ActiveRecord::RecordNotFound` exception if no matching record is found. +The `find` method will raise an `ActiveRecord::RecordNotFound` exception if no matching record is found. -#### `take` - -`Model.take` retrieves a record without any implicit ordering. For example: +You can also use this method to query for multiple objects. Call the `find` method and pass in an array of primary keys. The return will be an array containing all of the matching records for the supplied _primary keys_. For example: ```ruby -client = Client.take -# => #<Client id: 1, first_name: "Lifo"> +# Find the clients with primary keys 1 and 10. +client = Client.find([1, 10]) # Or even Client.find(1, 10) +# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">] ``` The SQL equivalent of the above is: ```sql -SELECT * FROM clients LIMIT 1 +SELECT * FROM clients WHERE (clients.id IN (1,10)) ``` -`Model.take` returns `nil` if no record is found and no exception will be raised. +WARNING: The `find` method will raise an `ActiveRecord::RecordNotFound` exception unless a matching record is found for **all** of the supplied primary keys. -TIP: The retrieved record may vary depending on the database engine. - -#### `first` +#### `take` -`Model.first` finds the first record ordered by the primary key. For example: +The `take` method retrieves a record without any implicit ordering. For example: ```ruby -client = Client.first +client = Client.take # => #<Client id: 1, first_name: "Lifo"> ``` The SQL equivalent of the above is: ```sql -SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 +SELECT * FROM clients LIMIT 1 ``` -`Model.first` returns `nil` if no matching record is found and no exception will be raised. - -#### `last` +The `take` method returns `nil` if no record is found and no exception will be raised. -`Model.last` finds the last record ordered by the primary key. For example: +You can pass in a numerical argument to the `take` method to return up to that number of results. For example ```ruby -client = Client.last -# => #<Client id: 221, first_name: "Russel"> +client = Client.take(2) +# => [ + #<Client id: 1, first_name: "Lifo">, + #<Client id: 220, first_name: "Sara"> +] ``` The SQL equivalent of the above is: ```sql -SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 -``` - -`Model.last` returns `nil` if no matching record is found and no exception will be raised. - -#### `find_by` - -`Model.find_by` finds the first record matching some conditions. For example: - -```ruby -Client.find_by first_name: 'Lifo' -# => #<Client id: 1, first_name: "Lifo"> - -Client.find_by first_name: 'Jon' -# => nil +SELECT * FROM clients LIMIT 2 ``` -It is equivalent to writing: +The `take!` method behaves exactly like `take`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. -```ruby -Client.where(first_name: 'Lifo').take -``` +TIP: The retrieved record may vary depending on the database engine. -#### `take!` +#### `first` -`Model.take!` retrieves a record without any implicit ordering. For example: +The `first` method finds the first record ordered by the primary key. For example: ```ruby -client = Client.take! +client = Client.first # => #<Client id: 1, first_name: "Lifo"> ``` The SQL equivalent of the above is: ```sql -SELECT * FROM clients LIMIT 1 +SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 ``` -`Model.take!` raises `ActiveRecord::RecordNotFound` if no matching record is found. - -#### `first!` +The `first` method returns `nil` if no matching record is found and no exception will be raised. -`Model.first!` finds the first record ordered by the primary key. For example: +You can pass in a numerical argument to the `first` method to return up to that number of results. For example ```ruby -client = Client.first! -# => #<Client id: 1, first_name: "Lifo"> +client = Client.first(3) +# => [ + #<Client id: 1, first_name: "Lifo">, + #<Client id: 2, first_name: "Fifo">, + #<Client id: 3, first_name: "Filo"> +] ``` The SQL equivalent of the above is: ```sql -SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1 +SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3 ``` -`Model.first!` raises `ActiveRecord::RecordNotFound` if no matching record is found. +The `first!` method behaves exactly like `first`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. -#### `last!` +#### `last` -`Model.last!` finds the last record ordered by the primary key. For example: +The `last` method finds the last record ordered by the primary key. For example: ```ruby -client = Client.last! +client = Client.last # => #<Client id: 221, first_name: "Russel"> ``` @@ -231,92 +215,56 @@ The SQL equivalent of the above is: SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 ``` -`Model.last!` raises `ActiveRecord::RecordNotFound` if no matching record is found. - -#### `find_by!` - -`Model.find_by!` finds the first record matching some conditions. It raises `ActiveRecord::RecordNotFound` if no matching record is found. For example: - -```ruby -Client.find_by! first_name: 'Lifo' -# => #<Client id: 1, first_name: "Lifo"> - -Client.find_by! first_name: 'Jon' -# => ActiveRecord::RecordNotFound -``` - -It is equivalent to writing: - -```ruby -Client.where(first_name: 'Lifo').take! -``` - -### Retrieving Multiple Objects +The `last` method returns `nil` if no matching record is found and no exception will be raised. -#### Using Multiple Primary Keys - -`Model.find(array_of_primary_key)` accepts an array of _primary keys_, returning an array containing all of the matching records for the supplied _primary keys_. For example: +You can pass in a numerical argument to the `last` method to return up to that number of results. For example ```ruby -# Find the clients with primary keys 1 and 10. -client = Client.find([1, 10]) # Or even Client.find(1, 10) -# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">] +client = Client.last(3) +# => [ + #<Client id: 219, first_name: "James">, + #<Client id: 220, first_name: "Sara">, + #<Client id: 221, first_name: "Russel"> +] ``` The SQL equivalent of the above is: ```sql -SELECT * FROM clients WHERE (clients.id IN (1,10)) +SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3 ``` -WARNING: `Model.find(array_of_primary_key)` will raise an `ActiveRecord::RecordNotFound` exception unless a matching record is found for **all** of the supplied primary keys. +The `last!` method behaves exactly like `last`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. -#### take +#### `find_by` -`Model.take(limit)` retrieves the first number of records specified by `limit` without any explicit ordering: +The `find_by` method finds the first record matching some conditions. For example: ```ruby -Client.take(2) -# => [#<Client id: 1, first_name: "Lifo">, - #<Client id: 2, first_name: "Raf">] -``` - -The SQL equivalent of the above is: +Client.find_by first_name: 'Lifo' +# => #<Client id: 1, first_name: "Lifo"> -```sql -SELECT * FROM clients LIMIT 2 +Client.find_by first_name: 'Jon' +# => nil ``` -#### first - -`Model.first(limit)` finds the first number of records specified by `limit` ordered by primary key: +It is equivalent to writing: ```ruby -Client.first(2) -# => [#<Client id: 1, first_name: "Lifo">, - #<Client id: 2, first_name: "Raf">] -``` - -The SQL equivalent of the above is: - -```sql -SELECT * FROM clients ORDER BY id ASC LIMIT 2 +Client.where(first_name: 'Lifo').take ``` -#### last - -`Model.last(limit)` finds the number of records specified by `limit` ordered by primary key in descending order: +The `find_by!` method behaves exactly like `find_by`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. For example: ```ruby -Client.last(2) -# => [#<Client id: 10, first_name: "Ryan">, - #<Client id: 9, first_name: "John">] +Client.find_by! first_name: 'does not exist' +# => ActiveRecord::RecordNotFound ``` -The SQL equivalent of the above is: +This is equivalent to writing: -```sql -SELECT * FROM clients ORDER BY id DESC LIMIT 2 +```ruby +Client.where(first_name: 'does not exist').take! ``` ### Retrieving Multiple Objects in Batches @@ -328,7 +276,7 @@ This may appear straightforward: ```ruby # This is very inefficient when the users table has thousands of rows. User.all.each do |user| - NewsLetter.weekly_deliver(user) + NewsMailer.weekly(user).deliver end ``` @@ -344,7 +292,15 @@ The `find_each` method retrieves a batch of records and then yields _each_ recor ```ruby User.find_each do |user| - NewsLetter.weekly_deliver(user) + NewsMailer.weekly(user).deliver +end +``` + +To add conditions to a `find_each` operation you can chain other Active Record methods such as `where`: + +```ruby +User.where(weekly_subscriber: true).find_each do |user| + NewsMailer.weekly(user).deliver end ``` @@ -360,7 +316,7 @@ The `:batch_size` option allows you to specify the number of records to be retri ```ruby User.find_each(batch_size: 5000) do |user| - NewsLetter.weekly_deliver(user) + NewsMailer.weekly(user).deliver end ``` @@ -372,7 +328,7 @@ For example, to send newsletters only to users with the primary key starting fro ```ruby User.find_each(start: 2000, batch_size: 5000) do |user| - NewsLetter.weekly_deliver(user) + NewsMailer.weekly(user).deliver end ``` @@ -707,7 +663,7 @@ Overriding Conditions You can specify certain conditions to be removed using the `unscope` method. For example: ```ruby -Article.where('id > 10').limit(20).order('id asc').except(:order) +Article.where('id > 10').limit(20).order('id asc').unscope(:order) ``` The SQL that would be executed: @@ -720,7 +676,7 @@ SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20 ``` -You can additionally unscope specific where clauses. For example: +You can also unscope specific `where` clauses. For example: ```ruby Article.where(id: 10, trashed: false).unscope(where: :id) @@ -759,8 +715,6 @@ The `reorder` method overrides the default scope order. For example: ```ruby class Article < ActiveRecord::Base - .. - .. has_many :comments, -> { order('posted_at DESC') } end @@ -1487,6 +1441,11 @@ If you'd like to use your own SQL to find records in a table you can use `find_b Client.find_by_sql("SELECT * FROM clients INNER JOIN orders ON clients.id = orders.client_id ORDER BY clients.created_at desc") +# => [ + #<Client id: 1, first_name: "Lucas" >, + #<Client id: 2, first_name: "Jan" >, + # ... +] ``` `find_by_sql` provides you with a simple way of making custom calls to the database and retrieving instantiated objects. @@ -1496,7 +1455,11 @@ Client.find_by_sql("SELECT * FROM clients `find_by_sql` has a close relative called `connection#select_all`. `select_all` will retrieve objects from the database using custom SQL just like `find_by_sql` but will not instantiate them. Instead, you will get an array of hashes where each hash indicates a record. ```ruby -Client.connection.select_all("SELECT * FROM clients WHERE id = '1'") +Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'") +# => [ + {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"}, + {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"} +] ``` ### `pluck` diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index cb459626d5..582bb240dd 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -871,7 +871,7 @@ should happen, an `Array` can be used. Moreover, you can apply both `:if` and ```ruby class Computer < ActiveRecord::Base validates :mouse, presence: true, - if: ["market.retail?", :desktop?] + if: ["market.retail?", :desktop?], unless: Proc.new { |c| c.trackpad.present? } end ``` @@ -910,8 +910,8 @@ end The easiest way to add custom validators for validating individual attributes is with the convenient `ActiveModel::EachValidator`. In this case, the custom validator class must implement a `validate_each` method which takes three -arguments: record, attribute and value which correspond to the instance, the -attribute to be validated and the value of the attribute in the passed +arguments: record, attribute, and value. These correspond to the instance, the +attribute to be validated, and the value of the attribute in the passed instance. ```ruby diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 4f37bf971a..5ed392d43d 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -1165,9 +1165,9 @@ Inserting data into HTML templates needs extra care. For example, you can't just #### Safe Strings -Active Support has the concept of <i>(html) safe</i> strings. A safe string is one that is marked as being insertable into HTML as is. It is trusted, no matter whether it has been escaped or not. +Active Support has the concept of _(html) safe_ strings. A safe string is one that is marked as being insertable into HTML as is. It is trusted, no matter whether it has been escaped or not. -Strings are considered to be <i>unsafe</i> by default: +Strings are considered to be _unsafe_ by default: ```ruby "".html_safe? # => false @@ -3672,9 +3672,9 @@ t.advance(seconds: 1) #### `Time.current` -Active Support defines `Time.current` to be today in the current time zone. That's like `Time.now`, except that it honors the user time zone, if defined. It also defines `Time.yesterday` and `Time.tomorrow`, and the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Time.current`. +Active Support defines `Time.current` to be today in the current time zone. That's like `Time.now`, except that it honors the user time zone, if defined. It also defines the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Time.current`. -When making Time comparisons using methods which honor the user time zone, make sure to use `Time.current` and not `Time.now`. There are cases where the user time zone might be in the future compared to the system time zone, which `Time.today` uses by default. This means `Time.now` may equal `Time.yesterday`. +When making Time comparisons using methods which honor the user time zone, make sure to use `Time.current` instead of `Time.now`. There are cases where the user time zone might be in the future compared to the system time zone, which `Time.now` uses by default. This means `Time.now.to_date` may equal `Date.yesterday`. #### `all_day`, `all_week`, `all_month`, `all_quarter` and `all_year` diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index 7e9b288ffd..a2ebf55335 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -79,7 +79,7 @@ used. Instead of: English ------- -Please use American English (<em>color</em>, <em>center</em>, <em>modularize</em>, etc). See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences). +Please use American English (*color*, *center*, *modularize*, etc). See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences). Example Code ------------ diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 709f9583ec..e31cefa5bb 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -124,19 +124,22 @@ with a built-in helper. In the source the generated code looked like this: The query string strategy has several disadvantages: 1. **Not all caches will reliably cache content where the filename only differs by -query parameters**<br> +query parameters** + [Steve Souders recommends](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/), "...avoiding a querystring for cacheable resources". He found that in this case 5-20% of requests will not be cached. Query strings in particular do not work at all with some CDNs for cache invalidation. -2. **The file name can change between nodes in multi-server environments.**<br> +2. **The file name can change between nodes in multi-server environments.** + The default query string in Rails 2.x is based on the modification time of the files. When assets are deployed to a cluster, there is no guarantee that the timestamps will be the same, resulting in different values being used depending on which server handles the request. -3. **Too much cache invalidation**<br> +3. **Too much cache invalidation** + When static assets are deployed with each new release of code, the mtime (time of last modification) of _all_ these files changes, forcing all remote clients to fetch them again, even when the content of those assets has not changed. @@ -490,8 +493,7 @@ The directives that work in JavaScript files also work in stylesheets one, requiring all stylesheets from the current directory. In this example, `require_self` is used. This puts the CSS contained within the -file (if any) at the precise location of the `require_self` call. If -`require_self` is called more than once, only the last call is respected. +file (if any) at the precise location of the `require_self` call. NOTE. If you want to use multiple Sass files, you should generally use the [Sass `@import` rule](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import) instead of these Sprockets directives. When using Sprockets directives, Sass files exist within diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 22f6f5e7d6..daf4113b66 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -105,7 +105,7 @@ class CreateOrders < ActiveRecord::Migration end create_table :orders do |t| - t.belongs_to :customer + t.belongs_to :customer, index: true t.datetime :order_date t.timestamps end @@ -136,7 +136,7 @@ class CreateSuppliers < ActiveRecord::Migration end create_table :accounts do |t| - t.belongs_to :supplier + t.belongs_to :supplier, index: true t.string :account_number t.timestamps end @@ -169,7 +169,7 @@ class CreateCustomers < ActiveRecord::Migration end create_table :orders do |t| - t.belongs_to :customer + t.belongs_to :customer, index:true t.datetime :order_date t.timestamps end @@ -216,8 +216,8 @@ class CreateAppointments < ActiveRecord::Migration end create_table :appointments do |t| - t.belongs_to :physician - t.belongs_to :patient + t.belongs_to :physician, index: true + t.belongs_to :patient, index: true t.datetime :appointment_date t.timestamps end @@ -295,13 +295,13 @@ class CreateAccountHistories < ActiveRecord::Migration end create_table :accounts do |t| - t.belongs_to :supplier + t.belongs_to :supplier, index: true t.string :account_number t.timestamps end create_table :account_histories do |t| - t.belongs_to :account + t.belongs_to :account, index: true t.integer :credit_rating t.timestamps end @@ -341,8 +341,8 @@ class CreateAssembliesAndParts < ActiveRecord::Migration end create_table :assemblies_parts, id: false do |t| - t.belongs_to :assembly - t.belongs_to :part + t.belongs_to :assembly, index: true + t.belongs_to :part, index: true end end end @@ -379,6 +379,8 @@ class CreateSuppliers < ActiveRecord::Migration t.string :account_number t.timestamps end + + add_index :accounts, :supplier_id end end ``` @@ -455,6 +457,8 @@ class CreatePictures < ActiveRecord::Migration t.string :imageable_type t.timestamps end + + add_index :pictures, :imageable_id end end ``` @@ -466,7 +470,7 @@ class CreatePictures < ActiveRecord::Migration def change create_table :pictures do |t| t.string :name - t.references :imageable, polymorphic: true + t.references :imageable, polymorphic: true, index: true t.timestamps end end @@ -496,7 +500,7 @@ In your migrations/schema, you will add a references column to the model itself. class CreateEmployees < ActiveRecord::Migration def change create_table :employees do |t| - t.references :manager + t.references :manager, index: true t.timestamps end end @@ -561,6 +565,8 @@ class CreateOrders < ActiveRecord::Migration t.string :order_number t.integer :customer_id end + + add_index :orders, :customer_id end end ``` @@ -594,6 +600,9 @@ class CreateAssembliesPartsJoinTable < ActiveRecord::Migration t.integer :assembly_id t.integer :part_id end + + add_index :assemblies_parts, :assembly_id + add_index :assemblies_parts, :part_id end end ``` @@ -1131,7 +1140,7 @@ The `has_one` association supports these options: ##### `:as` -Setting the `:as` option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail <a href="#polymorphic-associations">earlier in this guide</a>. +Setting the `:as` option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail [earlier in this guide](#polymorphic-associations). ##### `:autosave` @@ -1203,7 +1212,7 @@ The `:source_type` option specifies the source association type for a `has_one : ##### `:through` -The `:through` option specifies a join model through which to perform the query. `has_one :through` associations were discussed in detail <a href="#the-has-one-through-association">earlier in this guide</a>. +The `:through` option specifies a join model through which to perform the query. `has_one :through` associations were discussed in detail [earlier in this guide](#the-has-one-through-association). ##### `:validate` @@ -1497,7 +1506,7 @@ The `has_many` association supports these options: ##### `:as` -Setting the `:as` option indicates that this is a polymorphic association, as discussed <a href="#polymorphic-associations">earlier in this guide</a>. +Setting the `:as` option indicates that this is a polymorphic association, as discussed [earlier in this guide](#polymorphic-associations). ##### `:autosave` @@ -1579,7 +1588,7 @@ The `:source_type` option specifies the source association type for a `has_many ##### `:through` -The `:through` option specifies a join model through which to perform the query. `has_many :through` associations provide a way to implement many-to-many relationships, as discussed <a href="#the-has-many-through-association">earlier in this guide</a>. +The `:through` option specifies a join model through which to perform the query. `has_many :through` associations provide a way to implement many-to-many relationships, as discussed [earlier in this guide](#the-has-many-through-association). ##### `:validate` @@ -1632,7 +1641,7 @@ If you use a hash-style `where` option, then record creation via this associatio ##### `extending` -The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>. +The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail [later in this guide](#association-extensions). ##### `group` @@ -2082,7 +2091,7 @@ If you use a hash-style `where`, then record creation via this association will ##### `extending` -The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>. +The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail [later in this guide](#association-extensions). ##### `group` diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 3e39ecdad2..d0f3a596fe 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -185,7 +185,7 @@ end Cache Stores ------------ -Rails provides different stores for the cached data created by <b>action</b> and <b>fragment</b> caches. +Rails provides different stores for the cached data created by **action** and **fragment** caches. TIP: Page caches are always stored on disk. @@ -353,7 +353,12 @@ Instead of an options hash, you can also simply pass in a model, Rails will use class ProductsController < ApplicationController def show @product = Product.find(params[:id]) - respond_with(@product) if stale?(@product) + + if stale?(@product) + respond_to do |wants| + # ... normal response processing + end + end end end ``` diff --git a/guides/source/command_line.md b/guides/source/command_line.md index e283bcd0ef..a074b849c6 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -7,7 +7,6 @@ After reading this guide, you will know: * How to generate models, controllers, database migrations, and unit tests. * How to start a development server. * How to experiment with objects through an interactive shell. -* How to profile and benchmark your new creation. -------------------------------------------------------------------------------- @@ -62,7 +61,7 @@ With no further work, `rails server` will run our new shiny Rails app: $ cd commandsapp $ bin/rails server => Booting WEBrick -=> Rails 4.0.0 application starting in development on http://0.0.0.0:3000 +=> Rails 4.2.0 application starting in development on http://0.0.0.0:3000 => Call with -d to detach => Ctrl-C to shutdown server [2013-08-07 02:00:01] INFO WEBrick 1.3.1 @@ -150,8 +149,6 @@ $ bin/rails generate controller Greetings hello create test/controllers/greetings_controller_test.rb invoke helper create app/helpers/greetings_helper.rb - invoke test_unit - create test/helpers/greetings_helper_test.rb invoke assets invoke coffee create app/assets/javascripts/greetings.js.coffee @@ -237,8 +234,6 @@ $ bin/rails generate scaffold HighScore game:string score:integer create test/controllers/high_scores_controller_test.rb invoke helper create app/helpers/high_scores_helper.rb - invoke test_unit - create test/helpers/high_scores_helper_test.rb invoke jbuilder create app/views/high_scores/index.json.jbuilder create app/views/high_scores/show.json.jbuilder @@ -289,7 +284,7 @@ If you wish to test out some code without changing any data, you can do that by ```bash $ bin/rails console --sandbox -Loading development environment in sandbox (Rails 4.0.0) +Loading development environment in sandbox (Rails 4.2.0) Any modifications you make will be rolled back on exit irb(main):001:0> ``` @@ -402,13 +397,13 @@ About your application's environment Ruby version 1.9.3 (x86_64-linux) RubyGems version 1.3.6 Rack version 1.3 -Rails version 4.1.1 +Rails version 4.2.0 JavaScript Runtime Node.js (V8) -Active Record version 4.1.1 -Action Pack version 4.1.1 -Action View version 4.1.1 -Action Mailer version 4.1.1 -Active Support version 4.1.1 +Active Record version 4.2.0 +Action Pack version 4.2.0 +Action View version 4.2.0 +Action Mailer version 4.2.0 +Active Support version 4.2.0 Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag Application root /home/foobar/commandsapp Environment development diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 3d6b2f79c6..801cef5ca6 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -137,7 +137,7 @@ numbers. New applications filter out passwords by adding the following `config.f * `config.assets.enabled` a flag that controls whether the asset pipeline is enabled. It is set to true by default. -*`config.assets.raise_runtime_errors`* Set this flag to `true` to enable additional runtime error checking. Recommended in `config/environments/development.rb` to minimize unexpected behavior when deploying to `production`. +* `config.assets.raise_runtime_errors`* Set this flag to `true` to enable additional runtime error checking. Recommended in `config/environments/development.rb` to minimize unexpected behavior when deploying to `production`. * `config.assets.compress` a flag that enables the compression of compiled assets. It is explicitly set to true in `config/environments/production.rb`. @@ -151,6 +151,8 @@ pipeline is enabled. It is set to true by default. * `config.assets.prefix` defines the prefix where assets are served from. Defaults to `/assets`. +* `config.assets.manifest` defines the full path to be used for the asset precompiler's manifest file. Defaults to a file named `manifest-<random>.json` in the `config.assets.prefix` directory within the public folder. + * `config.assets.digest` enables the use of MD5 fingerprints in asset names. Set to `true` by default in `production.rb`. * `config.assets.debug` disables the concatenation and compression of assets. Set to `true` by default in `development.rb`. @@ -330,6 +332,8 @@ The schema dumper adds one additional configuration option: * `config.action_controller.action_on_unpermitted_parameters` enables logging or raising an exception if parameters that are not explicitly permitted are found. Set to `:log` or `:raise` to enable. The default value is `:log` in development and test environments, and `false` in all other environments. +* `config.action_controller.always_permitted_parameters` sets a list of whitelisted parameters that are permitted by default. The default values are `['controller', 'action']`. + ### Configuring Action Dispatch * `config.action_dispatch.session_store` sets the name of the store for session data. The default is `:cookie_store`; other valid options include `:active_record_store`, `:mem_cache_store` or the name of your own custom class. @@ -451,6 +455,18 @@ There are a number of settings available on `config.action_mailer`: config.action_mailer.interceptors = ["MailInterceptor"] ``` +* `config.action_mailer.preview_path` specifies the location of mailer previews. + + ```ruby + config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" + ``` + +* `config.action_mailer.show_previews` enable or disable mailer previews. By default this is `true` in development. + + ```ruby + config.action_mailer.show_previews = false + ``` + ### Configuring Active Support There are a few configuration options available in Active Support: @@ -982,3 +998,24 @@ If you get the above error, you might want to increase the size of connection pool by incrementing the `pool` option in `database.yml` NOTE. If you are running in a multi-threaded environment, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections. + + +Custom configuration +-------------------- + +You can configure your own code through the Rails configuration object with custom configuration. It works like this: + + ```ruby + config.x.payment_processing.schedule = :daily + config.x.payment_processing.retries = 3 + config.x.super_debugger = true + ``` + +These configuration points are then available through the configuration object: + + ```ruby + Rails.configuration.x.payment_processing.schedule # => :daily + Rails.configuration.x.payment_processing.retries # => 3 + Rails.configuration.x.super_debugger # => true + Rails.configuration.x.super_debugger.not_set # => nil + ``` diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 133ef58fd6..8bc4b10591 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -109,9 +109,7 @@ After applying their branch, test it out! Here are some things to think about: Once you're happy that the pull request contains a good change, comment on the GitHub issue indicating your approval. Your comment should indicate that you like the change and what you like about it. Something like: -<blockquote> -I like the way you've restructured that code in generate_finder_sql - much nicer. The tests look good too. -</blockquote> +>I like the way you've restructured that code in generate_finder_sql - much nicer. The tests look good too. If your comment simply says "+1", then odds are that other reviewers aren't going to take it too seriously. Show that you took the time to review the pull request. @@ -320,6 +318,12 @@ You can also run any single test separately: $ ARCONN=sqlite3 ruby -Itest test/cases/associations/has_many_associations_test.rb ``` +To run a single test against all adapters, use: + +```bash +$ bundle exec rake TEST=test/cases/associations/has_many_associations_test.rb +``` + You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` also. See the file `activerecord/RUNNING_UNIT_TESTS.rdoc` for information on running more targeted database tests, or the file `ci/travis.rb` for the test suite run by the continuous integration server. ### Warnings @@ -393,7 +397,7 @@ inside, just indent it with 4 spaces: class ArticlesController def index - respond_with Article.limit(10) + render json: Article.limit(10) end end diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index 5f738b76af..53b8566d83 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -159,10 +159,10 @@ class ArticlesController < ApplicationController def create @article = Article.new(params[:article]) logger.debug "New article: #{@article.attributes.inspect}" - logger.debug Article should be valid: #{@article.valid?}" + logger.debug "Article should be valid: #{@article.valid?}" if @article.save - flash[:notice] = Article was successfully created.' + flash[:notice] = 'Article was successfully created.' logger.debug "The article was saved and now the user is going to be redirected..." redirect_to(@article) else @@ -184,7 +184,8 @@ vbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=--b18cd92fba90eacf8137e "body"=>"I'm learning how to print in logs!!!", "published"=>"0"}, "authenticity_token"=>"2059c1286e93402e389127b1153204e0d1e275dd", "action"=>"create", "controller"=>"articles"} New article: {"updated_at"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!", - "published"=>false, "created_at"=>nil} Article should be valid: true + "published"=>false, "created_at"=>nil} +Article should be valid: true Article Create (0.000443) INSERT INTO "articles" ("updated_at", "title", "body", "published", "created_at") VALUES('2008-09-08 14:52:54', 'Debugging Rails', 'I''m learning how to print in logs!!!', 'f', '2008-09-08 14:52:54') @@ -308,7 +309,7 @@ For example: ```bash => Booting WEBrick -=> Rails 4.1.1 application starting in development on http://0.0.0.0:3000 +=> Rails 4.2.0 application starting in development on http://0.0.0.0:3000 => Run `rails server -h` for more startup options => Notice: server is listening on all interfaces (0.0.0.0). Consider using 127.0.0.1 (--binding option) => Ctrl-C to shutdown server @@ -421,11 +422,11 @@ then `backtrace` will supply the answer. --> #0 ArticlesController.index at /PathTo/project/test_app/app/controllers/articles_controller.rb:8 #1 ActionController::ImplicitRender.send_action(method#String, *args#Array) - at /PathToGems/actionpack-4.1.1/lib/action_controller/metal/implicit_render.rb:4 + at /PathToGems/actionpack-4.2.0/lib/action_controller/metal/implicit_render.rb:4 #2 AbstractController::Base.process_action(action#NilClass, *args#Array) - at /PathToGems/actionpack-4.1.1/lib/abstract_controller/base.rb:189 + at /PathToGems/actionpack-4.2.0/lib/abstract_controller/base.rb:189 #3 ActionController::Rendering.process_action(action#NilClass, *args#NilClass) - at /PathToGems/actionpack-4.1.1/lib/action_controller/metal/rendering.rb:10 + at /PathToGems/actionpack-4.2.0/lib/action_controller/metal/rendering.rb:10 ... ``` @@ -437,7 +438,7 @@ context. ``` (byebug) frame 2 -[184, 193] in /PathToGems/actionpack-4.1.1/lib/abstract_controller/base.rb +[184, 193] in /PathToGems/actionpack-4.2.0/lib/abstract_controller/base.rb 184: # is the intended way to override action dispatching. 185: # 186: # Notice that the first argument is the method to be dispatched @@ -654,7 +655,7 @@ instruction to be executed. In this case, the activesupport's `week` method. ``` (byebug) step -[50, 59] in /PathToGems/activesupport-4.1.1/lib/active_support/core_ext/numeric/time.rb +[50, 59] in /PathToGems/activesupport-4.2.0/lib/active_support/core_ext/numeric/time.rb 50: ActiveSupport::Duration.new(self * 24.hours, [[:days, self]]) 51: end 52: alias :day :days diff --git a/guides/source/engines.md b/guides/source/engines.md index 3145733d4c..24548a5b01 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -31,10 +31,12 @@ Engines are also closely related to plugins. The two share a common `lib` directory structure, and are both generated using the `rails plugin new` generator. The difference is that an engine is considered a "full plugin" by Rails (as indicated by the `--full` option that's passed to the generator -command). This guide will refer to them simply as "engines" throughout. An -engine **can** be a plugin, and a plugin **can** be an engine. +command). We'll actually be using the `--mountable` option here, which includes +all the features of `--full`, and then some. This guide will refer to these +"full plugins" simply as "engines" throughout. An engine **can** be a plugin, +and a plugin **can** be an engine. -The engine that will be created in this guide will be called "blorgh". The +The engine that will be created in this guide will be called "blorgh". This engine will provide blogging functionality to its host applications, allowing for new articles and comments to be created. At the beginning of this guide, you will be working solely within the engine itself, but in later sections you'll @@ -49,9 +51,8 @@ guide. It's important to keep in mind at all times that the application should **always** take precedence over its engines. An application is the object that -has final say in what goes on in the universe (with the universe being the -application's environment) where the engine should only be enhancing it, rather -than changing it drastically. +has final say in what goes on in its environment. The engine should +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 @@ -135,7 +136,7 @@ following to the dummy application's routes file at `test/dummy/config/routes.rb`: ```ruby -mount Blorgh::Engine, at: "blorgh" +mount Blorgh::Engine => "/blorgh" ``` ### Inside an Engine @@ -172,7 +173,7 @@ Within `lib/blorgh/engine.rb` is the base class for the engine: ```ruby module Blorgh - class Engine < Rails::Engine + class Engine < ::Rails::Engine isolate_namespace Blorgh end end @@ -321,8 +322,6 @@ invoke test_unit create test/controllers/blorgh/articles_controller_test.rb invoke helper create app/helpers/blorgh/articles_helper.rb -invoke test_unit -create test/helpers/blorgh/articles_helper_test.rb invoke assets invoke js create app/assets/javascripts/blorgh/articles.js @@ -394,7 +393,7 @@ end ``` This helps prevent conflicts with any other engine or application that may have -a article resource as well. +an article resource as well. Finally, the assets for this resource are generated in two files: `app/assets/javascripts/blorgh/articles.js` and @@ -506,8 +505,8 @@ NOTE: Because the `has_many` is defined inside a class that is inside the model for these objects, so there's no need to specify that using the `:class_name` option here. -Next, there needs to be a form so that comments can be created on a article. To add -this, put this line underneath the call to `render @article.comments` in +Next, there needs to be a form so that comments can be created on an article. To +add this, put this line underneath the call to `render @article.comments` in `app/views/blorgh/articles/show.html.erb`: ```erb @@ -559,8 +558,6 @@ invoke test_unit create test/controllers/blorgh/comments_controller_test.rb invoke helper create app/helpers/blorgh/comments_helper.rb -invoke test_unit -create test/helpers/blorgh/comments_helper_test.rb invoke assets invoke js create app/assets/javascripts/blorgh/comments.js @@ -739,13 +736,15 @@ the application. In the case of the `blorgh` engine, making articles and comment have authors would make a lot of sense. A typical application might have a `User` class that would be used to represent -authors for a article or a comment. But there could be a case where the application -calls this class something different, such as `Person`. For this reason, the -engine should not hardcode associations specifically for a `User` class. +authors for an article or a comment. But there could be a case where the +application calls this class something different, such as `Person`. For this +reason, the engine should not hardcode associations specifically for a `User` +class. To keep it simple in this case, the application will have a class called `User` -that represents the users of the application. It can be generated using this -command inside the application: +that represents the users of the application (we'll get into making this +configurable further on). It can be generated using this command inside the +application: ```bash rails g model user name:string diff --git a/guides/source/generators.md b/guides/source/generators.md index 25c67de993..2349908979 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -8,6 +8,7 @@ After reading this guide, you will know: * How to see which generators are available in your application. * How to create a generator using templates. * How Rails searches for generators before invoking them. +* How Rails internally generates Rails code from the templates. * How to customize your scaffold by creating new generators. * How to customize your scaffold by changing generator templates. * How to use fallbacks to avoid overwriting a huge set of generators. @@ -35,7 +36,7 @@ $ bin/rails generate helper --help Creating Your First Generator ----------------------------- -Since Rails 3.0, generators are built on top of [Thor](https://github.com/erikhuda/thor). Thor provides powerful options parsing and a great API for manipulating files. For instance, let's build a generator that creates an initializer file named `initializer.rb` inside `config/initializers`. +Since Rails 3.0, generators are built on top of [Thor](https://github.com/erikhuda/thor). Thor provides powerful options for parsing and a great API for manipulating files. For instance, let's build a generator that creates an initializer file named `initializer.rb` inside `config/initializers`. The first step is to create a file at `lib/generators/initializer_generator.rb` with the following content: @@ -191,8 +192,6 @@ $ bin/rails generate scaffold User name:string create test/controllers/users_controller_test.rb invoke helper create app/helpers/users_helper.rb - invoke test_unit - create test/helpers/users_helper_test.rb invoke jbuilder create app/views/users/index.json.jbuilder create app/views/users/show.json.jbuilder @@ -207,7 +206,7 @@ $ bin/rails generate scaffold User name:string Looking at this output, it's easy to understand how generators work in Rails 3.0 and above. The scaffold generator doesn't actually generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication. -Our first customization on the workflow will be to stop generating stylesheets, javascripts and test fixtures for scaffolds. We can achieve that by changing our configuration to the following: +Our first customization on the workflow will be to stop generating stylesheet, JavaScript and test fixture files for scaffolds. We can achieve that by changing our configuration to the following: ```ruby config.generators do |g| @@ -219,7 +218,7 @@ config.generators do |g| end ``` -If we generate another resource with the scaffold generator, we can see that stylesheets, javascripts and fixtures are not created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators. +If we generate another resource with the scaffold generator, we can see that stylesheet, JavaScript and fixture files are not created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators. To demonstrate this, we are going to create a new helper generator that simply adds some instance variable readers. First, we create a generator within the rails namespace, as this is where rails searches for generators used as hooks: @@ -342,6 +341,20 @@ end If you generate another resource, you can see that we get exactly the same result! This is useful if you want to customize your scaffold templates and/or layout by just creating `edit.html.erb`, `index.html.erb` and so on inside `lib/templates/erb/scaffold`. +Many scaffold templates in Rails are written in ERB tags which needs to be escaped, so that the output is a valid ERB code, that can be used correctly in Rails app. + +The following code in one of the generator file, + +```ruby +<%%= stylesheet_include_tag :application %> +``` + +when passed through the generator, would generate the following output. + +```ruby +<%= stylesheet_include_tag :application %> +``` + Adding Generators Fallbacks --------------------------- @@ -387,8 +400,6 @@ $ bin/rails generate scaffold Comment body:text create test/controllers/comments_controller_test.rb invoke my_helper create app/helpers/comments_helper.rb - invoke shoulda - create test/helpers/comments_helper_test.rb invoke jbuilder create app/views/comments/index.json.jbuilder create app/views/comments/show.json.jbuilder diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 4a0600d02a..1f91352c82 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -120,10 +120,10 @@ To verify that you have everything installed correctly, you should be able to run the following: ```bash -$ bin/rails --version +$ rails --version ``` -If it says something like "Rails 4.1.1", you are ready to continue. +If it says something like "Rails 4.2.0", you are ready to continue. ### Creating the Blog Application @@ -191,14 +191,15 @@ following in the `blog` directory: $ bin/rails server ``` -TIP: Compiling CoffeeScript to JavaScript requires a JavaScript runtime and the -absence of a runtime will give you an `execjs` error. Usually Mac OS X 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 default to the `Gemfile` in apps generated under JRuby. -You can investigate about all the supported runtimes at -[ExecJS](https://github.com/sstephenson/execjs#readme). +TIP: Compiling CoffeeScript and JavaScript asset compression requires you +have a JavaScript runtime available on your system, in the absence +of a runtime you will see an `execjs` error during asset compilation. +Usually Mac OS X and Windows come with a JavaScript runtime installed. +Rails adds the `therubyracer` gem to the generated `Gemfile` in a +commented line for new apps and you can uncomment if you need it. +`therubyrhino` is the recommended runtime for JRuby users and is added by +default to the `Gemfile` in apps generated under JRuby. You can investigate +about all the supported runtimes at [ExecJS](https://github.com/sstephenson/execjs#readme). This will fire up WEBrick, a web server distributed with Ruby by default. To see your application in action, open a browser window and navigate to @@ -256,8 +257,6 @@ invoke test_unit create test/controllers/welcome_controller_test.rb invoke helper create app/helpers/welcome_helper.rb -invoke test_unit -create test/helpers/welcome_helper_test.rb invoke assets invoke coffee create app/assets/javascripts/welcome.js.coffee @@ -450,9 +449,7 @@ available, Rails errors out. In the above image, the bottom line has been truncated. Let's see what the full thing looks like: -<blockquote> -Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views" -</blockquote> +>Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views" That's quite a lot of text! Let's quickly go through and understand what each part of it does. @@ -498,8 +495,8 @@ harmoniously! It's time to create the form for a new article. ### The first form -To create a form within this template, you will use a <em>form -builder</em>. The primary form builder for Rails is provided by a helper +To create a form within this template, you will use a *form +builder*. The primary form builder for Rails is provided by a helper method called `form_for`. To use this method, add this code into `app/views/articles/new.html.erb`: @@ -751,8 +748,7 @@ to create an article. Try it! You should get an error that looks like this: (images/getting_started/forbidden_attributes_for_new_article.png) Rails has several security features that help you write secure applications, -and you're running into one of them now. This one is called `[strong_parameters] -(http://guides.rubyonrails.org/action_controller_overview.html#strong-parameters)`, +and you're running into one of them now. This one is called [strong parameters](http://guides.rubyonrails.org/action_controller_overview.html#strong-parameters), which requires us to tell Rails exactly which parameters are allowed into our controller actions. @@ -912,7 +908,7 @@ And then finally, add the view for this action, located at </table> ``` -Now if you go to `http://localhost:3000/articles` you will see a list of all the +Now if you go to <http://localhost:3000/articles> you will see a list of all the articles that you have created. ### Adding links @@ -1007,7 +1003,7 @@ These changes will ensure that all articles have a title that is at least five characters long. Rails can validate a variety of conditions in a model, including the presence or uniqueness of columns, their format, and the existence of associated objects. Validations are covered in detail in [Active -Record Validations](active_record_validations.html) +Record Validations](active_record_validations.html). With the validation now in place, when you call `@article.save` on an invalid article, it will return `false`. If you open @@ -1108,7 +1104,7 @@ standout. Now you'll get a nice error message when saving an article without title when you attempt to do just that on the new article form -[(http://localhost:3000/articles/new)](http://localhost:3000/articles/new). +<http://localhost:3000/articles/new>: ![Form With Errors](images/getting_started/form_with_errors.png) @@ -1639,7 +1635,6 @@ This creates six files and one empty directory: | app/views/comments/ | Views of the controller are stored here | | test/controllers/comments_controller_test.rb | The test for the controller | | app/helpers/comments_helper.rb | A view helper file | -| test/helpers/comments_helper_test.rb | The test for the helper | | app/assets/javascripts/comment.js.coffee | CoffeeScript for the controller | | app/assets/stylesheets/comment.css.scss | Cascading style sheet for the controller | @@ -2012,7 +2007,7 @@ class CommentsController < ApplicationController ``` Now if you try to create a new article, you will be greeted with a basic HTTP -Authentication challenge +Authentication challenge: ![Basic HTTP Authentication Challenge](images/getting_started/challenge.png) @@ -2027,7 +2022,7 @@ along with a number of others. Security, especially in web applications, is a broad and detailed area. Security in your Rails application is covered in more depth in -The [Ruby on Rails Security Guide](security.html) +the [Ruby on Rails Security Guide](security.html). What's Next? @@ -2038,7 +2033,7 @@ update it and experiment on your own. But you don't have to do everything without help. As you need assistance getting up and running with Rails, feel free to consult these support resources: -* The [Ruby on Rails guides](index.html) +* The [Ruby on Rails Guides](index.html) * The [Ruby on Rails Tutorial](http://railstutorial.org/book) * The [Ruby on Rails mailing list](http://groups.google.com/group/rubyonrails-talk) * The [#rubyonrails](irc://irc.freenode.net/#rubyonrails) channel on irc.freenode.net diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 8340d6807f..1023598aa4 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -107,7 +107,7 @@ The **translations load path** (`I18n.load_path`) is just a Ruby Array of paths NOTE: The backend will lazy-load these translations when a translation is looked up for the first time. This makes it possible to just swap the backend with something else even after translations have already been announced. -The default `application.rb` files has instructions on how to add locales from another directory and how to set a different default locale. Just uncomment and edit the specific lines. +The default `application.rb` file has instructions on how to add locales from another directory and how to set a different default locale. Just uncomment and edit the specific lines. ```ruby # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. @@ -137,7 +137,7 @@ If you want to translate your Rails application to a **single language other tha However, you would probably like to **provide support for more locales** in your application. In such case, you need to set and pass the locale between requests. -WARNING: You may be tempted to store the chosen locale in a _session_ or a <em>cookie</em>. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [<em>RESTful</em>](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. +WARNING: You may be tempted to store the chosen locale in a _session_ or a *cookie*. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [*RESTful*](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. The _setting part_ is easy. You can set the locale in a `before_action` in the `ApplicationController` like this: @@ -262,7 +262,7 @@ get '/:locale' => 'dashboard#index' Do take special care about the **order of your routes**, so this route declaration does not "eat" other ones. (You may want to add it directly before the `root :to` declaration.) -NOTE: Have a look at two plugins which simplify work with routes in this way: Sven Fuchs's [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master) and Raul Murciano's [translate_routes](https://github.com/raul/translate_routes/tree/master). +NOTE: Have a look at two plugins which simplify working with routes in this way: Sven Fuchs's [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master) and Raul Murciano's [translate_routes](https://github.com/raul/translate_routes/tree/master). ### Setting the Locale from the Client Supplied Information @@ -437,11 +437,11 @@ TIP: Right now you might need to add some more date/time formats in order to mak ### Inflection Rules For Other Locales -Rails 4.0 allows you to define inflection rules (such as rules for singularization and pluralization) for locales other than English. In `config/initializers/inflections.rb`, you can define these rules for multiple locales. The initializer contains a default example for specifying additional rules for English; follow that format for other locales as you see fit. +Rails allows you to define inflection rules (such as rules for singularization and pluralization) for locales other than English. In `config/initializers/inflections.rb`, you can define these rules for multiple locales. The initializer contains a default example for specifying additional rules for English; follow that format for other locales as you see fit. ### Localized Views -Rails 2.3 introduces another convenient localization feature: localized views (templates). Let's say you have a _BooksController_ in your application. Your _index_ action renders content in `app/views/books/index.html.erb` template. When you put a _localized variant_ of this template: `index.es.html.erb` in the same directory, Rails will render content in this template, when the locale is set to `:es`. When the locale is set to the default locale, the generic `index.html.erb` view will be used. (Future Rails versions may well bring this _automagic_ localization to assets in `public`, etc.) +Let's say you have a _BooksController_ in your application. Your _index_ action renders content in `app/views/books/index.html.erb` template. When you put a _localized variant_ of this template: `index.es.html.erb` in the same directory, Rails will render content in this template, when the locale is set to `:es`. When the locale is set to the default locale, the generic `index.html.erb` view will be used. (Future Rails versions may well bring this _automagic_ localization to assets in `public`, etc.) You can make use of this feature, e.g. when working with a large amount of static content, which would be clumsy to put inside YAML or Ruby dictionaries. Bear in mind, though, that any change you would like to do later to the template must be propagated to all of them. diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 00b2761716..b81b048c35 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -98,9 +98,9 @@ configure the load path for your Gemfile's dependencies. A standard Rails application depends on several gems, specifically: -* abstract * actionmailer * actionpack +* actionview * activemodel * activerecord * activesupport @@ -119,7 +119,7 @@ A standard Rails application depends on several gems, specifically: * rails * railties * rake -* sqlite3-ruby +* sqlite3 * thor * treetop * tzinfo @@ -301,7 +301,7 @@ def default_options end ``` -There is no `REQUEST_METHOD` key in `ENV` so we can skip over that line. The next line merges in the options from `opt_parser` which is defined plainly in `Rack::Server` +There is no `REQUEST_METHOD` key in `ENV` so we can skip over that line. The next line merges in the options from `opt_parser` which is defined plainly in `Rack::Server`: ```ruby def opt_parser @@ -559,7 +559,7 @@ initialized. When `config/application.rb` has finished loading Rails and defined the application namespace, we go back to `config/environment.rb`, where the application is initialized. For example, if the application was called `Blog`, here we would find `Rails.application.initialize!`, which is -defined in `rails/application.rb` +defined in `rails/application.rb`. ### `railties/lib/rails/application.rb` @@ -575,7 +575,7 @@ end ``` As you can see, you can only initialize an app once. The initializers are run through -the `run_initializers` method which is defined in `railties/lib/rails/initializable.rb` +the `run_initializers` method which is defined in `railties/lib/rails/initializable.rb`: ```ruby def run_initializers(group=:default, *args) @@ -703,4 +703,4 @@ the last piece of our journey in the Rails initialization process. This high level overview will help you understand when your code is executed and how, and overall become a better Rails developer. If you still want to know more, the Rails source code itself is probably the -best place to go next.
\ No newline at end of file +best place to go next. diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md index f00f7bca1b..8b37b92139 100644 --- a/guides/source/layouts_and_rendering.md +++ b/guides/source/layouts_and_rendering.md @@ -903,7 +903,7 @@ You can also specify multiple videos to play by passing an array of videos to th This will produce: ```erb -<video><source src="trailer.ogg" /><source src="movie.ogg" /></video> +<video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video> ``` #### Linking to Audio Files with the `audio_tag` diff --git a/guides/source/plugins.md b/guides/source/plugins.md index a35648d341..f10699fbeb 100644 --- a/guides/source/plugins.md +++ b/guides/source/plugins.md @@ -45,7 +45,7 @@ $ bin/rails plugin new yaffle See usage and options by asking for help: ```bash -$ bin/rails plugin --help +$ bin/rails plugin new --help ``` Testing Your Newly Generated Plugin @@ -440,5 +440,5 @@ $ bin/rake rdoc * [Developing a RubyGem using Bundler](https://github.com/radar/guides/blob/master/gem-development.md) * [Using .gemspecs as Intended](http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended/) -* [Gemspec Reference](http://docs.rubygems.org/read/chapter/20) +* [Gemspec Reference](http://guides.rubygems.org/specification-reference/) * [GemPlugins: A Brief Introduction to the Future of Rails Plugins](http://www.intridea.com/blog/2008/6/11/gemplugins-a-brief-introduction-to-the-future-of-rails-plugins) diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md index 0bd608c007..6512b14e60 100644 --- a/guides/source/rails_application_templates.md +++ b/guides/source/rails_application_templates.md @@ -38,9 +38,11 @@ generate(:scaffold, "person name:string") route "root to: 'people#index'" rake("db:migrate") -git :init -git add: "." -git commit: %Q{ -m 'Initial commit' } +after_bundle do + git :init + git add: "." + git commit: %Q{ -m 'Initial commit' } +end ``` The following sections outline the primary methods provided by the API: @@ -228,6 +230,22 @@ git add: "." git commit: "-a -m 'Initial commit'" ``` +### after_bundle(&block) + +Registers a callback to be executed after the gems are bundled and binstubs +are generated. Useful for all generated files to version control: + +```ruby +after_bundle do + git :init + git add: '.' + git commit: "-a -m 'Initial commit'" +end +``` + +The callbacks gets executed even if `--skip-bundle` and/or `--skip-spring` has +been passed. + Advanced Usage -------------- diff --git a/guides/source/routing.md b/guides/source/routing.md index c8f8ba3044..af8c1bbcc4 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -645,6 +645,8 @@ match 'photos', to: 'photos#show', via: :all NOTE: Routing both `GET` and `POST` requests to a single action has security implications. In general, you should avoid routing all verbs to an action unless you have a good reason to. +NOTE: 'GET' in Rails won't check for CSRF token. You should never write to the database from 'GET' requests, for more information see the [security guide](security.html#csrf-countermeasures) on CSRF countermeasures. + ### Segment Constraints You can use the `:constraints` option to enforce a format for a dynamic segment: @@ -681,7 +683,7 @@ You can also constrain a route based on any method on the [Request object](actio You specify a request-based constraint the same way that you specify a segment constraint: ```ruby -get 'photos', constraints: { subdomain: 'admin' } +get 'photos', to: 'photos#index', constraints: { subdomain: 'admin' } ``` You can also specify constraints in a block form: diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md index f0230b428b..6206b3c715 100644 --- a/guides/source/ruby_on_rails_guides_guidelines.md +++ b/guides/source/ruby_on_rails_guides_guidelines.md @@ -13,17 +13,17 @@ After reading this guide, you will know: Markdown ------- -Guides are written in [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown). There is comprehensive [documentation for Markdown](http://daringfireball.net/projects/markdown/syntax), a [cheatsheet](http://daringfireball.net/projects/markdown/basics). +Guides are written in [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown). There is comprehensive [documentation for Markdown](http://daringfireball.net/projects/markdown/syntax), as well as a [cheatsheet](http://daringfireball.net/projects/markdown/basics). Prologue -------- -Each guide should start with motivational text at the top (that's the little introduction in the blue area). The prologue should tell the reader what the guide is about, and what they will learn. See for example the [Routing Guide](routing.html). +Each guide should start with motivational text at the top (that's the little introduction in the blue area). The prologue should tell the reader what the guide is about, and what they will learn. As an example, see the [Routing Guide](routing.html). -Titles +Headings ------ -The title of every guide uses `h1`; guide sections use `h2`; subsections `h3`; etc. However, the generated HTML output will have the heading tag starting from `<h2>`. +The title of every guide uses an `h1` heading; guide sections use `h2` headings; subsections use `h3` headings; etc. Note that the generated HTML output will use heading tags starting with `<h2>`. ``` Guide Title @@ -35,14 +35,14 @@ Section ### Sub Section ``` -Capitalize all words except for internal articles, prepositions, conjunctions, and forms of the verb to be: +When writing headings, capitalize all words except for prepositions, conjunctions, internal articles, and forms of the verb "to be": ``` #### Middleware Stack is an Array #### When are Objects Saved? ``` -Use the same typography as in regular text: +Use the same inline formatting as regular text: ``` ##### The `:content_type` Option @@ -51,25 +51,23 @@ Use the same typography as in regular text: API Documentation Guidelines ---------------------------- -The guides and the API should be coherent and consistent where appropriate. Please have a look at these particular sections of the [API Documentation Guidelines](api_documentation_guidelines.html): +The guides and the API should be coherent and consistent where appropriate. In particular, these sections of the [API Documentation Guidelines](api_documentation_guidelines.html) also apply to the guides: * [Wording](api_documentation_guidelines.html#wording) * [Example Code](api_documentation_guidelines.html#example-code) -* [Filenames](api_documentation_guidelines.html#filenames) +* [Filenames](api_documentation_guidelines.html#file-names) * [Fonts](api_documentation_guidelines.html#fonts) -Those guidelines apply also to guides. - HTML Guides ----------- Before generating the guides, make sure that you have the latest version of Bundler installed on your system. As of this writing, you must install Bundler 1.3.5 on your device. -To install the latest version of Bundler, simply run the `gem install bundler` command +To install the latest version of Bundler, run `gem install bundler`. ### Generation -To generate all the guides, just `cd` into the `guides` directory, run `bundle install` and execute: +To generate all the guides, just `cd` into the `guides` directory, run `bundle install`, and execute: ``` bundle exec rake guides:generate diff --git a/guides/source/security.md b/guides/source/security.md index 7e39986f8b..125dd82666 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -25,7 +25,7 @@ The Gartner Group however estimates that 75% of attacks are at the web applicati The threats against web applications include user account hijacking, bypass of access control, reading or modifying sensitive data, or presenting fraudulent content. Or an attacker might be able to install a Trojan horse program or unsolicited e-mail sending software, aim at financial enrichment or cause brand name damage by modifying company resources. In order to prevent attacks, minimize their impact and remove points of attack, first of all, you have to fully understand the attack methods in order to find the correct countermeasures. That is what this guide aims at. -In order to develop secure web applications you have to keep up to date on all layers and know your enemies. To keep up to date subscribe to security mailing lists, read security blogs and make updating and security checks a habit (check the <a href="#additional-resources">Additional Resources</a> chapter). It is done manually because that's how you find the nasty logical security problems. +In order to develop secure web applications you have to keep up to date on all layers and know your enemies. To keep up to date subscribe to security mailing lists, read security blogs and make updating and security checks a habit (check the [Additional Resources](#additional-resources) chapter). It is done manually because that's how you find the nasty logical security problems. Sessions -------- @@ -68,7 +68,7 @@ Hence, the cookie serves as temporary authentication for the web application. An * Most people don't clear out the cookies after working at a public terminal. So if the last user didn't log out of a web application, you would be able to use it as this user. Provide the user with a _log-out button_ in the web application, and _make it prominent_. -* Many cross-site scripting (XSS) exploits aim at obtaining the user's cookie. You'll read <a href="#cross-site-scripting-xss">more about XSS</a> later. +* Many cross-site scripting (XSS) exploits aim at obtaining the user's cookie. You'll read [more about XSS](#cross-site-scripting-xss) later. * Instead of stealing a cookie unknown to the attacker, they fix a user's session identifier (in the cookie) known to them. Read more about this so-called session fixation later. @@ -118,9 +118,9 @@ It works like this: * A user receives credits, the amount is stored in a session (which is a bad idea anyway, but we'll do this for demonstration purposes). * The user buys something. -* Their new, lower credit will be stored in the session. -* The dark side of the user forces them to take the cookie from the first step (which they copied) and replace the current cookie in the browser. -* The user has their credit back. +* The new adjusted credit value is stored in the session. +* The user takes the cookie from the first step (which they previously copied) and replaces the current cookie in the browser. +* The user has their original credit back. Including a nonce (a random value) in the session solves replay attacks. A nonce is valid only once, and the server has to keep track of all the valid nonces. It gets even more complicated if you have several application servers (mongrels). Storing nonces in a database table would defeat the entire purpose of CookieStore (avoiding accessing the database). @@ -187,7 +187,7 @@ This attack method works by including malicious code or a link in a page that ac ![](images/csrf.png) -In the <a href="#sessions">session chapter</a> you have learned that most Rails applications use cookie-based sessions. Either they store the session id in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is, that it will also send the cookie, if the request comes from a site of a different domain. Let's start with an example: +In the [session chapter](#sessions) you have learned that most Rails applications use cookie-based sessions. Either they store the session id in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is, that it will also send the cookie, if the request comes from a site of a different domain. Let's start with an example: * Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob's project management application, rather than an image file. * `<img src="http://www.webapp.com/project/1/destroy">` @@ -257,7 +257,7 @@ end The above method can be placed in the `ApplicationController` and will be called when a CSRF token is not present or is incorrect on a non-GET request. -Note that _cross-site scripting (XSS) vulnerabilities bypass all CSRF protections_. XSS gives the attacker access to all elements on a page, so they can read the CSRF security token from a form or directly submit the form. Read <a href="#cross-site-scripting-xss">more about XSS</a> later. +Note that _cross-site scripting (XSS) vulnerabilities bypass all CSRF protections_. XSS gives the attacker access to all elements on a page, so they can read the CSRF security token from a form or directly submit the form. Read [more about XSS](#cross-site-scripting-xss) later. Redirection and Files --------------------- @@ -477,7 +477,7 @@ config.filter_parameters << :password INFO: _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._ -Bruce Schneier, a security technologist, [has analyzed](http://www.schneier.com/blog/archives/2006/12/realworld_passw.html) 34,000 real-world user names and passwords from the MySpace phishing attack mentioned <a href="#examples-from-the-underground">below</a>. It turns out that most of the passwords are quite easy to crack. The 20 most common passwords are: +Bruce Schneier, a security technologist, [has analyzed](http://www.schneier.com/blog/archives/2006/12/realworld_passw.html) 34,000 real-world user names and passwords from the MySpace phishing attack mentioned [below](#examples-from-the-underground). It turns out that most of the passwords are quite easy to crack. The 20 most common passwords are: password1, abc123, myspace1, password, blink182, qwerty1, ****you, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, and monkey. @@ -630,7 +630,7 @@ Also, the second query renames some columns with the AS statement so that the we #### Countermeasures -Ruby on Rails has a built-in filter for special SQL characters, which will escape ' , " , NULL character and line breaks. <em class="highlight">Using `Model.find(id)` or `Model.find_by_some thing(something)` automatically applies this countermeasure</em>. But in SQL fragments, especially <em class="highlight">in conditions fragments (`where("...")`), the `connection.execute()` or `Model.find_by_sql()` methods, it has to be applied manually</em>. +Ruby on Rails has a built-in filter for special SQL characters, which will escape ' , " , NULL character and line breaks. *Using `Model.find(id)` or `Model.find_by_some thing(something)` automatically applies this countermeasure*. But in SQL fragments, especially *in conditions fragments (`where("...")`), the `connection.execute()` or `Model.find_by_sql()` methods, it has to be applied manually*. Instead of passing a string to the conditions option, you can pass an array to sanitize tainted strings like this: @@ -847,7 +847,7 @@ It is recommended to _use RedCloth in combination with a whitelist input filter_ NOTE: _The same security precautions have to be taken for Ajax actions as for "normal" ones. There is at least one exception, however: The output has to be escaped in the controller already, if the action doesn't render a view._ -If you use the [in_place_editor plugin](http://dev.rubyonrails.org/browser/plugins/in_place_editing), or actions that return a string, rather than rendering a view, _you have to escape the return value in the action_. Otherwise, if the return value contains a XSS string, the malicious code will be executed upon return to the browser. Escape any input value using the h() method. +If you use the [in_place_editor plugin](https://rubygems.org/gems/in_place_editing), or actions that return a string, rather than rendering a view, _you have to escape the return value in the action_. Otherwise, if the return value contains a XSS string, the malicious code will be executed upon return to the browser. Escape any input value using the h() method. ### Command Line Injection diff --git a/guides/source/testing.md b/guides/source/testing.md index c01b2e575a..2ecd560a87 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -144,7 +144,7 @@ In Rails, models tests are what you write to test your models. For this guide we will be using Rails _scaffolding_. It will create the model, a migration, controller and views for the new resource in a single operation. It will also create a full test suite following Rails best practices. We will be using examples from this generated code and will be supplementing it with additional examples where necessary. -NOTE: For more information on Rails <i>scaffolding</i>, refer to [Getting Started with Rails](getting_started.html) +NOTE: For more information on Rails _scaffolding_, refer to [Getting Started with Rails](getting_started.html) When you use `rails generate scaffold`, for a resource among other things it creates a test stub in the `test/models` folder: @@ -365,7 +365,7 @@ Ideally, you would like to include a test for everything which could possibly br By now you've caught a glimpse of some of the assertions that are available. Assertions are the worker bees of testing. They are the ones that actually perform the checks to ensure that things are going as planned. There are a bunch of different types of assertions you can use. -Here's an extract of the assertions you can use with `minitest`, the default testing library used by Rails. The `[msg]` parameter is an optional string message you can specify to make your test failure messages clearer. It's not required. +Here's an extract of the assertions you can use with [`Minitest`](https://github.com/seattlerb/minitest), the default testing library used by Rails. The `[msg]` parameter is an optional string message you can specify to make your test failure messages clearer. It's not required. | Assertion | Purpose | | ---------------------------------------------------------------- | ------- | @@ -377,8 +377,12 @@ Here's an extract of the assertions you can use with `minitest`, the default tes | `assert_not_same( expected, actual, [msg] )` | Ensures that `expected.equal?(actual)` is false.| | `assert_nil( obj, [msg] )` | Ensures that `obj.nil?` is true.| | `assert_not_nil( obj, [msg] )` | Ensures that `obj.nil?` is false.| +| `assert_empty( obj, [msg] )` | Ensures that `obj` is `empty?`.| +| `assert_not_empty( obj, [msg] )` | Ensures that `obj` is not `empty?`.| | `assert_match( regexp, string, [msg] )` | Ensures that a string matches the regular expression.| | `assert_no_match( regexp, string, [msg] )` | Ensures that a string doesn't match the regular expression.| +| `assert_includes( collection, obj, [msg] )` | Ensures that `obj` is in `collection`.| +| `assert_not_includes( collection, obj, [msg] )` | Ensures that `obj` is not in `collection`.| | `assert_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.| | `assert_not_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.| | `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.| @@ -392,9 +396,13 @@ Here's an extract of the assertions you can use with `minitest`, the default tes | `assert_not_respond_to( obj, symbol, [msg] )` | Ensures that `obj` does not respond to `symbol`.| | `assert_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is true.| | `assert_not_operator( obj1, operator, [obj2], [msg] )` | Ensures that `obj1.operator(obj2)` is false.| +| `assert_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is true, e.g. `assert_predicate str, :empty?`| +| `assert_not_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is false, e.g. `assert_not_predicate str, :empty?`| | `assert_send( array, [msg] )` | Ensures that executing the method listed in `array[1]` on the object in `array[0]` with the parameters of `array[2 and up]` is true. This one is weird eh?| | `flunk( [msg] )` | Ensures failure. This is useful to explicitly mark a test that isn't finished yet.| +The above are subset of assertions that minitest supports. For an exhaustive & more up-to-date list, please check [Minitest API documentation](http://docs.seattlerb.org/minitest/), specifically [`Minitest::Assertions`](http://docs.seattlerb.org/minitest/Minitest/Assertions.html) + Because of the modular nature of the testing framework, it is possible to create your own assertions. In fact, that's exactly what Rails does. It includes some specialized assertions to make your life easier. NOTE: Creating your own assertions is an advanced topic that we won't cover in this tutorial. @@ -585,7 +593,7 @@ Here's another example that uses `flash`, `assert_redirected_to`, and `assert_di ```ruby test "should create article" do - assert_difference('article.count') do + assert_difference('Article.count') do post :create, article: {title: 'Hi', body: 'This is my first article.'} end assert_redirected_to article_path(assigns(:article)) @@ -597,13 +605,13 @@ end Testing the response to your request by asserting the presence of key HTML elements and their content is a useful way to test the views of your application. The `assert_select` assertion allows you to do this by using a simple yet powerful syntax. -NOTE: You may find references to `assert_tag` in other documentation, but this is now deprecated in favor of `assert_select`. +NOTE: You may find references to `assert_tag` in other documentation. This has been removed in 4.2. Use `assert_select` instead. There are two forms of `assert_select`: -`assert_select(selector, [equality], [message])` ensures that the equality condition is met on the selected elements through the selector. The selector may be a CSS selector expression (String), an expression with substitution values, or an `HTML::Selector` object. +`assert_select(selector, [equality], [message])` ensures that the equality condition is met on the selected elements through the selector. The selector may be a CSS selector expression (String) or an expression with substitution values. -`assert_select(element, selector, [equality], [message])` ensures that the equality condition is met on all the selected elements through the selector starting from the _element_ (instance of `HTML::Node`) and its descendants. +`assert_select(element, selector, [equality], [message])` ensures that the equality condition is met on all the selected elements through the selector starting from the _element_ (instance of `Nokogiri::XML::Node` or `Nokogiri::XML::NodeSet`) and its descendants. For example, you could verify the contents on the title element in your response with: @@ -633,7 +641,7 @@ assert_select "ol" do end ``` -The `assert_select` assertion is quite powerful. For more advanced usage, refer to its [documentation](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/SelectorAssertions.html). +The `assert_select` assertion is quite powerful. For more advanced usage, refer to its [documentation](https://github.com/rails/rails-dom-testing/blob/master/lib/rails/dom/testing/assertions/selector_assertions.rb). #### Additional View-Based Assertions @@ -996,7 +1004,7 @@ class UserControllerTest < ActionController::TestCase assert_equal "You have been invited by me@example.com", invite_email.subject assert_equal 'friend@example.com', invite_email.to[0] - assert_match(/Hi friend@example.com/, invite_email.body) + assert_match(/Hi friend@example.com/, invite_email.body.to_s) end end ``` @@ -1006,17 +1014,9 @@ Testing helpers In order to test helpers, all you need to do is check that the output of the helper method matches what you'd expect. Tests related to the helpers are -located under the `test/helpers` directory. Rails provides a generator which -generates both the helper and the test file: - -```bash -$ bin/rails generate helper User - create app/helpers/user_helper.rb - invoke test_unit - create test/helpers/user_helper_test.rb -``` +located under the `test/helpers` directory. -The generated test file contains the following code: +A helper test looks like so: ```ruby require 'test_helper' @@ -1049,7 +1049,6 @@ The built-in `minitest` based testing is not the only way to test Rails applicat * [NullDB](http://avdi.org/projects/nulldb/), a way to speed up testing by avoiding database use. * [Factory Girl](https://github.com/thoughtbot/factory_girl/tree/master), a replacement for fixtures. -* [Machinist](https://github.com/notahat/machinist/tree/master), another replacement for fixtures. * [Fixture Builder](https://github.com/rdy/fixture_builder), a tool that compiles Ruby factories into fixtures before a test run. * [MiniTest::Spec Rails](https://github.com/metaskills/minitest-spec-rails), use the MiniTest::Spec DSL within your rails tests. * [Shoulda](http://www.thoughtbot.com/projects/shoulda), an extension to `test/unit` with additional helpers, macros, and assertions. diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index d1d24eac66..799d5f3bc9 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -3,6 +3,8 @@ A Guide for Upgrading Ruby on Rails This guide provides steps to be followed when you upgrade your applications to a newer version of Ruby on Rails. These steps are also available in individual release guides. +-------------------------------------------------------------------------------- + General Advice -------------- @@ -50,6 +52,76 @@ Upgrading from Rails 4.1 to Rails 4.2 NOTE: This section is a work in progress. +### Serialized attributes + +When using a custom coder (e.g. `serialize :metadata, JSON`), +assigning `nil` to a serialized attribute will save it to the database +as `NULL` instead of passing the `nil` value through the coder (e.g. `"null"` +when using the `JSON` coder). + +### `after_bundle` in Rails templates + +If you have a Rails template that adds all the files in version control, it +fails to add the generated binstubs because it gets executed before Bundler: + +```ruby +# template.rb +generate(:scaffold, "person name:string") +route "root to: 'people#index'" +rake("db:migrate") + +git :init +git add: "." +git commit: %Q{ -m 'Initial commit' } +``` + +You can now wrap the `git` calls in an `after_bundle` block. It will be run +after the binstubs have been generated. + +```ruby +# template.rb +generate(:scaffold, "person name:string") +route "root to: 'people#index'" +rake("db:migrate") + +after_bundle do + git :init + git add: "." + git commit: %Q{ -m 'Initial commit' } +end +``` + +### Rails Html Sanitizer + +There's a new choice for sanitizing HTML fragments in your applications. The +venerable html-scanner approach is now officially being deprecated in favor of +[`Rails Html Sanitizer`](https://github.com/rails/rails-html-sanitizer). + +This means the methods `sanitize`, `sanitize_css`, `strip_tags` and +`strip_links` are backed by a new implementation. + +In the next major Rails version `Rails Html Sanitizer` will be the default +sanitizer. It already is for new applications. + +Include this in your Gemfile to try it out today: + +```ruby +gem 'rails-html-sanitizer' +``` + +This new sanitizer uses [Loofah](https://github.com/flavorjones/loofah) internally. Loofah in turn uses Nokogiri, which +wraps XML parsers written in both C and Java, so sanitization should be faster +no matter which Ruby version you run. + +The new version updates `sanitize`, so it can take a `Loofah::Scrubber` for +powerful scrubbing. +[See some examples of scrubbers here](https://github.com/flavorjones/loofah#loofahscrubber). + +Two new scrubbers have also been added: `PermitScrubber` and `TargetScrubber`. +Read the [gem's readme](https://github.com/rails/rails-html-sanitizer) for more information. + +The documentation for `PermitScrubber` and `TargetScrubber` explains how you +can gain complete control over when and how elements should be stripped. Upgrading from Rails 4.0 to Rails 4.1 ------------------------------------- @@ -239,6 +311,16 @@ If your application depends on one of these features, you can get them back by adding the [`activesupport-json_encoder`](https://github.com/rails/activesupport-json_encoder) gem to your Gemfile. +#### JSON representation of Time objects + +`#as_json` for objects with time component (`Time`, `DateTime`, `ActiveSupport::TimeWithZone`) +now returns millisecond precision by default. If you need to keep old behavior with no millisecond +precision, set the following in an initializer: + +``` +ActiveSupport::JSON::Encoding.time_precision = 0 +``` + ### Usage of `return` within inline callback blocks Previously, Rails allowed inline callback blocks to use `return` this way: @@ -430,6 +512,20 @@ symbol access is no longer supported. This is also the case for `store_accessors` based on top of `json` or `hstore` columns. Make sure to use string keys consistently. +### Explicit block use for `ActiveSupport::Callbacks` + +Rails 4.1 now expects an explicit block to be passed when calling +`ActiveSupport::Callbacks.set_callback`. This change stems from +`ActiveSupport::Callbacks` being largely rewritten for the 4.1 release. + +```ruby +# Previously in Rails 4.0 +set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff } + +# Now in Rails 4.1 +set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff } +``` + Upgrading from Rails 3.2 to Rails 4.0 ------------------------------------- @@ -561,6 +657,9 @@ Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must rep * Rails 4.0 has changed `serialized_attributes` and `attr_readonly` to class methods only. You shouldn't use instance methods since it's now deprecated. You should change them to use class methods, e.g. `self.serialized_attributes` to `self.class.serialized_attributes`. +* When using the default coder, assigning `nil` to a serialized attribute will save it +to the database as `NULL` instead of passing the `nil` value through YAML (`"--- \n...\n"`). + * Rails 4.0 has removed `attr_accessible` and `attr_protected` feature in favor of Strong Parameters. You can use the [Protected Attributes gem](https://github.com/rails/protected_attributes) for a smooth upgrade path. * If you are not using Protected Attributes, you can remove any options related to diff --git a/install.rb b/install.rb index bff8fee934..96e4153165 100644 --- a/install.rb +++ b/install.rb @@ -5,7 +5,7 @@ if version.nil? exit(64) end -%w( activesupport activemodel activerecord actionpack actionview actionmailer railties ).each do |framework| +%w( activesupport activemodel activerecord actionpack actionview actionmailer railties activejob ).each do |framework| puts "Installing #{framework}..." `cd #{framework} && gem build #{framework}.gemspec && gem install #{framework}-#{version}.gem --no-ri --no-rdoc && rm #{framework}-#{version}.gem` end diff --git a/rails.gemspec b/rails.gemspec index 4800df0df4..b1a7d16722 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -24,6 +24,7 @@ Gem::Specification.new do |s| s.add_dependency 'activemodel', version s.add_dependency 'activerecord', version s.add_dependency 'actionmailer', version + s.add_dependency 'activejob', version s.add_dependency 'railties', version s.add_dependency 'bundler', '>= 1.3.0', '< 2.0' diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index c33a4ed192..5e2b82c3e9 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,4 +1,55 @@ -* Deprecate `Rails::Rack::LogTailer` with not replacement. +* Add a `required` option to the model generator for associations + + *Sean Griffin* + +* Add `after_bundle` callbacks in Rails templates. Useful for allowing the + generated binstubs to be added to version control. + + Fixes #16292. + + *Stefan Kanev* + +* Pull in the custom configuration concept from dhh/custom_configuration, which allows you to + configure your own code through the Rails configuration object with custom configuration: + + # config/environments/production.rb + config.x.payment_processing.schedule = :daily + config.x.payment_processing.retries = 3 + config.x.super_debugger = true + + These configuration points are then available through the configuration object: + + Rails.configuration.x.payment_processing.schedule # => :daily + Rails.configuration.x.payment_processing.retries # => 3 + Rails.configuration.x.super_debugger # => true + Rails.configuration.x.super_debugger.not_set # => nil + + *DHH* + +* Scaffold generator `_form` partial adds `class="field"` for password + confirmation fields. + + *noinkling* + +* Add `Rails::Application.config_for` to load a configuration for the current + environment. + + # config/exception_notification.yml: + production: + url: http://127.0.0.1:8080 + namespace: my_app_production + development: + url: http://localhost:3001 + namespace: my_app_development + + # config/production.rb + MyApp::Application.configure do + config.middleware.use ExceptionNotifier, config_for(:exception_notification) + end + + *Rafael Mendonça França*, *DHH* + +* Deprecate `Rails::Rack::LogTailer` without replacement. *Rafael Mendonça França* diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index ecd8c22dd8..e7172e491f 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -29,7 +29,13 @@ module Rails autoload :WelcomeController class << self - attr_accessor :application, :cache, :logger + @application = @app_class = nil + + attr_writer :application + attr_accessor :app_class, :cache, :logger + def application + @application ||= (app_class.instance if app_class) + end delegate :initialize!, :initialized?, to: :application diff --git a/railties/lib/rails/all.rb b/railties/lib/rails/all.rb index 2e83c0fe14..45361fca83 100644 --- a/railties/lib/rails/all.rb +++ b/railties/lib/rails/all.rb @@ -5,6 +5,7 @@ require "rails" action_controller action_view action_mailer + active_job rails/test_unit sprockets ).each do |framework| diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb index 3e32576040..4d49244807 100644 --- a/railties/lib/rails/api/task.rb +++ b/railties/lib/rails/api/task.rb @@ -50,6 +50,13 @@ module Rails ) }, + 'activejob' => { + :include => %w( + README.md + lib/active_job/**/*.rb + ) + }, + 'railties' => { :include => %w( README.rdoc diff --git a/railties/lib/rails/app_rails_loader.rb b/railties/lib/rails/app_rails_loader.rb index 56f05b3844..39d8007333 100644 --- a/railties/lib/rails/app_rails_loader.rb +++ b/railties/lib/rails/app_rails_loader.rb @@ -2,6 +2,8 @@ require 'pathname' module Rails module AppRailsLoader + extend self + RUBY = Gem.ruby EXECUTABLES = ['bin/rails', 'script/rails'] BUNDLER_WARNING = <<EOS @@ -26,7 +28,7 @@ generate it and add it to source control: EOS - def self.exec_app_rails + def exec_app_rails original_cwd = Dir.pwd loop do @@ -54,7 +56,7 @@ EOS end end - def self.find_executable + def find_executable EXECUTABLES.find { |exe| File.file?(exe) } end end diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 362713eb75..6a4660bad0 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -87,7 +87,15 @@ module Rails class << self def inherited(base) super - base.instance + Rails.app_class = base + end + + def instance + super.run_load_hooks! + end + + def create(initial_variable_values = {}, &block) + new(initial_variable_values, &block).run_load_hooks! end # Makes the +new+ method public. @@ -116,24 +124,33 @@ module Rails @ordered_railties = nil @railties = nil @message_verifiers = {} + @ran_load_hooks = false - Rails.application ||= self + # are these actually used? + @initial_variable_values = initial_variable_values + @block = block add_lib_to_load_path! + end + + # Returns true if the application is initialized. + def initialized? + @initialized + end + + def run_load_hooks! # :nodoc: + return self if @ran_load_hooks + @ran_load_hooks = true ActiveSupport.run_load_hooks(:before_configuration, self) - initial_variable_values.each do |variable_name, value| + @initial_variable_values.each do |variable_name, value| if INITIAL_VARIABLES.include?(variable_name) instance_variable_set("@#{variable_name}", value) end end - instance_eval(&block) if block_given? - end - - # Returns true if the application is initialized. - def initialized? - @initialized + instance_eval(&@block) if @block + self end # Implements call according to the Rack API. It simply @@ -187,6 +204,38 @@ module Rails end end + # Convenience for loading config/foo.yml for the current Rails env. + # + # Example: + # + # # config/exception_notification.yml: + # production: + # url: http://127.0.0.1:8080 + # namespace: my_app_production + # development: + # url: http://localhost:3001 + # namespace: my_app_development + # + # # config/production.rb + # MyApp::Application.configure do + # config.middleware.use ExceptionNotifier, config_for(:exception_notification) + # end + def config_for(name) + yaml = Pathname.new("#{paths["config"].existent.first}/#{name}.yml") + + if yaml.exist? + require "yaml" + require "erb" + (YAML.load(ERB.new(yaml.read).result) || {})[Rails.env] || {} + else + raise "Could not load configuration. No such file - #{yaml}" + end + rescue Psych::SyntaxError => e + raise "YAML syntax error occurred while parsing #{yaml}. " \ + "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \ + "Error: #{e.message}" + end + # Stores some of the Rails initial environment parameters which # will be used by middlewares and engines to configure themselves. def env_config @@ -207,7 +256,8 @@ module Rails "action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt, "action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt, "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt, - "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer + "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer, + "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest }) end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 5e8f4de847..782bc4b0f1 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -13,7 +13,7 @@ module Rails :railties_order, :relative_url_root, :secret_key_base, :secret_token, :serve_static_assets, :ssl_options, :static_cache_control, :session_options, :time_zone, :reload_classes_only_on_change, - :beginning_of_week, :filter_redirect + :beginning_of_week, :filter_redirect, :x attr_writer :log_level attr_reader :encoding @@ -48,6 +48,7 @@ module Rails @eager_load = nil @secret_token = nil @secret_key_base = nil + @x = Custom.new @assets = ActiveSupport::OrderedOptions.new @assets.enabled = true @@ -154,6 +155,17 @@ module Rails def annotations SourceAnnotationExtractor::Annotation end + + private + class Custom + def initialize + @configurations = Hash.new + end + + def method_missing(method, *args) + @configurations[method] ||= ActiveSupport::OrderedOptions.new + end + end end end end diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 5b8509b2e9..7a1bb1e25c 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -22,8 +22,6 @@ module Rails initializer :add_builtin_route do |app| if Rails.env.development? app.routes.append do - get '/rails/mailers' => "rails/mailers#index" - get '/rails/mailers/*path' => "rails/mailers#preview" get '/rails/info/properties' => "rails/info#properties" get '/rails/info/routes' => "rails/info#routes" get '/rails/info' => "rails/info#index" diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb index 6146b6c1db..c3b7bb6f84 100644 --- a/railties/lib/rails/commands/server.rb +++ b/railties/lib/rails/commands/server.rb @@ -9,7 +9,17 @@ module Rails def parse!(args) args, options = args.dup, {} - opt_parser = OptionParser.new do |opts| + option_parser(options).parse! args + + options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" + options[:server] = args.shift + options + end + + private + + def option_parser(options) + OptionParser.new do |opts| opts.banner = "Usage: rails server [mongrel, thin, etc] [options]" opts.on("-p", "--port=port", Integer, "Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v } @@ -37,12 +47,6 @@ module Rails opts.on("-h", "--help", "Show this help message.") { puts opts; exit } end - - opt_parser.parse! args - - options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development" - options[:server] = args.shift - options end end diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index b36ab3d0d5..dc3da1eb41 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -395,7 +395,7 @@ module Rails end unless mod.respond_to?(:railtie_routes_url_helpers) - define_method(:railtie_routes_url_helpers) { railtie.routes.url_helpers } + define_method(:railtie_routes_url_helpers) {|include_path_helpers = true| railtie.routes.url_helpers(include_path_helpers) } end end end @@ -509,7 +509,7 @@ module Rails def call(env) env.merge!(env_config) if env['SCRIPT_NAME'] - env.merge! "ROUTES_#{routes.object_id}_SCRIPT_NAME" => env['SCRIPT_NAME'].dup + env["ROUTES_#{routes.object_id}_SCRIPT_NAME"] = env['SCRIPT_NAME'].dup end app.call(env) end diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index a239874df0..4709914947 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -7,6 +7,7 @@ module Rails def initialize(*) # :nodoc: super @in_group = nil + @after_bundle_callbacks = [] end # Adds an entry into +Gemfile+ for the supplied gem. @@ -232,6 +233,16 @@ module Rails log File.read(find_in_source_paths(path)) end + # Registers a callback to be executed after bundle and spring binstubs + # have run. + # + # after_bundle do + # git add: '.' + # end + def after_bundle(&block) + @after_bundle_callbacks << block + end + protected # Define log for backwards compatibility. If just one argument is sent, diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb index cf3b7acfff..682092fdf2 100644 --- a/railties/lib/rails/generators/actions/create_migration.rb +++ b/railties/lib/rails/generators/actions/create_migration.rb @@ -55,7 +55,8 @@ module Rails else say_status :conflict, :red raise Error, "Another migration is already named #{migration_file_name}: " + - "#{existing_migration}. Use --force to replace this migration file." + "#{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 76f8a1b816..caaaae09e6 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -113,7 +113,7 @@ module Rails javascript_gemfile_entry, jbuilder_gemfile_entry, sdoc_gemfile_entry, - spring_gemfile_entry, + psych_gemfile_entry, @extra_entries].flatten.find_all(&@gem_filter) end @@ -194,19 +194,19 @@ module Rails def self.path(name, path, comment = nil) new(name, nil, comment, path: path) end - - def padding(max_width) - ' ' * (max_width - name.length + 2) - end end def rails_gemfile_entry if options.dev? [GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH), - GemfileEntry.github('arel', 'rails/arel')] + GemfileEntry.github('arel', 'rails/arel'), + GemfileEntry.github('rack', 'rack/rack'), + GemfileEntry.github('i18n', 'svenfuchs/i18n')] elsif options.edge? [GemfileEntry.github('rails', 'rails/rails'), - GemfileEntry.github('arel', 'rails/arel')] + GemfileEntry.github('arel', 'rails/arel'), + GemfileEntry.github('rack', 'rack/rack'), + GemfileEntry.github('i18n', 'svenfuchs/i18n')] else [GemfileEntry.version('rails', Rails::VERSION::STRING, @@ -305,10 +305,12 @@ module Rails end end - def spring_gemfile_entry - return [] unless spring_install? - comment = 'Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring' - GemfileEntry.new('spring', nil, comment, group: :development) + def psych_gemfile_entry + return [] unless defined?(Rubinius) + + comment = 'Use Psych as the YAML engine, instead of Syck, so serialized ' \ + 'data can be read safely from different rubies (see http://git.io/uuLVag)' + GemfileEntry.new('psych', '~> 2.0', comment, platforms: :rbx) end def bundle_command(command) diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb index da99e74435..bba9141fb8 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb @@ -17,7 +17,7 @@ <%%= f.label :password %><br> <%%= f.password_field :password %> </div> - <div> + <div class="field"> <%%= f.label :password_confirmation %><br> <%%= f.password_field :password_confirmation %> <% else -%> diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb index 025b1d8699..5e194783ff 100644 --- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb +++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb @@ -1,3 +1,5 @@ +<p id="notice"><%%= notice %></p> + <h1>Listing <%= plural_table_name.titleize %></h1> <table> diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb index c5326d70d1..f16bd8e082 100644 --- a/railties/lib/rails/generators/generated_attribute.rb +++ b/railties/lib/rails/generators/generated_attribute.rb @@ -44,8 +44,11 @@ module Rails return $1, limit: $2.to_i when /decimal\{(\d+)[,.-](\d+)\}/ return :decimal, precision: $1.to_i, scale: $2.to_i - when /(references|belongs_to)\{polymorphic\}/ - return $1, polymorphic: true + when /(references|belongs_to)\{(.+)\}/ + type = $1 + provided_options = $2.split(/[,.-]/) + options = Hash[provided_options.map { |opt| [opt.to_sym, true] }] + return type, options else return type, {} end @@ -123,7 +126,11 @@ module Rails end def polymorphic? - self.attr_options.has_key?(:polymorphic) + self.attr_options[:polymorphic] + end + + def required? + self.attr_options[:required] end def has_index? @@ -139,12 +146,21 @@ module Rails end def inject_options - "".tap { |s| @attr_options.each { |k,v| s << ", #{k}: #{v.inspect}" } } + "".tap { |s| options_for_migration.each { |k,v| s << ", #{k}: #{v.inspect}" } } end def inject_index_options has_uniq_index? ? ", unique: true" : "" end + + def options_for_migration + @attr_options.dup.tap do |options| + if required? + options.delete(:required) + options[:null] = false + end + end + end end end end diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 188e62b6c8..9110c129d1 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -259,6 +259,12 @@ module Rails public_task :apply_rails_template, :run_bundle public_task :generate_spring_binstubs + def run_after_bundle_callbacks + @after_bundle_callbacks.each do |callback| + callback.call + end + end + protected def self.banner diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 5bdbd58097..337f4dd6c1 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -1,6 +1,5 @@ source 'https://rubygems.org' -<% max_width = gemfile_entries.map { |g| g.name.length }.max -%> <% gemfile_entries.each do |gem| -%> <% if gem.comment -%> @@ -8,7 +7,7 @@ source 'https://rubygems.org' <% end -%> <%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<%= %(, '#{gem.version}') if gem.version -%> <% if gem.options.any? -%> -,<%= gem.padding(max_width) %><%= gem.options.map { |k,v| +, <%= gem.options.map { |k,v| "#{k}: #{v.inspect}" }.join(', ') %> <% end -%> <% end -%> @@ -16,20 +15,34 @@ source 'https://rubygems.org' # Use ActiveModel has_secure_password # gem 'bcrypt', '~> 3.1.7' +# Use Rails Html Sanitizer for HTML sanitization +gem 'rails-html-snaitizer', github: 'rails/rails', branch: 'master' +#temporary gem until a new version of loofah is released +gem 'loofah', github: 'kaspth/loofah', branch: 'single-scrub' + # Use Unicorn as the app server # gem 'unicorn' # Use Capistrano for deployment # gem 'capistrano-rails', group: :development +group :development, :test do <% unless defined?(JRUBY_VERSION) -%> -# To use a debugger + # Call 'debugger' anywhere in the code to stop execution and get a debugger console <%- if RUBY_VERSION < '2.0.0' -%> -# gem 'debugger', group: [:development, :test] + gem 'debugger' <%- else -%> -# gem 'byebug', group: [:development, :test] + gem 'byebug' <%- end -%> + + # Access an IRB console on exceptions page and /console in development + gem 'web-console', '2.0.0.beta1' +<%- if spring_install? %> + # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + gem 'spring' +<% end -%> <% end -%> +end <% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince/) -%> # Windows does not include zoneinfo files, so bundle the tzinfo-data gem diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb b/railties/lib/rails/generators/rails/app/templates/config/boot.rb index 5e5f0c1fac..6b750f00b1 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/boot.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb @@ -1,4 +1,3 @@ -# Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index bbb409616d..35e3035a0b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -30,7 +30,8 @@ Rails.application.configure do # number of complex assets. config.assets.debug = true - # Generate digests for assets URLs. + # Asset digests allow you to set far-future HTTP expiration dates on all assets, + # yet still be able to expire them through the digest params. config.assets.digest = true # Adds additional error checking when serving assets at runtime. 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 a2aa7c09db..bec7716a9b 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 @@ -30,10 +30,11 @@ Rails.application.configure do # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # Generate digests for assets URLs. + # Asset digests allow you to set far-future HTTP expiration dates on all assets, + # yet still be able to expire them through the digest params. config.assets.digest = true - # `config.assets.precompile` has moved to config/initializers/assets.rb + # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb <%- end -%> # Specifies the header that your server uses for sending files. @@ -43,8 +44,8 @@ Rails.application.configure do # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # config.force_ssl = true - # Set to :debug to see everything in the log. - config.log_level = :info + # Set to :info to decrease the log volume. + config.log_level = :debug # Prepend all log lines with the following tags. # config.log_tags = [ :subdomain, :uuid ] @@ -69,9 +70,6 @@ Rails.application.configure do # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify - # Disable automatic flushing of the log to improve performance. - # config.autoflush_log = false - # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new <%- 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 d2f4ec33a6..01ef3e6630 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt @@ -3,6 +3,9 @@ # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' +# Add additional assets to the asset load path +# Rails.application.config.assets.paths << Emoji.images_path + # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. # Rails.application.config.assets.precompile += %w( search.js ) diff --git a/railties/lib/rails/generators/rails/controller/USAGE b/railties/lib/rails/generators/rails/controller/USAGE index de33900e0a..64239ad599 100644 --- a/railties/lib/rails/generators/rails/controller/USAGE +++ b/railties/lib/rails/generators/rails/controller/USAGE @@ -16,4 +16,3 @@ Example: Test: test/controllers/credit_cards_controller_test.rb Views: app/views/credit_cards/debit.html.erb [...] Helper: app/helpers/credit_cards_helper.rb - Test: test/helpers/credit_cards_helper_test.rb diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb index fbecab1823..a48cc13ed7 100644 --- a/railties/lib/rails/generators/rails/controller/controller_generator.rb +++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb @@ -2,7 +2,7 @@ module Rails module Generators class ControllerGenerator < NamedBase # :nodoc: argument :actions, type: :array, default: [], banner: "action action" - class_option :skip_routes, type: :boolean, desc: "Dont' add routes to config/routes.rb." + class_option :skip_routes, type: :boolean, desc: "Don't add routes to config/routes.rb." check_class_collision suffix: "Controller" diff --git a/railties/lib/rails/generators/rails/helper/USAGE b/railties/lib/rails/generators/rails/helper/USAGE index 30e323a858..8855ef3b01 100644 --- a/railties/lib/rails/generators/rails/helper/USAGE +++ b/railties/lib/rails/generators/rails/helper/USAGE @@ -5,13 +5,9 @@ Description: To create a helper within a module, specify the helper name as a path like 'parent_module/helper_name'. - This generates a helper class in app/helpers and invokes the configured - test framework. - Example: `rails generate helper CreditCard` Credit card helper. Helper: app/helpers/credit_card_helper.rb - Test: test/helpers/credit_card_helper_test.rb diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE index 833b7beb7f..2a6b8700e3 100644 --- a/railties/lib/rails/generators/rails/model/USAGE +++ b/railties/lib/rails/generators/rails/model/USAGE @@ -6,6 +6,11 @@ Description: model's attributes. Timestamps are added by default, so you don't have to specify them by hand as 'created_at:datetime updated_at:datetime'. + As a special case, specifying 'password:digest' will generate a + password_digest field of string type, and configure your generated model and + tests for use with ActiveModel has_secure_password (assuming the default ORM + and test framework are being used). + You don't have to think up every attribute up front, but it helps to sketch out a few so you can start working with the model immediately. @@ -27,7 +32,8 @@ Available field types: `rails generate model post title:string body:text` will generate a title column with a varchar type and a body column with a text - type. You can use the following types: + type. If no type is specified the string type will be used by default. + You can use the following types: integer primary_key @@ -73,6 +79,10 @@ Available field types: `rails generate model user username:string{30}:uniq` `rails generate model product supplier:references{polymorphic}:index` + If you require a `password_digest` string column for use with + has_secure_password, you should specify `password:digest`: + + `rails generate model user password:digest` Examples: `rails generate model account` diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile index 796587f316..35ad9fbf9e 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile +++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile @@ -31,7 +31,7 @@ end <% end -%> <%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<%= %(, '#{gem.version}') if gem.version -%> <% if gem.options.any? -%> -,<%= gem.padding(max_width) %><%= gem.options.map { |k,v| +, <%= gem.options.map { |k,v| "#{k}: #{v.inspect}" }.join(', ') %> <% end -%> <% end -%> diff --git a/railties/lib/rails/generators/rails/scaffold/USAGE b/railties/lib/rails/generators/rails/scaffold/USAGE index 4a3eb2c7c7..1b2a944103 100644 --- a/railties/lib/rails/generators/rails/scaffold/USAGE +++ b/railties/lib/rails/generators/rails/scaffold/USAGE @@ -9,11 +9,16 @@ Description: Attributes are field arguments specifying the model's attributes. You can optionally pass the type and an index to each field. For instance: - "title body:text tracking_id:integer:uniq" will generate a title field of + 'title body:text tracking_id:integer:uniq' will generate a title field of string type, a body with text type and a tracking_id as an integer with an unique index. "index" could also be given instead of "uniq" if one desires a non unique index. + As a special case, specifying 'password:digest' will generate a + password_digest field of string type, and configure your generated model, + controller, views, and test suite for use with ActiveModel + has_secure_password (assuming they are using Rails defaults). + Timestamps are added by default, so you don't have to specify them by hand as 'created_at:datetime updated_at:datetime'. @@ -33,3 +38,4 @@ Examples: `rails generate scaffold post` `rails generate scaffold post title body:text published:boolean` `rails generate scaffold purchase amount:decimal tracking_id:integer:uniq` + `rails generate scaffold user email:uniq password:digest` diff --git a/railties/lib/rails/generators/test_unit/helper/helper_generator.rb b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb index 0db76f9eaf..bde4e88915 100644 --- a/railties/lib/rails/generators/test_unit/helper/helper_generator.rb +++ b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb @@ -3,11 +3,7 @@ require 'rails/generators/test_unit' module TestUnit # :nodoc: module Generators # :nodoc: class HelperGenerator < Base # :nodoc: - check_class_collision suffix: "HelperTest" - - def create_helper_files - template 'helper_test.rb', File.join('test/helpers', class_path, "#{file_name}_helper_test.rb") - end + # Rails does not generate anything here. end end end diff --git a/railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb b/railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb deleted file mode 100644 index 7d37bda0f9..0000000000 --- a/railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb +++ /dev/null @@ -1,6 +0,0 @@ -require 'test_helper' - -<% module_namespacing do -%> -class <%= class_name %>HelperTest < ActionView::TestCase -end -<% end -%> diff --git a/railties/lib/rails/generators/testing/assertions.rb b/railties/lib/rails/generators/testing/assertions.rb index 2e877f8762..bd069e4bd0 100644 --- a/railties/lib/rails/generators/testing/assertions.rb +++ b/railties/lib/rails/generators/testing/assertions.rb @@ -1,3 +1,5 @@ +require 'shellwords' + module Rails module Generators module Testing diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb index 7576eba6e0..e0600d0b59 100644 --- a/railties/lib/rails/generators/testing/behaviour.rb +++ b/railties/lib/rails/generators/testing/behaviour.rb @@ -100,6 +100,23 @@ module Rails dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, '') Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first end + + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) + end end end end diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb index 3b35798679..9962e6d943 100644 --- a/railties/lib/rails/rack/logger.rb +++ b/railties/lib/rails/rack/logger.rb @@ -34,7 +34,7 @@ module Rails instrumenter = ActiveSupport::Notifications.instrumenter instrumenter.start 'request.action_dispatch', request: request - logger.info started_request_message(request) + logger.info { started_request_message(request) } resp = @app.call(env) resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) } resp diff --git a/railties/lib/rails/rubyprof_ext.rb b/railties/lib/rails/rubyprof_ext.rb deleted file mode 100644 index 017eba3a76..0000000000 --- a/railties/lib/rails/rubyprof_ext.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'prof' - -module Prof #:nodoc: - # Adapted from Shugo Maeda's unprof.rb - def self.print_profile(results, io = $stderr) - total = results.detect { |i| - i.method_class.nil? && i.method_id == :"#toplevel" - }.total_time - total = 0.001 if total < 0.001 - - io.puts " %% cumulative self self total" - io.puts " time seconds seconds calls ms/call ms/call name" - - sum = 0.0 - results.each do |r| - sum += r.self_time - - name = if r.method_class.nil? - r.method_id.to_s - elsif r.method_class.is_a?(Class) - "#{r.method_class}##{r.method_id}" - else - "#{r.method_class}.#{r.method_id}" - end - io.printf "%6.2f %8.3f %8.3f %8d %8.2f %8.2f %s\n", - r.self_time / total * 100, - sum, - r.self_time, - r.count, - r.self_time * 1000 / r.count, - r.total_time * 1000 / r.count, - name - end - end -end diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb index 9ccc286b4e..b6533a5fb2 100644 --- a/railties/test/abstract_unit.rb +++ b/railties/test/abstract_unit.rb @@ -26,3 +26,26 @@ end def jruby_skip(message = '') skip message if defined?(JRUBY_VERSION) end + +class ActiveSupport::TestCase + private + + unless defined?(:capture) + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) + end + end +end diff --git a/railties/test/app_rails_loader_test.rb b/railties/test/app_rails_loader_test.rb index 1d3b80253a..d4885447e6 100644 --- a/railties/test/app_rails_loader_test.rb +++ b/railties/test/app_rails_loader_test.rb @@ -3,13 +3,27 @@ require 'abstract_unit' require 'rails/app_rails_loader' class AppRailsLoaderTest < ActiveSupport::TestCase + def loader + @loader ||= Class.new do + extend Rails::AppRailsLoader + + def self.exec_arguments + @exec_arguments + end + + def self.exec(*args) + @exec_arguments = args + end + end + end + def write(filename, contents=nil) FileUtils.mkdir_p(File.dirname(filename)) File.write(filename, contents) end def expects_exec(exe) - Rails::AppRailsLoader.expects(:exec).with(Rails::AppRailsLoader::RUBY, exe) + assert_equal [Rails::AppRailsLoader::RUBY, exe], loader.exec_arguments end setup do @@ -22,30 +36,30 @@ class AppRailsLoaderTest < ActiveSupport::TestCase exe = "#{script_dir}/rails" test "is not in a Rails application if #{exe} is not found in the current or parent directories" do - File.stubs(:file?).with('bin/rails').returns(false) - File.stubs(:file?).with('script/rails').returns(false) + def loader.find_executables; end - assert !Rails::AppRailsLoader.exec_app_rails + assert !loader.exec_app_rails end test "is not in a Rails application if #{exe} exists but is a folder" do FileUtils.mkdir_p(exe) - assert !Rails::AppRailsLoader.exec_app_rails + assert !loader.exec_app_rails end ['APP_PATH', 'ENGINE_PATH'].each do |keyword| test "is in a Rails application if #{exe} exists and contains #{keyword}" do write exe, keyword + loader.exec_app_rails + expects_exec exe - Rails::AppRailsLoader.exec_app_rails end test "is not in a Rails application if #{exe} exists but doesn't contain #{keyword}" do write exe - assert !Rails::AppRailsLoader.exec_app_rails + assert !loader.exec_app_rails end test "is in a Rails application if parent directory has #{exe} containing #{keyword} and chdirs to the root directory" do @@ -54,8 +68,9 @@ class AppRailsLoaderTest < ActiveSupport::TestCase Dir.chdir('foo/bar') + loader.exec_app_rails + expects_exec exe - Rails::AppRailsLoader.exec_app_rails # Compare the realpath in case either of them has symlinks. # diff --git a/railties/test/application/configuration/base_test.rb b/railties/test/application/configuration/base_test.rb new file mode 100644 index 0000000000..d6a82b139d --- /dev/null +++ b/railties/test/application/configuration/base_test.rb @@ -0,0 +1,37 @@ +require 'isolation/abstract_unit' +require 'rack/test' +require 'env_helpers' + +module ApplicationTests + module ConfigurationTests + class BaseTest < ActiveSupport::TestCase + def setup + build_app + boot_rails + FileUtils.rm_rf("#{app_path}/config/environments") + end + + def teardown + teardown_app + FileUtils.rm_rf(new_app) if File.directory?(new_app) + end + + 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 + end + end +end
\ No newline at end of file diff --git a/railties/test/application/configuration/custom_test.rb b/railties/test/application/configuration/custom_test.rb new file mode 100644 index 0000000000..045537fc28 --- /dev/null +++ b/railties/test/application/configuration/custom_test.rb @@ -0,0 +1,15 @@ +require 'application/configuration/base_test' + +class ApplicationTests::ConfigurationTests::CustomTest < ApplicationTests::ConfigurationTests::BaseTest + test 'access custom configuration point' do + add_to_config <<-RUBY + config.x.resque.inline_jobs = :always + config.x.resque.timeout = 60 + RUBY + require_environment + + assert_equal :always, Rails.configuration.x.resque.inline_jobs + assert_equal 60, Rails.configuration.x.resque.timeout + assert_nil Rails.configuration.x.resque.nothing + end +end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 207a0c7e86..e661b6f4cc 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -66,13 +66,25 @@ module ApplicationTests config.action_dispatch.show_exceptions = true RUBY + app_file 'db/migrate/20140708012246_create_user.rb', <<-RUBY + class CreateUser < ActiveRecord::Migration + def change + create_table :users + end + end + RUBY + require "#{app_path}/config/environment" - ActiveRecord::Migrator.stubs(:needs_migration?).returns(true) - ActiveRecord::NullMigration.any_instance.stubs(:mtime).returns(1) - get "/foo" - assert_equal 500, last_response.status - assert_match "ActiveRecord::PendingMigrationError", last_response.body + ActiveRecord::Migrator.migrations_paths = ["#{app_path}/db/migrate"] + + begin + get "/foo" + assert_equal 500, last_response.status + assert_match "ActiveRecord::PendingMigrationError", last_response.body + ensure + ActiveRecord::Migrator.migrations_paths = nil + end end test "Rails.groups returns available groups" do @@ -372,6 +384,8 @@ module ApplicationTests end RUBY + token = "cf50faa3fe97702ca1ae" + app_file 'app/controllers/posts_controller.rb', <<-RUBY class PostsController < ApplicationController def show @@ -381,6 +395,10 @@ module ApplicationTests def update render text: "update" end + + private + + def form_authenticity_token; token; end # stub the authenticy token end RUBY @@ -392,8 +410,6 @@ module ApplicationTests require "#{app_path}/config/environment" - token = "cf50faa3fe97702ca1ae" - PostsController.any_instance.stubs(:form_authenticity_token).returns(token) params = {authenticity_token: token} get "/posts/1" @@ -424,7 +440,7 @@ module ApplicationTests end get "/" - assert last_response.body =~ /_xsrf_token_here/ + assert_match "_xsrf_token_here", last_response.body end test "sets ActionDispatch.test_app" do @@ -714,6 +730,44 @@ module ApplicationTests assert_match "We're sorry, but something went wrong", last_response.body end + test "config.action_controller.always_permitted_parameters are: controller, action by default" do + require "#{app_path}/config/environment" + assert_equal %w(controller action), ActionController::Parameters.always_permitted_parameters + end + + test "config.action_controller.always_permitted_parameters = ['controller', 'action', 'format']" do + add_to_config <<-RUBY + config.action_controller.always_permitted_parameters = %w( controller action format ) + RUBY + require "#{app_path}/config/environment" + assert_equal %w( controller action format ), ActionController::Parameters.always_permitted_parameters + end + + test "config.action_controller.always_permitted_parameters = ['controller','action','format'] does not raise exeception" do + app_file 'app/controllers/posts_controller.rb', <<-RUBY + class PostsController < ActionController::Base + def create + render text: params.permit(post: [:title]) + end + end + RUBY + + add_to_config <<-RUBY + routes.prepend do + resources :posts + end + config.action_controller.always_permitted_parameters = %w( controller action format ) + config.action_controller.action_on_unpermitted_parameters = :raise + RUBY + + require "#{app_path}/config/environment" + + assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters + + post "/posts", {post: {"title" =>"zomg"}, format: "json"} + assert_equal 200, last_response.status + end + test "config.action_controller.action_on_unpermitted_parameters is :log by default on development" do ENV["RAILS_ENV"] = "development" @@ -837,79 +891,79 @@ module ApplicationTests end test "rake_tasks block works at instance level" do - $ran_block = false - app_file "config/environments/development.rb", <<-RUBY Rails.application.configure do + config.ran_block = false + rake_tasks do - $ran_block = true + config.ran_block = true end end RUBY require "#{app_path}/config/environment" + assert_not Rails.configuration.ran_block - assert !$ran_block require 'rake' require 'rake/testtask' require 'rdoc/task' Rails.application.load_tasks - assert $ran_block + assert Rails.configuration.ran_block end test "generators block works at instance level" do - $ran_block = false - app_file "config/environments/development.rb", <<-RUBY Rails.application.configure do + config.ran_block = false + generators do - $ran_block = true + config.ran_block = true end end RUBY require "#{app_path}/config/environment" + assert_not Rails.configuration.ran_block - assert !$ran_block Rails.application.load_generators - assert $ran_block + assert Rails.configuration.ran_block end test "console block works at instance level" do - $ran_block = false - app_file "config/environments/development.rb", <<-RUBY Rails.application.configure do + config.ran_block = false + console do - $ran_block = true + config.ran_block = true end end RUBY require "#{app_path}/config/environment" + assert_not Rails.configuration.ran_block - assert !$ran_block Rails.application.load_console - assert $ran_block + assert Rails.configuration.ran_block end test "runner block works at instance level" do - $ran_block = false - app_file "config/environments/development.rb", <<-RUBY Rails.application.configure do + config.ran_block = false + runner do - $ran_block = true + config.ran_block = true end end RUBY require "#{app_path}/config/environment" + assert_not Rails.configuration.ran_block - assert !$ran_block Rails.application.load_runner - assert $ran_block + assert Rails.configuration.ran_block end test "loading the first existing database configuration available" do @@ -923,9 +977,115 @@ module ApplicationTests require "#{app_path}/config/environment" - db_config = Rails.application.config.database_configuration + assert_kind_of Hash, Rails.application.config.database_configuration + end + + test 'config.action_mailer.show_previews defaults to true in development' do + Rails.env = "development" + require "#{app_path}/config/environment" + + assert Rails.application.config.action_mailer.show_previews + end + + test 'config.action_mailer.show_previews defaults to false in production' do + Rails.env = "production" + require "#{app_path}/config/environment" + + assert_equal false, Rails.application.config.action_mailer.show_previews + end + + test 'config.action_mailer.show_previews can be set in the configuration file' do + Rails.env = "production" + add_to_config <<-RUBY + config.action_mailer.show_previews = true + RUBY + require "#{app_path}/config/environment" + + assert_equal true, Rails.application.config.action_mailer.show_previews + end + + test "config_for loads custom configuration from yaml files" do + app_file 'config/custom.yml', <<-RUBY + development: + key: 'custom key' + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + + require "#{app_path}/config/environment" + + assert_equal 'custom key', Rails.application.config.my_custom_config['key'] + end + + test "config_for raises an exception if the file does not exist" do + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + + exception = assert_raises(RuntimeError) do + require "#{app_path}/config/environment" + end + + assert_equal "Could not load configuration. No such file - #{app_path}/config/custom.yml", exception.message + end + + test "config_for without the environment configured returns an empty hash" do + app_file 'config/custom.yml', <<-RUBY + test: + key: 'custom key' + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + require "#{app_path}/config/environment" + + assert_equal({}, Rails.application.config.my_custom_config) + end + + test "config_for with empty file returns an empty hash" do + app_file 'config/custom.yml', <<-RUBY + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + require "#{app_path}/config/environment" + + assert_equal({}, Rails.application.config.my_custom_config) + end + + test "config_for containing ERB tags should evaluate" do + app_file 'config/custom.yml', <<-RUBY + development: + key: <%= 'custom key' %> + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + require "#{app_path}/config/environment" + + assert_equal 'custom key', Rails.application.config.my_custom_config['key'] + end + + test "config_for with syntax error show a more descritive exception" do + app_file 'config/custom.yml', <<-RUBY + development: + key: foo: + RUBY + + add_to_config <<-RUBY + config.my_custom_config = config_for('custom') + RUBY + + exception = assert_raises(RuntimeError) do + require "#{app_path}/config/environment" + end - assert db_config.is_a?(Hash) + assert_match 'YAML syntax error occurred while parsing', exception.message end end end diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index 8e76bf27f3..ae550331bd 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -50,7 +50,7 @@ module ApplicationTests assert_equal "test.rails", ActionMailer::Base.default_url_options[:host] end - test "does not include url helpers as action methods" do + test "includes url helpers as action methods" do app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get "/foo", :to => lambda { |env| [200, {}, []] }, :as => :foo @@ -66,8 +66,8 @@ module ApplicationTests require "#{app_path}/config/environment" assert Foo.method_defined?(:foo_path) + assert Foo.method_defined?(:foo_url) assert Foo.method_defined?(:main_app) - assert_equal Set.new(["notify"]), Foo.action_methods end test "allows to not load all helpers for controllers" do diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb index bc34897cdf..9ee54796a4 100644 --- a/railties/test/application/initializers/i18n_test.rb +++ b/railties/test/application/initializers/i18n_test.rb @@ -184,28 +184,13 @@ en: assert_fallbacks ca: [:ca, :"es-ES", :es, :'en-US', :en] end - test "config.i18n.enforce_available_locales is set to true by default and avoids I18n warnings" do - add_to_config <<-RUBY - config.i18n.default_locale = :it - RUBY - - output = capture(:stderr) { load_app } - assert_no_match %r{deprecated.*enforce_available_locales}, output - assert_equal true, I18n.enforce_available_locales - - assert_raise I18n::InvalidLocale do - I18n.locale = :es - end - end - test "disable config.i18n.enforce_available_locales" do add_to_config <<-RUBY config.i18n.enforce_available_locales = false config.i18n.default_locale = :fr RUBY - output = capture(:stderr) { load_app } - assert_no_match %r{deprecated.*enforce_available_locales}, output + load_app assert_equal false, I18n.enforce_available_locales assert_nothing_raised do @@ -220,8 +205,7 @@ en: config.i18n.default_locale = :fr RUBY - output = capture(:stderr) { load_app } - assert_no_match %r{deprecated.*enforce_available_locales}, output + load_app assert_equal false, I18n.enforce_available_locales assert_nothing_raised do diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb index c588fd7012..55e917c3ec 100644 --- a/railties/test/application/mailer_previews_test.rb +++ b/railties/test/application/mailer_previews_test.rb @@ -26,6 +26,20 @@ module ApplicationTests assert_equal 404, last_response.status end + test "/rails/mailers is accessible with correct configuraiton" do + add_to_config "config.action_mailer.show_previews = true" + app("production") + get "/rails/mailers" + assert_equal 200, last_response.status + end + + test "/rails/mailers is not accessible with show_previews = false" do + add_to_config "config.action_mailer.show_previews = false" + app("development") + get "/rails/mailers" + assert_equal 404, last_response.status + end + test "mailer previews are loaded from the default preview_path" do mailer 'notifier', <<-RUBY class Notifier < ActionMailer::Base @@ -403,6 +417,58 @@ module ApplicationTests assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body end + test "*_path helpers emit a deprecation" do + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get 'foo', to: 'foo#index' + end + RUBY + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def path_in_view + mail to: "to@example.org" + end + + def path_in_mailer + @url = foo_path + mail to: "to@example.org" + end + end + RUBY + + html_template 'notifier/path_in_view', "<%= link_to 'foo', foo_path %>" + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def path_in_view + Notifier.path_in_view + end + + def path_in_mailer + Notifier.path_in_mailer + end + end + RUBY + + app('development') + + assert_deprecated do + get "/rails/mailers/notifier/path_in_view.html" + assert_equal 200, last_response.status + end + + html_template 'notifier/path_in_mailer', "No ERB in here" + + assert_deprecated do + get "/rails/mailers/notifier/path_in_mailer.html" + assert_equal 200, last_response.status + end + end + private def build_app super diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 1557b90d27..33eb034b1c 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -188,7 +188,7 @@ module ApplicationTests end end - etag = "5af83e3196bf99f440f31f2e1a6c9afe".inspect + etag = "W/" + "5af83e3196bf99f440f31f2e1a6c9afe".inspect get "/" assert_equal 200, last_response.status diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb index f8d8a673ae..9ebf163671 100644 --- a/railties/test/application/multiple_applications_test.rb +++ b/railties/test/application/multiple_applications_test.rb @@ -36,23 +36,23 @@ module ApplicationTests end def test_initialization_of_application_with_previous_config - application1 = AppTemplate::Application.new(config: Rails.application.config) - application2 = AppTemplate::Application.new + application1 = AppTemplate::Application.create(config: Rails.application.config) + application2 = AppTemplate::Application.create assert_equal Rails.application.config, application1.config, "Creating a new application while setting an initial config should result in the same config" assert_not_equal Rails.application.config, application2.config, "New applications without setting an initial config should not have the same config" end def test_initialization_of_application_with_previous_railties - application1 = AppTemplate::Application.new(railties: Rails.application.railties) - application2 = AppTemplate::Application.new + application1 = AppTemplate::Application.create(railties: Rails.application.railties) + application2 = AppTemplate::Application.create assert_equal Rails.application.railties, application1.railties assert_not_equal Rails.application.railties, application2.railties end def test_initialize_new_application_with_all_previous_initialization_variables - application1 = AppTemplate::Application.new( + application1 = AppTemplate::Application.create( config: Rails.application.config, railties: Rails.application.railties, routes_reloader: Rails.application.routes_reloader, @@ -72,26 +72,26 @@ module ApplicationTests end def test_rake_tasks_defined_on_different_applications_go_to_the_same_class - $run_count = 0 + run_count = 0 application1 = AppTemplate::Application.new application1.rake_tasks do - $run_count += 1 + run_count += 1 end application2 = AppTemplate::Application.new application2.rake_tasks do - $run_count += 1 + run_count += 1 end require "#{app_path}/config/environment" - assert_equal 0, $run_count, "The count should stay at zero without any calls to the rake tasks" + assert_equal 0, run_count, "The count should stay at zero without any calls to the rake tasks" require 'rake' require 'rake/testtask' require 'rdoc/task' Rails.application.load_tasks - assert_equal 2, $run_count, "Calling a rake task should result in two increments to the count" + assert_equal 2, run_count, "Calling a rake task should result in two increments to the count" end def test_multiple_applications_can_be_initialized @@ -100,56 +100,56 @@ module ApplicationTests def test_initializers_run_on_different_applications_go_to_the_same_class application1 = AppTemplate::Application.new - $run_count = 0 + run_count = 0 AppTemplate::Application.initializer :init0 do - $run_count += 1 + run_count += 1 end application1.initializer :init1 do - $run_count += 1 + run_count += 1 end AppTemplate::Application.new.initializer :init2 do - $run_count += 1 + run_count += 1 end - assert_equal 0, $run_count, "Without loading the initializers, the count should be 0" + assert_equal 0, run_count, "Without loading the initializers, the count should be 0" # Set config.eager_load to false so that an eager_load warning doesn't pop up AppTemplate::Application.new { config.eager_load = false }.initialize! - assert_equal 3, $run_count, "There should have been three initializers that incremented the count" + assert_equal 3, run_count, "There should have been three initializers that incremented the count" end def test_consoles_run_on_different_applications_go_to_the_same_class - $run_count = 0 - AppTemplate::Application.console { $run_count += 1 } - AppTemplate::Application.new.console { $run_count += 1 } + run_count = 0 + AppTemplate::Application.console { run_count += 1 } + AppTemplate::Application.new.console { run_count += 1 } - assert_equal 0, $run_count, "Without loading the consoles, the count should be 0" + assert_equal 0, run_count, "Without loading the consoles, the count should be 0" Rails.application.load_console - assert_equal 2, $run_count, "There should have been two consoles that increment the count" + assert_equal 2, run_count, "There should have been two consoles that increment the count" end def test_generators_run_on_different_applications_go_to_the_same_class - $run_count = 0 - AppTemplate::Application.generators { $run_count += 1 } - AppTemplate::Application.new.generators { $run_count += 1 } + run_count = 0 + AppTemplate::Application.generators { run_count += 1 } + AppTemplate::Application.new.generators { run_count += 1 } - assert_equal 0, $run_count, "Without loading the generators, the count should be 0" + assert_equal 0, run_count, "Without loading the generators, the count should be 0" Rails.application.load_generators - assert_equal 2, $run_count, "There should have been two generators that increment the count" + assert_equal 2, run_count, "There should have been two generators that increment the count" end def test_runners_run_on_different_applications_go_to_the_same_class - $run_count = 0 - AppTemplate::Application.runner { $run_count += 1 } - AppTemplate::Application.new.runner { $run_count += 1 } + run_count = 0 + AppTemplate::Application.runner { run_count += 1 } + AppTemplate::Application.new.runner { run_count += 1 } - assert_equal 0, $run_count, "Without loading the runners, the count should be 0" + assert_equal 0, run_count, "Without loading the runners, the count should be 0" Rails.application.load_runner - assert_equal 2, $run_count, "There should have been two runners that increment the count" + assert_equal 2, run_count, "There should have been two runners that increment the count" end def test_isolate_namespace_on_an_application diff --git a/railties/test/application/rack/logger_test.rb b/railties/test/application/rack/logger_test.rb index 701843a6fd..0082ec9cd2 100644 --- a/railties/test/application/rack/logger_test.rb +++ b/railties/test/application/rack/logger_test.rb @@ -11,10 +11,12 @@ module ApplicationTests def setup build_app + add_to_config <<-RUBY + config.logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + RUBY + require "#{app_path}/config/environment" super - @logger = MockLogger.new - Rails.stubs(:logger).returns(@logger) end def teardown @@ -23,7 +25,7 @@ module ApplicationTests end def logs - @logs ||= @logger.logged(:info).join("\n") + @logs ||= Rails.logger.logged(:info).join("\n") end test "logger logs proper HTTP GET verb and path" do diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb index a6900a57c4..a3819b93b2 100644 --- a/railties/test/application/rake/migrations_test.rb +++ b/railties/test/application/rake/migrations_test.rb @@ -184,6 +184,21 @@ module ApplicationTests assert_match(/create_table "books"/, structure_dump) end end + + test 'test migration status migrated file is deleted' do + Dir.chdir(app_path) do + `rails generate model user username:string password:string; + rails generate migration add_email_to_users email:string; + rake db:migrate + rm db/migrate/*email*.rb` + + output = `rake db:migrate:status` + File.write('test.txt', output) + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output) + end + end end end end diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb index 1273f9d4c2..4aea3e980f 100644 --- a/railties/test/commands/console_test.rb +++ b/railties/test/commands/console_test.rb @@ -6,7 +6,13 @@ class Rails::ConsoleTest < ActiveSupport::TestCase include EnvHelpers class FakeConsole - def self.start; end + def self.started? + @started + end + + def self.start + @started = true + end end def test_sandbox_option @@ -25,17 +31,18 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end def test_start - FakeConsole.expects(:start) start + + assert app.console.started? assert_match(/Loading \w+ environment \(Rails/, output) end def test_start_with_sandbox - app.expects(:sandbox=).with(true) - FakeConsole.expects(:start) - start ["--sandbox"] + + assert app.console.started? + assert app.sandbox assert_match(/Loading \w+ environment in sandbox \(Rails/, output) end @@ -51,9 +58,12 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end def test_start_with_debugger - rails_console = Rails::Console.new(app, parse_arguments(["--debugger"])) - rails_console.expects(:require_debugger).returns(nil) + stubbed_console = Class.new(Rails::Console) do + def require_debugger + end + end + rails_console = stubbed_console.new(app, parse_arguments(["--debugger"])) silence_stream(STDOUT) { rails_console.start } end end @@ -64,7 +74,7 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end def test_console_defaults_to_IRB - app = build_app(console: nil) + app = build_app(nil) assert_equal IRB, Rails::Console.new(app).console end @@ -115,8 +125,12 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present - Rails::Console.stubs(:available_environments).returns(['dev']) - options = Rails::Console.parse_arguments(['dev']) + stubbed_console = Class.new(Rails::Console) do + def available_environments + ['dev'] + end + end + options = stubbed_console.parse_arguments(['dev']) assert_match('dev', options[:environment]) end @@ -131,15 +145,29 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end def app - @app ||= build_app(console: FakeConsole) + @app ||= build_app(FakeConsole) end - def build_app(config) - config = mock("config", config) - app = mock("app", config: config) - app.stubs(:sandbox=).returns(nil) - app.expects(:load_console) - app + def build_app(console) + mocked_console = Class.new do + attr_reader :sandbox, :console + + def initialize(console) + @console = console + end + + def config + self + end + + def sandbox=(arg) + @sandbox = arg + end + + def load_console + end + end + mocked_console.new(console) end def parse_arguments(args) diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index 24db395e6e..ede08e7b86 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'minitest/mock' require 'rails/commands/dbconsole' class Rails::DBConsoleTest < ActiveSupport::TestCase @@ -26,20 +27,21 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase "timeout"=> "3000" } } - app_db_config(config_sample) - assert_equal Rails::DBConsole.new.config, config_sample["test"] + app_db_config(config_sample) do + assert_equal Rails::DBConsole.new.config, config_sample["test"] + end end def test_config_with_no_db_config - app_db_config(nil) - assert_raise(ActiveRecord::AdapterNotSpecified) { - Rails::DBConsole.new.config - } + app_db_config(nil) do + assert_raise(ActiveRecord::AdapterNotSpecified) { + Rails::DBConsole.new.config + } + end end def test_config_with_database_url_only ENV['DATABASE_URL'] = 'postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000' - app_db_config(nil) expected = { "adapter" => "postgresql", "host" => "localhost", @@ -50,7 +52,10 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase "pool" => "5", "timeout" => "3000" }.sort - assert_equal expected, Rails::DBConsole.new.config.sort + + app_db_config(nil) do + assert_equal expected, Rails::DBConsole.new.config.sort + end end def test_config_choose_database_url_if_exists @@ -68,8 +73,9 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase "timeout" => "3000" } } - app_db_config(sample_config) - assert_equal host, Rails::DBConsole.new.config["host"] + app_db_config(sample_config) do + assert_equal host, Rails::DBConsole.new.config["host"] + end end def test_env @@ -78,58 +84,65 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase ENV['RAILS_ENV'] = nil ENV['RACK_ENV'] = nil - Rails.stubs(:respond_to?).with(:env).returns(false) - assert_equal Rails::DBConsole.new.environment, "development" + Rails.stub(:respond_to?, false) do + assert_equal Rails::DBConsole.new.environment, "development" - ENV['RACK_ENV'] = "rack_env" - assert_equal Rails::DBConsole.new.environment, "rack_env" + ENV['RACK_ENV'] = "rack_env" + assert_equal Rails::DBConsole.new.environment, "rack_env" - ENV['RAILS_ENV'] = "rails_env" - assert_equal Rails::DBConsole.new.environment, "rails_env" + ENV['RAILS_ENV'] = "rails_env" + assert_equal Rails::DBConsole.new.environment, "rails_env" + end ensure ENV['RAILS_ENV'] = "test" end def test_rails_env_is_development_when_argument_is_dev - Rails::DBConsole.stubs(:available_environments).returns(['development', 'test']) - options = Rails::DBConsole.new.send(:parse_arguments, ['dev']) - assert_match('development', options[:environment]) + dbconsole = Rails::DBConsole.new + + dbconsole.stub(:available_environments, ['development', 'test']) do + options = dbconsole.send(:parse_arguments, ['dev']) + assert_match('development', options[:environment]) + end end def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present - Rails::DBConsole.stubs(:available_environments).returns(['dev']) - options = Rails::DBConsole.new.send(:parse_arguments, ['dev']) - assert_match('dev', options[:environment]) + dbconsole = Rails::DBConsole.new + + dbconsole.stub(:available_environments, ['dev']) do + options = dbconsole.send(:parse_arguments, ['dev']) + assert_match('dev', options[:environment]) + end end def test_mysql - dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], 'db') start(adapter: 'mysql', database: 'db') assert !aborted + assert_equal [%w[mysql mysql5], 'db'], dbconsole.find_cmd_and_exec_args end def test_mysql_full - dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db') start(adapter: 'mysql', database: 'db', host: 'locahost', port: 1234, socket: 'socket', username: 'user', password: 'qwerty', encoding: 'UTF-8') assert !aborted + assert_equal [%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db'], dbconsole.find_cmd_and_exec_args end def test_mysql_include_password - dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], '--user=user', '--password=qwerty', 'db') start({adapter: 'mysql', database: 'db', username: 'user', password: 'qwerty'}, ['-p']) assert !aborted + assert_equal [%w[mysql mysql5], '--user=user', '--password=qwerty', 'db'], dbconsole.find_cmd_and_exec_args end def test_postgresql - dbconsole.expects(:find_cmd_and_exec).with('psql', 'db') start(adapter: 'postgresql', database: 'db') assert !aborted + assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args end def test_postgresql_full - dbconsole.expects(:find_cmd_and_exec).with('psql', 'db') start(adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3', host: 'host', port: 5432) assert !aborted + assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args assert_equal 'user', ENV['PGUSER'] assert_equal 'host', ENV['PGHOST'] assert_equal '5432', ENV['PGPORT'] @@ -137,60 +150,60 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase end def test_postgresql_include_password - dbconsole.expects(:find_cmd_and_exec).with('psql', 'db') start({adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3'}, ['-p']) assert !aborted + assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args assert_equal 'user', ENV['PGUSER'] assert_equal 'q1w2e3', ENV['PGPASSWORD'] end def test_sqlite - dbconsole.expects(:find_cmd_and_exec).with('sqlite', 'db') start(adapter: 'sqlite', database: 'db') assert !aborted + assert_equal ['sqlite', 'db'], dbconsole.find_cmd_and_exec_args end def test_sqlite3 - dbconsole.expects(:find_cmd_and_exec).with('sqlite3', Rails.root.join('db.sqlite3').to_s) start(adapter: 'sqlite3', database: 'db.sqlite3') assert !aborted + assert_equal ['sqlite3', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args end def test_sqlite3_mode - dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '-html', Rails.root.join('db.sqlite3').to_s) start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--mode', 'html']) assert !aborted + assert_equal ['sqlite3', '-html', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args end def test_sqlite3_header - dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '-header', Rails.root.join('db.sqlite3').to_s) start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--header']) + assert_equal ['sqlite3', '-header', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args end def test_sqlite3_db_absolute_path - dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '/tmp/db.sqlite3') start(adapter: 'sqlite3', database: '/tmp/db.sqlite3') assert !aborted + assert_equal ['sqlite3', '/tmp/db.sqlite3'], dbconsole.find_cmd_and_exec_args end def test_sqlite3_db_without_defined_rails_root - Rails.stubs(:respond_to?) - Rails.expects(:respond_to?).with(:root).once.returns(false) - dbconsole.expects(:find_cmd_and_exec).with('sqlite3', Rails.root.join('../config/db.sqlite3').to_s) - start(adapter: 'sqlite3', database: 'config/db.sqlite3') - assert !aborted + Rails.stub(:respond_to?, false) do + start(adapter: 'sqlite3', database: 'config/db.sqlite3') + assert !aborted + assert_equal ['sqlite3', Rails.root.join('../config/db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args + end end def test_oracle - dbconsole.expects(:find_cmd_and_exec).with('sqlplus', 'user@db') start(adapter: 'oracle', database: 'db', username: 'user', password: 'secret') assert !aborted + assert_equal ['sqlplus', 'user@db'], dbconsole.find_cmd_and_exec_args end def test_oracle_include_password - dbconsole.expects(:find_cmd_and_exec).with('sqlplus', 'user/secret@db') start({adapter: 'oracle', database: 'db', username: 'user', password: 'secret'}, ['-p']) assert !aborted + assert_equal ['sqlplus', 'user/secret@db'], dbconsole.find_cmd_and_exec_args end def test_unknown_command_line_client @@ -223,16 +236,27 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase private def app_db_config(results) - Rails.application.config.stubs(:database_configuration).returns(results || {}) + Rails.application.config.stub(:database_configuration, results || {}) do + yield + end end def dbconsole - @dbconsole ||= Rails::DBConsole.new(nil) + @dbconsole ||= Class.new(Rails::DBConsole) do + attr_reader :find_cmd_and_exec_args + + def find_cmd_and_exec(*args) + @find_cmd_and_exec_args = args + end + end.new(nil) end def start(config = {}, argv = []) - dbconsole.stubs(config: config.stringify_keys, arguments: argv) - capture_abort { dbconsole.start } + dbconsole.stub(:config, config.stringify_keys) do + dbconsole.stub(:arguments, argv) do + capture_abort { dbconsole.start } + end + end end def capture_abort diff --git a/railties/test/engine_test.rb b/railties/test/engine_test.rb index 7970913d21..f46fb748f5 100644 --- a/railties/test/engine_test.rb +++ b/railties/test/engine_test.rb @@ -11,4 +11,15 @@ class EngineTest < ActiveSupport::TestCase assert !engine.routes? end + + def test_application_can_be_subclassed + klass = Class.new(Rails::Application) do + attr_reader :hello + def initialize + @hello = "world" + super + end + end + assert_equal "world", klass.instance.hello + end end diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index 6d6de0fb52..a4337926d1 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -1,6 +1,7 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/app/app_generator' require 'env_helpers' +require 'mocha/setup' # FIXME: stop using mocha class ActionsTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -242,7 +243,7 @@ class ActionsTest < Rails::Generators::TestCase protected def action(*args, &block) - silence(:stdout){ generator.send(*args, &block) } + capture(:stdout){ generator.send(*args, &block) } end end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 2ac5410960..70c439672f 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -1,6 +1,7 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/app/app_generator' require 'generators/shared_generator_tests' +require 'mocha/setup' # FIXME: stop using mocha DEFAULT_APP_FILES = %w( .gitignore @@ -193,20 +194,20 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_config_database_is_added_by_default run_generator assert_file "config/database.yml", /sqlite3/ - unless defined?(JRUBY_VERSION) - assert_gem "sqlite3" - else + if defined?(JRUBY_VERSION) assert_gem "activerecord-jdbcsqlite3-adapter" + else + assert_gem "sqlite3" end end def test_config_another_database run_generator([destination_root, "-d", "mysql"]) assert_file "config/database.yml", /mysql/ - unless defined?(JRUBY_VERSION) - assert_gem "mysql2" - else + if defined?(JRUBY_VERSION) assert_gem "activerecord-jdbcmysql-adapter" + else + assert_gem "mysql2" end end @@ -218,10 +219,10 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_config_postgresql_database run_generator([destination_root, "-d", "postgresql"]) assert_file "config/database.yml", /postgresql/ - unless defined?(JRUBY_VERSION) - assert_gem "pg" - else + if defined?(JRUBY_VERSION) assert_gem "activerecord-jdbcpostgresql-adapter" + else + assert_gem "pg" end end @@ -250,9 +251,9 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_gem "activerecord-jdbc-adapter" end - def test_config_jdbc_database_when_no_option_given - if defined?(JRUBY_VERSION) - run_generator([destination_root]) + if defined?(JRUBY_VERSION) + def test_config_jdbc_database_when_no_option_given + run_generator assert_file "config/database.yml", /sqlite3/ assert_gem "activerecord-jdbcsqlite3-adapter" end @@ -294,11 +295,11 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_inclusion_of_javascript_runtime - run_generator([destination_root]) + run_generator if defined?(JRUBY_VERSION) assert_gem "therubyrhino" else - assert_file "Gemfile", /# gem\s+["']therubyracer["']+, \s+platforms: :ruby$/ + assert_file "Gemfile", /# gem 'therubyracer', platforms: :ruby/ end end @@ -339,7 +340,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_inclusion_of_jbuilder run_generator - assert_file "Gemfile", /gem 'jbuilder'/ + assert_gem 'jbuilder' end def test_inclusion_of_a_debugger @@ -350,9 +351,9 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_match(/debugger/, content) end elsif RUBY_VERSION < '2.0.0' - assert_file "Gemfile", /# gem 'debugger'/ + assert_gem 'debugger' else - assert_file "Gemfile", /# gem 'byebug'/ + assert_gem 'byebug' end end @@ -397,7 +398,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end def test_new_hash_style - run_generator [destination_root] + run_generator assert_file "config/initializers/session_store.rb" do |file| assert_match(/config.session_store :cookie_store, key: '_.+_session'/, file) end @@ -418,9 +419,14 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "foo bar/config/initializers/session_store.rb", /key: '_foo_bar/ end + def test_web_console + run_generator + assert_gem 'web-console' + end + def test_spring run_generator - assert_file "Gemfile", /gem 'spring', \s+group: :development/ + assert_gem 'spring' end def test_spring_binstubs @@ -487,13 +493,41 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_psych_gem + run_generator + gem_regex = /gem 'psych',\s+'~> 2.0', \s+platforms: :rbx/ + + assert_file "Gemfile" do |content| + if defined?(Rubinius) + assert_match(gem_regex, content) + else + assert_no_match(gem_regex, content) + end + end + end + + def test_after_bundle_callback + path = 'http://example.org/rails_template' + template = %{ after_bundle { run 'echo ran after_bundle' } } + template.instance_eval "def read; self; end" # Make the string respond to read + + generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) + + bundler_first = sequence('bundle, binstubs, after_bundle') + generator.expects(:bundle_command).with('install').once.in_sequence(bundler_first) + generator.expects(:bundle_command).with('exec spring binstub --all').in_sequence(bundler_first) + generator.expects(:run).with('echo ran after_bundle').in_sequence(bundler_first) + + quietly { generator.invoke_all } + end + protected def action(*args, &block) - silence(:stdout) { generator.send(*args, &block) } + capture(:stdout) { generator.send(*args, &block) } end def assert_gem(gem) - assert_file "Gemfile", /^gem\s+["']#{gem}["']$/ + assert_file "Gemfile", /^\s*gem\s+["']#{gem}["']$*/ end end diff --git a/railties/test/generators/argv_scrubber_test.rb b/railties/test/generators/argv_scrubber_test.rb index 31c2d846e2..31e07bc8da 100644 --- a/railties/test/generators/argv_scrubber_test.rb +++ b/railties/test/generators/argv_scrubber_test.rb @@ -1,5 +1,5 @@ -require 'active_support/testing/autorun' require 'active_support/test_case' +require 'active_support/testing/autorun' require 'rails/generators/rails/app/app_generator' require 'tempfile' diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb index c906d56b8f..a7d56dd352 100644 --- a/railties/test/generators/controller_generator_test.rb +++ b/railties/test/generators/controller_generator_test.rb @@ -28,13 +28,11 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_invokes_helper run_generator assert_file "app/helpers/account_helper.rb" - assert_file "test/helpers/account_helper_test.rb" end def test_does_not_invoke_helper_if_required run_generator ["account", "--skip-helper"] assert_no_file "app/helpers/account_helper.rb" - assert_no_file "test/helpers/account_helper_test.rb" end def test_invokes_assets @@ -73,7 +71,7 @@ class ControllerGeneratorTest < Rails::Generators::TestCase def test_skip_routes run_generator ["account", "foo", "--skip-routes"] assert_file "config/routes.rb" do |routes| - assert_no_match /get 'account\/foo'/, routes + assert_no_match(/get 'account\/foo'/, routes) end end diff --git a/railties/test/generators/generator_test.rb b/railties/test/generators/generator_test.rb index b136239795..7871399dd7 100644 --- a/railties/test/generators/generator_test.rb +++ b/railties/test/generators/generator_test.rb @@ -1,5 +1,5 @@ -require 'active_support/testing/autorun' require 'active_support/test_case' +require 'active_support/testing/autorun' require 'rails/generators/app_base' module Rails diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index 77ec2f1c0c..e7990de754 100644 --- a/railties/test/generators/generators_test_helper.rb +++ b/railties/test/generators/generators_test_helper.rb @@ -7,7 +7,7 @@ module Rails class << self remove_possible_method :root def root - @root ||= File.expand_path(File.join(File.dirname(__FILE__), '..', 'fixtures')) + @root ||= File.expand_path('../../fixtures', __FILE__) end end end @@ -41,4 +41,12 @@ module GeneratorsTestHelper FileUtils.mkdir_p(destination) FileUtils.cp routes, destination end + + def quietly + silence_stream(STDOUT) do + silence_stream(STDERR) do + yield + end + end + end end diff --git a/railties/test/generators/helper_generator_test.rb b/railties/test/generators/helper_generator_test.rb index 81d4fcb129..add04f21a4 100644 --- a/railties/test/generators/helper_generator_test.rb +++ b/railties/test/generators/helper_generator_test.rb @@ -13,26 +13,11 @@ class HelperGeneratorTest < Rails::Generators::TestCase assert_file "app/helpers/admin_helper.rb", /module AdminHelper/ end - def test_invokes_default_test_framework - run_generator - assert_file "test/helpers/admin_helper_test.rb", /class AdminHelperTest < ActionView::TestCase/ - end - - def test_logs_if_the_test_framework_cannot_be_found - content = run_generator ["admin", "--test-framework=rspec"] - assert_match(/rspec \[not found\]/, content) - end - def test_check_class_collision content = capture(:stderr){ run_generator ["object"] } assert_match(/The name 'ObjectHelper' is either already used in your application or reserved/, content) end - def test_check_class_collision_on_tests - content = capture(:stderr){ run_generator ["another_object"] } - assert_match(/The name 'AnotherObjectHelperTest' is either already used in your application or reserved/, content) - end - def test_namespaced_and_not_namespaced_helpers run_generator ["products"] diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb index 6fac643ed0..72f5fe29ca 100644 --- a/railties/test/generators/migration_generator_test.rb +++ b/railties/test/generators/migration_generator_test.rb @@ -159,6 +159,18 @@ class MigrationGeneratorTest < Rails::Generators::TestCase end end + def test_add_migration_with_required_references + migration = "add_references_to_books" + run_generator [migration, "author:belongs_to{required}", "distributor:references{polymorphic,required}"] + + assert_migration "db/migrate/#{migration}.rb" do |content| + assert_method :change, content do |change| + assert_match(/add_reference :books, :author, index: true, null: false/, change) + assert_match(/add_reference :books, :distributor, polymorphic: true, index: true, null: false/, change) + end + end + end + def test_create_join_table_migration migration = "add_media_join_table" run_generator [migration, "artist_id", "musics:uniq"] diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index b67cf02d7b..c78597c81b 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -1,5 +1,6 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/model/model_generator' +require 'active_support/core_ext/string/strip' class ModelGeneratorTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -222,7 +223,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_migration_with_timestamps run_generator - assert_migration "db/migrate/create_accounts.rb", /t.timestamps/ + assert_migration "db/migrate/create_accounts.rb", /t.timestamps null: false/ end def test_migration_timestamps_are_skipped @@ -363,6 +364,49 @@ class ModelGeneratorTest < Rails::Generators::TestCase end end + def test_required_belongs_to_adds_required_association + run_generator ["account", "supplier:references{required}"] + + expected_file = <<-FILE.strip_heredoc + class Account < ActiveRecord::Base + belongs_to :supplier, required: true + end + FILE + assert_file "app/models/account.rb", expected_file + end + + def test_required_polymorphic_belongs_to_generages_correct_model + run_generator ["account", "supplier:references{required,polymorphic}"] + + expected_file = <<-FILE.strip_heredoc + class Account < ActiveRecord::Base + belongs_to :supplier, polymorphic: true, required: true + end + FILE + assert_file "app/models/account.rb", expected_file + end + + def test_required_and_polymorphic_are_order_independent + run_generator ["account", "supplier:references{polymorphic.required}"] + + expected_file = <<-FILE.strip_heredoc + class Account < ActiveRecord::Base + belongs_to :supplier, polymorphic: true, required: true + end + FILE + assert_file "app/models/account.rb", expected_file + end + + def test_required_adds_null_false_to_column + run_generator ["account", "supplier:references{required}"] + + assert_migration "db/migrate/create_accounts.rb" do |m| + assert_method :change, m do |up| + assert_match(/t\.references :supplier,.*\snull: false/, up) + end + end + end + private def assert_generated_fixture(path, parsed_contents) fixture_file = File.new File.expand_path(path, destination_root) diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb index ac5cfff229..4199e00b0d 100644 --- a/railties/test/generators/named_base_test.rb +++ b/railties/test/generators/named_base_test.rb @@ -1,5 +1,6 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator' +require 'mocha/setup' # FIXME: stop using mocha # Mock out what we need from AR::Base. module ActiveRecord diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index d677c21f15..7eeb084eab 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -47,7 +47,6 @@ class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase def test_helper_is_also_namespaced run_generator assert_file "app/helpers/test_app/account_helper.rb", /module TestApp/, / module AccountHelper/ - assert_file "test/helpers/test_app/account_helper_test.rb", /module TestApp/, / class AccountHelperTest/ end def test_invokes_default_test_framework @@ -229,7 +228,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Helpers assert_file "app/helpers/test_app/product_lines_helper.rb" - assert_file "test/helpers/test_app/product_lines_helper_test.rb" # Stylesheets assert_file "app/assets/stylesheets/scaffold.css" @@ -260,7 +258,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Helpers assert_no_file "app/helpers/test_app/product_lines_helper.rb" - assert_no_file "test/helpers/test_app/product_lines_helper_test.rb" # Stylesheets (should not be removed) assert_file "app/assets/stylesheets/scaffold.css" @@ -297,7 +294,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Helpers assert_file "app/helpers/test_app/admin/roles_helper.rb" - assert_file "test/helpers/test_app/admin/roles_helper_test.rb" # Stylesheets assert_file "app/assets/stylesheets/scaffold.css" @@ -329,7 +325,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Helpers assert_no_file "app/helpers/test_app/admin/roles_helper.rb" - assert_no_file "test/helpers/test_app/admin/roles_helper_test.rb" # Stylesheets (should not be removed) assert_file "app/assets/stylesheets/scaffold.css" @@ -366,7 +361,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Helpers assert_file "app/helpers/test_app/admin/user/special/roles_helper.rb" - assert_file "test/helpers/test_app/admin/user/special/roles_helper_test.rb" # Stylesheets assert_file "app/assets/stylesheets/scaffold.css" @@ -397,7 +391,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Helpers assert_no_file "app/helpers/test_app/admin/user/special/roles_helper.rb" - assert_no_file "test/helpers/test_app/admin/user/special/roles_helper_test.rb" # Stylesheets (should not be removed) assert_file "app/assets/stylesheets/scaffold.css" diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 7180efee41..985644e8af 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -1,6 +1,7 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/plugin/plugin_generator' require 'generators/shared_generator_tests' +require 'mocha/setup' # FIXME: stop using mocha DEFAULT_PLUGIN_FILES = %w( .gitignore @@ -94,7 +95,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase end def test_generating_adds_dummy_app_without_javascript_and_assets_deps - run_generator [destination_root] + run_generator assert_file "test/dummy/app/assets/stylesheets/application.css" @@ -334,7 +335,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase Object.const_set('APP_PATH', Rails.root) FileUtils.touch gemfile_path - run_generator [destination_root] + run_generator assert_file gemfile_path, /gem 'bukkits', path: 'tmp\/bukkits'/ ensure @@ -375,19 +376,19 @@ class PluginGeneratorTest < Rails::Generators::TestCase name = `git config user.name`.chomp rescue "TODO: Write your name" email = `git config user.email`.chomp rescue "TODO: Write your email address" - run_generator [destination_root] + run_generator assert_file "bukkits.gemspec" do |contents| - assert_match(/#{Regexp.escape(name)}/, contents) - assert_match(/#{Regexp.escape(email)}/, contents) + assert_match name, contents + assert_match email, contents end end def test_git_name_in_license_file name = `git config user.name`.chomp rescue "TODO: Write your name" - run_generator [destination_root] + run_generator assert_file "MIT-LICENSE" do |contents| - assert_match(/#{Regexp.escape(name)}/, contents) + assert_match name, contents end end @@ -397,11 +398,11 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, '--skip-git'] assert_file "MIT-LICENSE" do |contents| - assert_match(/#{Regexp.escape(name)}/, contents) + assert_match name, contents end assert_file "bukkits.gemspec" do |contents| - assert_match(/#{Regexp.escape(name)}/, contents) - assert_match(/#{Regexp.escape(email)}/, contents) + assert_match name, contents + assert_match email, contents end end @@ -415,10 +416,10 @@ protected end def assert_match_sqlite3(contents) - unless defined?(JRUBY_VERSION) - assert_match(/group :development do\n gem 'sqlite3'\nend/, contents) - else + if defined?(JRUBY_VERSION) assert_match(/group :development do\n gem 'activerecord-jdbcsqlite3-adapter'\nend/, contents) + else + assert_match(/group :development do\n gem 'sqlite3'\nend/, contents) end end end diff --git a/railties/test/generators/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb index dcdff22152..581d80d60e 100644 --- a/railties/test/generators/resource_generator_test.rb +++ b/railties/test/generators/resource_generator_test.rb @@ -36,7 +36,6 @@ class ResourceGeneratorTest < Rails::Generators::TestCase assert_file "test/controllers/accounts_controller_test.rb", /class AccountsControllerTest < ActionController::TestCase/ assert_file "app/helpers/accounts_helper.rb", /module AccountsHelper/ - assert_file "test/helpers/accounts_helper_test.rb", /class AccountsHelperTest < ActionView::TestCase/ end def test_resource_controller_with_actions diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb index 3c1123b53d..ca972a3bdd 100644 --- a/railties/test/generators/scaffold_controller_generator_test.rb +++ b/railties/test/generators/scaffold_controller_generator_test.rb @@ -81,7 +81,6 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase def test_helper_are_invoked_with_a_pluralized_name run_generator assert_file "app/helpers/users_helper.rb", /module UsersHelper/ - assert_file "test/helpers/users_helper_test.rb", /class UsersHelperTest < ActionView::TestCase/ end def test_views_are_generated @@ -93,6 +92,14 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_no_file "app/views/layouts/users.html.erb" end + def test_index_page_have_notice + run_generator + + %w(index show).each do |view| + assert_file "app/views/users/#{view}.html.erb", /notice/ + end + end + def test_functional_tests run_generator ["User", "name:string", "age:integer", "organization:references{polymorphic}"] @@ -118,7 +125,6 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase def test_skip_helper_if_required run_generator ["User", "name:string", "age:integer", "--no-helper"] assert_no_file "app/helpers/users_helper.rb" - assert_no_file "test/helpers/users_helper_test.rb" end def test_skip_layout_if_required diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index 524bbde2b7..637bde2a44 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -70,7 +70,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Helpers assert_file "app/helpers/product_lines_helper.rb" - assert_file "test/helpers/product_lines_helper_test.rb" # Assets assert_file "app/assets/stylesheets/scaffold.css" @@ -114,7 +113,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Helpers assert_no_file "app/helpers/product_lines_helper.rb" - assert_no_file "test/helpers/product_lines_helper_test.rb" # Assets assert_file "app/assets/stylesheets/scaffold.css", /:visited/ @@ -182,7 +180,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Helpers assert_file "app/helpers/admin/roles_helper.rb" - assert_file "test/helpers/admin/roles_helper_test.rb" # Assets assert_file "app/assets/stylesheets/scaffold.css", /:visited/ @@ -216,7 +213,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Helpers assert_no_file "app/helpers/admin/roles_helper.rb" - assert_no_file "test/helpers/admin/roles_helper_test.rb" # Assets assert_file "app/assets/stylesheets/scaffold.css" diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 0b4edafaca..127e059bf1 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -1,6 +1,7 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/model/model_generator' require 'rails/generators/test_unit/model/model_generator' +require 'mocha/setup' # FIXME: stop using mocha class GeneratorsTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -151,6 +152,8 @@ class GeneratorsTest < Rails::Generators::TestCase klass = Rails::Generators.find_by_namespace(:plugin, :remarkable) assert klass assert_equal "test_unit:plugin", klass.namespace + ensure + Rails::Generators.fallbacks.delete(:remarkable) end def test_fallbacks_for_generators_on_find_by_namespace_with_context @@ -158,18 +161,26 @@ class GeneratorsTest < Rails::Generators::TestCase klass = Rails::Generators.find_by_namespace(:remarkable, :rails, :plugin) assert klass assert_equal "test_unit:plugin", klass.namespace + ensure + Rails::Generators.fallbacks.delete(:remarkable) end def test_fallbacks_for_generators_on_invoke Rails::Generators.fallbacks[:shoulda] = :test_unit TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {}) Rails::Generators.invoke "shoulda:model", ["Account"] + ensure + Rails::Generators.fallbacks.delete(:shoulda) end def test_nested_fallbacks_for_generators + Rails::Generators.fallbacks[:shoulda] = :test_unit Rails::Generators.fallbacks[:super_shoulda] = :shoulda TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {}) Rails::Generators.invoke "super_shoulda:model", ["Account"] + ensure + Rails::Generators.fallbacks.delete(:shoulda) + Rails::Generators.fallbacks.delete(:super_shoulda) end def test_developer_options_are_overwritten_by_user_options diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 6c50911666..92d6a1729c 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -291,6 +291,33 @@ class ActiveSupport::TestCase include TestHelpers::Paths include TestHelpers::Rack include TestHelpers::Generation + + private + + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) + end + + def quietly + silence_stream(STDOUT) do + silence_stream(STDERR) do + yield + end + end + end end # Create a scope and build a fixture rails app diff --git a/railties/test/path_generation_test.rb b/railties/test/path_generation_test.rb new file mode 100644 index 0000000000..13bf29d3c3 --- /dev/null +++ b/railties/test/path_generation_test.rb @@ -0,0 +1,88 @@ +# encoding: utf-8 +require 'abstract_unit' +require 'active_support/core_ext/object/with_options' +require 'active_support/core_ext/object/json' +require 'rails' +require 'rails/application' + +ROUTING = ActionDispatch::Routing + +class PathGenerationTest < ActiveSupport::TestCase + attr_reader :app + + class TestSet < ROUTING::RouteSet + def initialize(block) + @block = block + super() + end + + class Dispatcher < ROUTING::RouteSet::Dispatcher + def initialize(defaults, set, block) + super(defaults) + @block = block + @set = set + end + + def controller_reference(controller_param) + block = @block + set = @set + Class.new(ActionController::Base) { + include set.url_helpers + define_method(:process) { |name| block.call(self) } + def to_a; [200, {}, []]; end + } + end + end + + def dispatcher defaults + TestSet::Dispatcher.new defaults, self, @block + end + end + + def send_request(uri_or_host, method, path, script_name = nil) + host = uri_or_host.host unless path + path ||= uri_or_host.path + + params = {'PATH_INFO' => path, + 'REQUEST_METHOD' => method, + 'HTTP_HOST' => host } + + params['SCRIPT_NAME'] = script_name if script_name + + status, headers, body = app.call(params) + new_body = [] + body.each { |part| new_body << part } + body.close if body.respond_to? :close + [status, headers, new_body] + end + + def test_original_script_name + original_logger = Rails.logger + Rails.logger = Logger.new nil + + app = Class.new(Rails::Application) { + attr_accessor :controller + def initialize + super + app = self + @routes = TestSet.new ->(c) { app.controller = c } + secrets.secret_key_base = "foo" + secrets.secret_token = "foo" + end + def app; routes; end + } + + @app = app + app.routes.draw { resource :blogs } + + url = URI("http://example.org/blogs") + + send_request(url, 'GET', nil, '/FOO') + assert_equal '/FOO/blogs', app.instance.controller.blogs_path + + send_request(url, 'GET', nil) + assert_equal '/blogs', app.instance.controller.blogs_path + ensure + Rails.logger = original_logger + end +end diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb index ed4559ec6f..1aeb9ec339 100644 --- a/railties/test/paths_test.rb +++ b/railties/test/paths_test.rb @@ -1,5 +1,6 @@ require 'abstract_unit' require 'rails/paths' +require 'mocha/setup' # FIXME: stop using mocha class PathsTest < ActiveSupport::TestCase def setup diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb index 6ebd47fff9..fcc79b57fb 100644 --- a/railties/test/rack_logger_test.rb +++ b/railties/test/rack_logger_test.rb @@ -39,11 +39,11 @@ module Rails def setup @subscriber = Subscriber.new @notifier = ActiveSupport::Notifications.notifier - notifier.subscribe 'request.action_dispatch', subscriber + @subscription = notifier.subscribe 'request.action_dispatch', subscriber end def teardown - notifier.unsubscribe subscriber + notifier.unsubscribe @subscription end def test_notification diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index a9b237d0a5..8d61af4972 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'mocha/setup' # FIXME: stop using mocha module ActionController class Base diff --git a/tasks/release.rb b/tasks/release.rb index 767feaf236..de05dfad99 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -1,4 +1,4 @@ -FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack actionmailer railties ) +FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack actionmailer railties activejob ) root = File.expand_path('../../', __FILE__) version = File.read("#{root}/RAILS_VERSION").strip diff --git a/tools/line_statistics b/tools/line_statistics new file mode 100755 index 0000000000..5eb5cfdc3d --- /dev/null +++ b/tools/line_statistics @@ -0,0 +1,41 @@ +#!/usr/bin/env ruby +# Example: +# files = FileList["lib/active_record/**/*.rb"] +# CodeTools::LineStatistics.new(files).print_loc +module CodeTools + class LineStatistics + + # @param files [Array, FileList, Enumerable] + # e.g. FileList["lib/active_record/**/*.rb"] + def initialize(files) + @files = Array(files).compact + end + + # Calculates LOC for each file + # Outputs each file and a total LOC + def print_loc + lines, codelines, total_lines, total_codelines = 0, 0, 0, 0 + + @files.each do |file_name| + next if file_name =~ /vendor/ + File.open(file_name, 'r') do |f| + while line = f.gets + lines += 1 + next if line =~ /^\s*$/ + next if line =~ /^\s*#/ + codelines += 1 + end + end + puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}" + + total_lines += lines + total_codelines += codelines + + lines, codelines = 0, 0 + end + + puts "Total: Lines #{total_lines}, LOC #{total_codelines}" + end + + end +end diff --git a/tools/profile b/tools/profile index fbea67492b..a35dd18b77 100755 --- a/tools/profile +++ b/tools/profile @@ -1,71 +1,133 @@ #!/usr/bin/env ruby # Example: -# tools/profile activesupport/lib/active_support.rb +# tools/profile activesupport/lib/active_support.rb [ruby-prof mode] [ruby-prof printer] ENV['NO_RELOAD'] ||= '1' ENV['RAILS_ENV'] ||= 'development' -require 'benchmark' +module CodeTools + class Profiler + Error = Class.new(StandardError) -module RequireProfiler - private - def require(file, *args) RequireProfiler.profile(file) { super } end - def load(file, *args) RequireProfiler.profile(file) { super } end + attr_reader :path, :mode + def initialize(path, mode=nil) + assert_ruby_file_exists(path) + @path, @mode = path, mode + require 'benchmark' + end + + def profile_requires + GC.start + before_rss = `ps -o rss= -p #{Process.pid}`.to_i + + if mode + require 'ruby-prof' + RubyProf.measure_mode = RubyProf.const_get(mode.upcase) + RubyProf.start + else + Object.instance_eval { include RequireProfiler } + end + + elapsed = Benchmark.realtime { require path } + results = RubyProf.stop if mode - @depth, @stats = 0, [] - class << self - attr_accessor :depth - attr_accessor :stats + GC.start + after_rss = `ps -o rss= -p #{Process.pid}`.to_i - def profile(file) - stats << [file, depth] - self.depth += 1 - result = nil - elapsed = Benchmark.realtime { result = yield } - self.depth -= 1 - stats.pop if stats.last.first == file - stats << [file, depth, elapsed] if result - result + if mode + if printer = ARGV.shift + puts "RubyProf outputting to stderr with printer #{printer}" + RubyProf.const_get("#{printer.to_s.classify}Printer").new(results).print($stdout) + elsif RubyProf.const_defined?(:CallStackPrinter) + filename = "#{File.basename(path, '.rb')}.#{mode}.html" + puts "RubyProf outputting to #{filename}" + File.open(filename, 'w') do |out| + RubyProf::CallStackPrinter.new(results).print(out) + end + else + filename = "#{File.basename(path, '.rb')}.#{mode}.callgrind" + puts "RubyProf outputting to #{filename}" + File.open(filename, 'w') do |out| + RubyProf::CallTreePrinter.new(results).print(out) + end + end + end + + RequireProfiler.stats.each do |file, depth, sec| + if sec + puts "%8.1f ms %s%s" % [sec * 1000, ' ' * depth, file] + else + puts "#{' ' * (13 + depth)}#{file}" + end + end + puts "%8.1f ms %d KB RSS" % [elapsed * 1000, after_rss - before_rss] end - end -end -GC.start -before_rss = `ps -o rss= -p #{Process.pid}`.to_i + private -path = ARGV.shift -if mode = ARGV.shift - require 'ruby-prof' - RubyProf.measure_mode = RubyProf.const_get(mode.upcase) - RubyProf.start -else - Object.instance_eval { include RequireProfiler } -end + def assert_ruby_file_exists(path) + fail Error.new("No such file") unless File.exists?(path) + fail Error.new("#{path} is a directory") if File.directory?(path) + ruby_extension = File.extname(path) == '.rb' + ruby_executable = File.open(path, 'rb') {|f| f.readline } =~ [/\A#!.*ruby/] + fail Error.new("Not a ruby file") unless ruby_extension or ruby_executable + end -elapsed = Benchmark.realtime { require path } -results = RubyProf.stop if mode + module RequireProfiler + private + def require(file, *args) RequireProfiler.profile(file) { super } end + def load(file, *args) RequireProfiler.profile(file) { super } end -GC.start -after_rss = `ps -o rss= -p #{Process.pid}`.to_i + @depth, @stats = 0, [] + class << self + attr_accessor :depth + attr_accessor :stats -if mode - if printer = ARGV.shift - RubyProf.const_get("#{printer.to_s.classify}Printer").new(results).print($stdout) - elsif RubyProf.const_defined?(:CallStackPrinter) - File.open("#{File.basename(path, '.rb')}.#{mode}.html", 'w') do |out| - RubyProf::CallStackPrinter.new(results).print(out) + def profile(file) + stats << [file, depth] + self.depth += 1 + result = nil + elapsed = Benchmark.realtime { result = yield } + self.depth -= 1 + stats.pop if stats.last.first == file + stats << [file, depth, elapsed] if result + result + end + end end - else - File.open("#{File.basename(path, '.rb')}.#{mode}.callgrind", 'w') do |out| - RubyProf::CallTreePrinter.new(results).print(out) + end +end +# ruby-prof printer name causes the third arg to be sent :classify +# which is probably overkill if you already know the name of the ruby-prof +# printer you want to use, e.g. Graph +begin + require 'active_support/inflector' + require 'active_support/core_ext/string/inflections' +rescue LoadError + STDERR.puts $!.message + class String + # File activesupport/lib/active_support/inflector/methods.rb, line 150 + def classify + # strip out any leading schema name + camelize(self.sub(/.*\./, '')) + end + # File activesupport/lib/active_support/inflector/methods.rb, line 68 + def camelize(uppercase_first_letter = true) + string = self + if uppercase_first_letter + string = string.sub(/^[a-z\d]*/) { $&.capitalize } + else + string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { $&.downcase } + end + string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::') end end end - -RequireProfiler.stats.each do |file, depth, sec| - if sec - puts "%8.1f ms %s%s" % [sec * 1000, ' ' * depth, file] +if $0 == __FILE__ + if (filename = ARGV.shift) + path = File.expand_path(filename) + mode = ARGV.shift + CodeTools::Profiler.new(path, mode).profile_requires else - puts "#{' ' * (13 + depth)}#{file}" + STDERR.puts "No file path entered. Usage is tools/profile path/to/file.rb [ruby-prof mode] [ruby-prof printer]" end end -puts "%8.1f ms %d KB RSS" % [elapsed * 1000, after_rss - before_rss] |